Rust · 28691 bytes Raw Blame History
1 //! Annotation overlay window using gartk-x11.
2
3 use anyhow::{Context, Result};
4
5 use crate::annotate::canvas::AnnotationCanvas;
6 use crate::annotate::history::{History, Snapshot};
7 use crate::annotate::state::{AnnotationResult, AnnotationState, ToolType};
8 use crate::annotate::tools::{self, Tool};
9 use crate::annotate::ui::{
10 color_picker::{ColorPicker, ColorPickerResult},
11 Toolbar, ToolbarClickResult, TOOLBAR_HEIGHT,
12 };
13
14 use gartk_core::{InputEvent, Key, KeyEvent, Modifiers, MouseButton, MouseEvent, Point};
15 use gartk_render::Surface;
16 use gartk_x11::{Connection, CursorManager, Window, WindowConfig};
17 use x11rb::connection::Connection as X11Connection;
18 use x11rb::protocol::xproto::{self, AtomEnum, ConnectionExt, ImageFormat, PropMode};
19 use x11rb::wrapper::ConnectionExt as WrapperConnectionExt;
20
21 /// Maximum bytes per put_image request (conservative, below typical 256KB limit).
22 const MAX_PUT_IMAGE_BYTES: usize = 65536;
23
24 /// Annotation overlay for editing screenshots.
25 pub struct AnnotationOverlay {
26 /// X11 connection.
27 conn: Connection,
28 /// Overlay window.
29 window: Window,
30 /// Graphics context for blitting.
31 gc: u32,
32 /// Annotation canvas.
33 canvas: AnnotationCanvas,
34 /// Annotation state.
35 state: AnnotationState,
36 /// Current tool instance.
37 tool: Box<dyn Tool>,
38 /// Undo/redo history.
39 history: History,
40 /// Toolbar UI.
41 toolbar: Toolbar,
42 /// Toolbar surface (reused to avoid allocation each frame).
43 toolbar_surface: Surface,
44 /// Cursor manager.
45 cursor_manager: CursorManager,
46 /// Whether a redraw is needed.
47 needs_redraw: bool,
48 /// Image offset from top (for toolbar).
49 image_offset_y: i32,
50 /// Color picker dialog (when open).
51 color_picker: Option<ColorPicker>,
52 /// Color picker surface (reused to avoid allocation).
53 color_picker_surface: Option<Surface>,
54 }
55
56 impl AnnotationOverlay {
57 /// Create a new annotation overlay.
58 ///
59 /// # Arguments
60 /// * `image_data` - RGBA pixel data of the screenshot
61 /// * `width` - Image width
62 /// * `height` - Image height
63 pub fn new(image_data: &[u8], width: u32, height: u32) -> Result<Self> {
64 tracing::debug!("Creating annotation overlay for image {}x{}", width, height);
65
66 // Connect to X11
67 let conn = Connection::connect(None).context("Failed to connect to X11")?;
68
69 // Window size = image size + toolbar height
70 let window_width = width;
71 let window_height = height + TOOLBAR_HEIGHT;
72 tracing::debug!("Window size will be {}x{} (including {}px toolbar)",
73 window_width, window_height, TOOLBAR_HEIGHT);
74
75 // Center window on screen
76 let screen_width = conn.screen_width();
77 let screen_height = conn.screen_height();
78 let pos_x = (screen_width as i32 - window_width as i32) / 2;
79 let pos_y = (screen_height as i32 - window_height as i32) / 2;
80
81 // Create floating window (no override_redirect so gar can manage it)
82 let config = WindowConfig::new()
83 .title("garshot annotation")
84 .class("garshot-annotate")
85 .size(window_width, window_height)
86 .position(pos_x.max(0), pos_y.max(0))
87 .map_on_create(false);
88
89 let window = Window::create(conn.clone(), config).context("Failed to create window")?;
90 tracing::debug!("Created window {} at position ({}, {})",
91 window.id(), pos_x.max(0), pos_y.max(0));
92
93 // Set window type to DIALOG so gar floats it automatically
94 let window_type_atom = conn.inner()
95 .intern_atom(false, b"_NET_WM_WINDOW_TYPE")?
96 .reply()
97 .context("Failed to intern window type atom")?
98 .atom;
99 let dialog_type_atom = conn.inner()
100 .intern_atom(false, b"_NET_WM_WINDOW_TYPE_DIALOG")?
101 .reply()
102 .context("Failed to intern dialog type atom")?
103 .atom;
104 conn.inner().change_property32(
105 PropMode::REPLACE,
106 window.id(),
107 window_type_atom,
108 AtomEnum::ATOM,
109 &[dialog_type_atom],
110 )?;
111
112 // Create GC for blitting
113 let gc = conn.generate_id()?;
114 conn.inner()
115 .create_gc(gc, window.id(), &Default::default())?;
116
117 // Create canvas
118 let canvas =
119 AnnotationCanvas::new(image_data, width, height).context("Failed to create canvas")?;
120
121 // Create toolbar (sized to image width)
122 let toolbar = Toolbar::new(width);
123
124 // Create cursor manager
125 let cursor_manager =
126 CursorManager::new(conn.clone()).context("Failed to create cursor manager")?;
127
128 // Create initial tool
129 let tool = tools::create_tool(ToolType::Arrow);
130
131 // Create reusable toolbar surface
132 let toolbar_surface = Surface::new(width, TOOLBAR_HEIGHT)
133 .context("Failed to create toolbar surface")?;
134
135 // Set WM_NORMAL_HINTS to lock window size (prevents WM from resizing)
136 // Flags: PMinSize (16) | PMaxSize (32) | PSize (8) = 56
137 let size_hints: [u32; 18] = [
138 56, // flags: PSize | PMinSize | PMaxSize
139 0, 0, // x, y (obsolete)
140 window_width, window_height, // width, height (PSize)
141 window_width, window_height, // min_width, min_height (PMinSize)
142 window_width, window_height, // max_width, max_height (PMaxSize)
143 0, 0, // width_inc, height_inc
144 0, 0, // min_aspect_num, min_aspect_den
145 0, 0, // max_aspect_num, max_aspect_den
146 0, 0, // base_width, base_height
147 0, // win_gravity
148 ];
149 conn.inner().change_property32(
150 PropMode::REPLACE,
151 window.id(),
152 AtomEnum::WM_NORMAL_HINTS,
153 AtomEnum::WM_SIZE_HINTS,
154 &size_hints,
155 )?;
156 tracing::debug!("Set WM_NORMAL_HINTS: min/max {}x{}", window_width, window_height);
157
158 // Tell compositor to bypass this window (reduces effects/lag from picom)
159 let bypass_atom = conn.inner()
160 .intern_atom(false, b"_NET_WM_BYPASS_COMPOSITOR")?
161 .reply()
162 .context("Failed to intern bypass atom")?
163 .atom;
164 conn.inner().change_property32(
165 PropMode::REPLACE,
166 window.id(),
167 bypass_atom,
168 AtomEnum::CARDINAL,
169 &[1], // 1 = bypass compositor
170 )?;
171
172 Ok(Self {
173 conn,
174 window,
175 gc,
176 canvas,
177 state: AnnotationState::new(),
178 tool,
179 history: History::new(),
180 toolbar,
181 toolbar_surface,
182 cursor_manager,
183 needs_redraw: true,
184 image_offset_y: TOOLBAR_HEIGHT as i32,
185 color_picker: None,
186 color_picker_surface: None,
187 })
188 }
189
190 /// Run the annotation overlay event loop.
191 pub fn run(mut self) -> Result<AnnotationResult> {
192 // Set full opacity to prevent picom from making window transparent
193 self.set_full_opacity()?;
194
195 // Map window (gar will manage it as a floating dialog)
196 self.window.map()?;
197
198 // Flush and sync to ensure window is mapped before we try to draw
199 self.conn.inner().flush()?;
200 self.conn.inner().sync()?;
201
202 // Focus and raise window for keyboard input (no grab - allows WM keybinds to work)
203 self.window.focus()?;
204 self.window.raise()?;
205
206 // Set initial cursor
207 self.update_cursor()?;
208
209 // Initial draw
210 self.redraw()?;
211
212 // Flush to ensure initial draw is visible
213 self.conn.inner().flush()?;
214
215 // Event loop
216 loop {
217 // Wait for first event
218 let event = self.conn.inner().wait_for_event()?;
219 let mut needs_redraw = false;
220
221 // Translate and handle the first event
222 if let Some(input_event) = self.translate_event(&event) {
223 needs_redraw |= self.handle_event(input_event)?;
224 }
225
226 // Check if finished after first event
227 if self.state.is_finished() {
228 break;
229 }
230
231 // Process all pending events before redrawing (batching)
232 while let Some(event) = self.conn.inner().poll_for_event()? {
233 if let Some(input_event) = self.translate_event(&event) {
234 needs_redraw |= self.handle_event(input_event)?;
235 }
236 if self.state.is_finished() {
237 break;
238 }
239 }
240
241 // Single redraw for all batched events
242 if needs_redraw {
243 self.redraw()?;
244 }
245
246 // Check if finished
247 if self.state.is_finished() {
248 break;
249 }
250 }
251
252 // Return result
253 Ok(self.state.take_result().unwrap_or(AnnotationResult::Cancel))
254 }
255
256 /// Translate X11 event to InputEvent.
257 fn translate_event(&self, event: &x11rb::protocol::Event) -> Option<InputEvent> {
258 use x11rb::protocol::Event;
259
260 match event {
261 Event::ButtonPress(e) => Some(InputEvent::MousePress(MouseEvent {
262 position: Point::new(e.event_x as i32, e.event_y as i32 - self.image_offset_y),
263 button: match e.detail {
264 1 => Some(MouseButton::Left),
265 2 => Some(MouseButton::Middle),
266 3 => Some(MouseButton::Right),
267 _ => None,
268 },
269 modifiers: self.translate_modifiers(e.state),
270 })),
271 Event::ButtonRelease(e) => Some(InputEvent::MouseRelease(MouseEvent {
272 position: Point::new(e.event_x as i32, e.event_y as i32 - self.image_offset_y),
273 button: match e.detail {
274 1 => Some(MouseButton::Left),
275 2 => Some(MouseButton::Middle),
276 3 => Some(MouseButton::Right),
277 _ => None,
278 },
279 modifiers: self.translate_modifiers(e.state),
280 })),
281 Event::MotionNotify(e) => Some(InputEvent::MouseMove(MouseEvent {
282 position: Point::new(e.event_x as i32, e.event_y as i32 - self.image_offset_y),
283 button: None,
284 modifiers: self.translate_modifiers(e.state),
285 })),
286 Event::KeyPress(e) => {
287 let key = self.translate_keycode(e.detail);
288 Some(InputEvent::Key(KeyEvent {
289 key,
290 keycode: e.detail,
291 pressed: true,
292 modifiers: self.translate_modifiers(e.state),
293 }))
294 }
295 Event::KeyRelease(e) => {
296 let key = self.translate_keycode(e.detail);
297 Some(InputEvent::Key(KeyEvent {
298 key,
299 keycode: e.detail,
300 pressed: false,
301 modifiers: self.translate_modifiers(e.state),
302 }))
303 }
304 Event::Expose(_) => Some(InputEvent::Expose),
305 _ => None,
306 }
307 }
308
309 /// Translate X11 key modifiers.
310 fn translate_modifiers(&self, state: xproto::KeyButMask) -> Modifiers {
311 Modifiers {
312 shift: state.contains(xproto::KeyButMask::SHIFT),
313 ctrl: state.contains(xproto::KeyButMask::CONTROL),
314 alt: state.contains(xproto::KeyButMask::MOD1),
315 super_key: state.contains(xproto::KeyButMask::MOD4),
316 caps_lock: state.contains(xproto::KeyButMask::LOCK),
317 num_lock: state.contains(xproto::KeyButMask::MOD2),
318 }
319 }
320
321 /// Translate X11 keycode to Key.
322 fn translate_keycode(&self, keycode: u8) -> Key {
323 // Common keycodes (evdev-based)
324 match keycode {
325 9 => Key::Escape,
326 36 => Key::Return,
327 22 => Key::Backspace,
328 65 => Key::Space,
329 104 => Key::Return, // Numpad enter
330
331 // Letters (a-z: keycodes 38-58 approximately)
332 38 => Key::Char('a'),
333 56 => Key::Char('b'),
334 54 => Key::Char('c'),
335 40 => Key::Char('d'),
336 26 => Key::Char('e'),
337 41 => Key::Char('f'),
338 42 => Key::Char('g'),
339 43 => Key::Char('h'),
340 31 => Key::Char('i'),
341 44 => Key::Char('j'),
342 45 => Key::Char('k'),
343 46 => Key::Char('l'),
344 58 => Key::Char('m'),
345 57 => Key::Char('n'),
346 32 => Key::Char('o'),
347 33 => Key::Char('p'),
348 24 => Key::Char('q'),
349 27 => Key::Char('r'),
350 39 => Key::Char('s'),
351 28 => Key::Char('t'),
352 30 => Key::Char('u'),
353 55 => Key::Char('v'),
354 25 => Key::Char('w'),
355 53 => Key::Char('x'),
356 29 => Key::Char('y'),
357 52 => Key::Char('z'),
358
359 // Numbers
360 10 => Key::Char('1'),
361 11 => Key::Char('2'),
362 12 => Key::Char('3'),
363 13 => Key::Char('4'),
364 14 => Key::Char('5'),
365 15 => Key::Char('6'),
366 16 => Key::Char('7'),
367 17 => Key::Char('8'),
368 18 => Key::Char('9'),
369 19 => Key::Char('0'),
370
371 // Special characters
372 20 => Key::Char('-'),
373 21 => Key::Char('='),
374
375 _ => Key::Unknown(keycode),
376 }
377 }
378
379 /// Handle an input event.
380 fn handle_event(&mut self, event: InputEvent) -> Result<bool> {
381 let mut needs_redraw = false;
382
383 // If color picker is open, route events to it first
384 if let Some(ref mut picker) = self.color_picker {
385 match picker.handle_event(&event) {
386 ColorPickerResult::Confirm(color) => {
387 self.state.properties.color = color;
388 self.color_picker = None;
389 self.color_picker_surface = None;
390 return Ok(true);
391 }
392 ColorPickerResult::Cancel => {
393 self.color_picker = None;
394 self.color_picker_surface = None;
395 return Ok(true);
396 }
397 ColorPickerResult::StartEyedropper => {
398 // Close color picker and start eyedropper
399 let original_color = picker.current_color();
400 self.color_picker = None;
401 self.color_picker_surface = None;
402
403 // Run eyedropper (this blocks until user picks or cancels)
404 match self.run_eyedropper()? {
405 Some(color) => {
406 self.state.properties.color = color;
407 }
408 None => {
409 // Cancelled - reopen picker with original color
410 self.open_color_picker(original_color)?;
411 }
412 }
413 return Ok(true);
414 }
415 ColorPickerResult::Changed => {
416 // Color changed, redraw picker only
417 self.redraw_color_picker()?;
418 return Ok(false); // Don't redraw the whole overlay
419 }
420 ColorPickerResult::None => {
421 // No change, no redraw needed
422 return Ok(false);
423 }
424 }
425 }
426
427 match &event {
428 // Handle toolbar clicks
429 InputEvent::MousePress(e) if e.position.y < 0 => {
430 // Click is in toolbar area (y < 0 because of offset)
431 let toolbar_pos = Point::new(e.position.x, e.position.y + self.image_offset_y);
432 match self.toolbar.handle_click(toolbar_pos) {
433 ToolbarClickResult::Tool(tool_type) => {
434 self.select_tool(tool_type)?;
435 needs_redraw = true;
436 }
437 ToolbarClickResult::ColorPreview => {
438 self.open_color_picker(self.state.properties.color)?;
439 needs_redraw = true;
440 }
441 ToolbarClickResult::None => {}
442 }
443 return Ok(needs_redraw);
444 }
445
446 // Handle keyboard shortcuts
447 InputEvent::Key(e) if e.pressed => {
448 // Ctrl+Enter: Save
449 if e.modifiers.ctrl && e.key == Key::Return {
450 self.save()?;
451 return Ok(false);
452 }
453
454 // Escape: Cancel (if not in text editing mode)
455 if e.key == Key::Escape && !self.tool.is_drawing() {
456 self.state.finish_cancel();
457 return Ok(false);
458 }
459
460 // Ctrl+Z: Undo
461 if e.modifiers.ctrl && e.key == Key::Char('z') {
462 if e.modifiers.shift {
463 self.redo()?;
464 } else {
465 self.undo()?;
466 }
467 needs_redraw = true;
468 return Ok(needs_redraw);
469 }
470
471 // Ctrl+Y: Redo
472 if e.modifiers.ctrl && e.key == Key::Char('y') {
473 self.redo()?;
474 needs_redraw = true;
475 return Ok(needs_redraw);
476 }
477
478 // Tool shortcuts (only when not editing text)
479 if !self.tool.is_drawing() {
480 if let Key::Char(c) = e.key {
481 // Number keys for color presets
482 if let Some(digit) = c.to_digit(10) {
483 if digit >= 1 && digit <= 9 {
484 self.state.set_color_preset((digit - 1) as usize);
485 needs_redraw = true;
486 return Ok(needs_redraw);
487 }
488 }
489
490 // Tool shortcuts
491 if let Some(tool_type) = ToolType::from_shortcut(c) {
492 self.select_tool(tool_type)?;
493 needs_redraw = true;
494 return Ok(needs_redraw);
495 }
496
497 // Line width adjustment
498 if c == '+' || c == '=' {
499 self.state.properties.increase_line_width();
500 needs_redraw = true;
501 return Ok(needs_redraw);
502 }
503 if c == '-' {
504 self.state.properties.decrease_line_width();
505 needs_redraw = true;
506 return Ok(needs_redraw);
507 }
508 }
509 }
510 }
511
512 // Handle expose events (window needs redraw)
513 InputEvent::Expose => {
514 return Ok(true);
515 }
516
517 _ => {}
518 }
519
520 // Pass event to current tool
521 let tool_needs_redraw = self.tool.handle_event(&event, &self.state.properties);
522 needs_redraw |= tool_needs_redraw;
523
524 // Check if tool completed a drawing
525 if let InputEvent::MouseRelease(_) = &event {
526 if self.tool.can_commit() && !self.tool.is_drawing() {
527 self.commit_tool()?;
528 needs_redraw = true;
529 }
530 }
531
532 Ok(needs_redraw)
533 }
534
535 /// Select a tool.
536 fn select_tool(&mut self, tool_type: ToolType) -> Result<()> {
537 // Commit current tool if it has a pending drawing
538 if self.tool.can_commit() {
539 self.commit_tool()?;
540 }
541
542 self.state.select_tool(tool_type);
543 self.tool = tools::create_tool(tool_type);
544 self.update_cursor()?;
545
546 Ok(())
547 }
548
549 /// Commit the current tool's drawing to the canvas.
550 fn commit_tool(&mut self) -> Result<()> {
551 // Save snapshot for undo
552 let snapshot = Snapshot::new(self.canvas.snapshot_annotations()?);
553 self.history.push(snapshot);
554
555 // Commit tool drawing to annotations layer
556 let ctx = self.canvas.annotations_surface().context()?;
557 self.tool.commit(&ctx, &self.state.properties);
558
559 // Reset tool
560 self.tool.reset();
561
562 Ok(())
563 }
564
565 /// Undo last action.
566 fn undo(&mut self) -> Result<()> {
567 let current = Snapshot::new(self.canvas.snapshot_annotations()?);
568 if let Some(previous) = self.history.undo(current) {
569 self.canvas.restore_annotations(&previous.data)?;
570 }
571 Ok(())
572 }
573
574 /// Redo last undone action.
575 fn redo(&mut self) -> Result<()> {
576 let current = Snapshot::new(self.canvas.snapshot_annotations()?);
577 if let Some(next) = self.history.redo(current) {
578 self.canvas.restore_annotations(&next.data)?;
579 }
580 Ok(())
581 }
582
583 /// Save the annotated image.
584 fn save(&mut self) -> Result<()> {
585 // Commit any pending tool drawing
586 if self.tool.can_commit() {
587 self.commit_tool()?;
588 }
589
590 // Export final image
591 let data = self.canvas.export()?;
592 let width = self.canvas.width();
593 let height = self.canvas.height();
594
595 self.state.finish_save(data, width, height);
596
597 Ok(())
598 }
599
600 /// Update cursor for current tool.
601 fn update_cursor(&mut self) -> Result<()> {
602 let shape = self.tool.cursor();
603 self.cursor_manager.set_window_cursor(self.window.id(), shape)?;
604 Ok(())
605 }
606
607 /// Open the color picker dialog.
608 fn open_color_picker(&mut self, initial_color: gartk_core::Color) -> Result<()> {
609 use crate::annotate::ui::{PICKER_HEIGHT, PICKER_WIDTH};
610
611 let screen_width = self.canvas.width();
612 let screen_height = self.canvas.height() + TOOLBAR_HEIGHT;
613
614 let picker = ColorPicker::new(initial_color, screen_width, screen_height);
615
616 // Create surface for color picker if needed
617 if self.color_picker_surface.is_none() {
618 self.color_picker_surface = Some(
619 Surface::new(PICKER_WIDTH, PICKER_HEIGHT)
620 .context("Failed to create color picker surface")?,
621 );
622 }
623
624 self.color_picker = Some(picker);
625 Ok(())
626 }
627
628 /// Redraw just the color picker (not the whole overlay).
629 fn redraw_color_picker(&mut self) -> Result<()> {
630 if let (Some(picker), Some(surface)) = (&self.color_picker, &self.color_picker_surface) {
631 picker.draw(surface)?;
632 self.blit_color_picker()?;
633 }
634 Ok(())
635 }
636
637 /// Run the eyedropper to sample a color.
638 fn run_eyedropper(&mut self) -> Result<Option<gartk_core::Color>> {
639 use crate::annotate::ui::color_picker::eyedropper::{Eyedropper, EyedropperResult};
640
641 let eyedropper = Eyedropper::new()?;
642 let result = eyedropper.run()?;
643
644 // Restore focus to our window after eyedropper closes
645 self.window.focus()?;
646
647 match result {
648 EyedropperResult::Color(color) => Ok(Some(color)),
649 EyedropperResult::Cancel => Ok(None),
650 }
651 }
652
653 /// Redraw the overlay.
654 fn redraw(&mut self) -> Result<()> {
655 // Draw current tool preview
656 self.canvas.clear_preview()?;
657 let preview_ctx = self.canvas.preview_surface().context()?;
658 self.tool.draw_preview(&preview_ctx, &self.state.properties);
659
660 // Render canvas layers
661 self.canvas.render()?;
662
663 // Update toolbar state
664 self.toolbar
665 .update(self.state.current_tool, &self.state.properties);
666
667 // Blit to window (toolbar + image)
668 self.blit_to_window()?;
669
670 // Draw color picker if open
671 if let (Some(picker), Some(surface)) = (&self.color_picker, &self.color_picker_surface) {
672 picker.draw(surface)?;
673 self.blit_color_picker()?;
674 }
675
676 self.needs_redraw = false;
677
678 Ok(())
679 }
680
681 /// Blit canvas and toolbar to window.
682 fn blit_to_window(&mut self) -> Result<()> {
683 let width = self.canvas.width();
684 let height = self.canvas.height();
685
686 // Draw toolbar to reusable surface
687 self.toolbar.draw(&self.toolbar_surface)?;
688
689 // Get toolbar data and convert to BGRA
690 let toolbar_data = self.toolbar_surface.to_rgba()?;
691 let toolbar_bgra = rgba_to_bgra(&toolbar_data);
692
693 // Blit toolbar at y=0 (toolbar is small, no chunking needed)
694 self.conn.inner().put_image(
695 ImageFormat::Z_PIXMAP,
696 self.window.id(),
697 self.gc,
698 width as u16,
699 TOOLBAR_HEIGHT as u16,
700 0,
701 0,
702 0,
703 24,
704 &toolbar_bgra,
705 )?;
706
707 // Get image composite data
708 let surface = self.canvas.composite_surface_mut();
709 let data = surface.to_rgba()?;
710 let bgra = rgba_to_bgra(&data);
711
712 // Blit image at y=TOOLBAR_HEIGHT using chunked put_image for large images
713 put_image_chunked(
714 self.conn.inner(),
715 self.window.id(),
716 self.gc,
717 width as u16,
718 height as u16,
719 0,
720 self.image_offset_y as i16,
721 &bgra,
722 )?;
723
724 self.conn.inner().flush()?;
725
726 Ok(())
727 }
728
729 /// Blit color picker to window.
730 fn blit_color_picker(&mut self) -> Result<()> {
731 use crate::annotate::ui::{PICKER_HEIGHT, PICKER_WIDTH};
732
733 let picker = self.color_picker.as_ref().ok_or_else(|| anyhow::anyhow!("No color picker"))?;
734 let surface = self.color_picker_surface.as_mut().ok_or_else(|| anyhow::anyhow!("No color picker surface"))?;
735
736 let rect = picker.rect();
737
738 // Get picker data and convert to BGRA
739 let data = surface.to_rgba()?;
740 let bgra = rgba_to_bgra(&data);
741
742 // Blit at picker position (adjusted for toolbar offset)
743 self.conn.inner().put_image(
744 ImageFormat::Z_PIXMAP,
745 self.window.id(),
746 self.gc,
747 PICKER_WIDTH as u16,
748 PICKER_HEIGHT as u16,
749 rect.x as i16,
750 (rect.y + self.image_offset_y) as i16,
751 0,
752 24,
753 &bgra,
754 )?;
755
756 self.conn.inner().flush()?;
757
758 Ok(())
759 }
760
761 /// Set window to full opacity to prevent compositor transparency.
762 fn set_full_opacity(&self) -> Result<()> {
763 // Intern the _NET_WM_WINDOW_OPACITY atom
764 let opacity_atom = self.conn.inner()
765 .intern_atom(false, b"_NET_WM_WINDOW_OPACITY")?
766 .reply()
767 .context("Failed to intern opacity atom")?
768 .atom;
769
770 // Full opacity = 0xFFFFFFFF (max u32)
771 let opacity: u32 = 0xFFFFFFFF;
772
773 self.conn.inner().change_property32(
774 PropMode::REPLACE,
775 self.window.id(),
776 opacity_atom,
777 AtomEnum::CARDINAL,
778 &[opacity],
779 )?;
780
781 self.conn.inner().flush()?;
782 Ok(())
783 }
784 }
785
786 /// Convert RGBA to BGRA (X11 native format).
787 fn rgba_to_bgra(data: &[u8]) -> Vec<u8> {
788 let mut bgra = data.to_vec();
789 for chunk in bgra.chunks_exact_mut(4) {
790 chunk.swap(0, 2); // Swap R and B
791 }
792 bgra
793 }
794
795 /// Put image data in chunks to avoid exceeding X11 max request size.
796 fn put_image_chunked<C: X11Connection>(
797 conn: &C,
798 drawable: u32,
799 gc: u32,
800 width: u16,
801 height: u16,
802 dst_x: i16,
803 dst_y: i16,
804 data: &[u8],
805 ) -> Result<()> {
806 let bytes_per_row = width as usize * 4;
807 let rows_per_chunk = (MAX_PUT_IMAGE_BYTES / bytes_per_row).max(1);
808
809 let mut y = 0u16;
810 while (y as usize) < height as usize {
811 let chunk_height = ((height as usize - y as usize).min(rows_per_chunk)) as u16;
812 let start = y as usize * bytes_per_row;
813 let end = start + chunk_height as usize * bytes_per_row;
814 let chunk_data = &data[start..end];
815
816 conn.put_image(
817 ImageFormat::Z_PIXMAP,
818 drawable,
819 gc,
820 width,
821 chunk_height,
822 dst_x,
823 dst_y + y as i16,
824 0,
825 24,
826 chunk_data,
827 )?;
828
829 y += chunk_height;
830 }
831
832 Ok(())
833 }
834