@@ -6,7 +6,10 @@ use crate::annotate::canvas::AnnotationCanvas; |
| 6 | 6 | use crate::annotate::history::{History, Snapshot}; |
| 7 | 7 | use crate::annotate::state::{AnnotationResult, AnnotationState, ToolType}; |
| 8 | 8 | use crate::annotate::tools::{self, Tool}; |
| 9 | | -use crate::annotate::ui::{Toolbar, TOOLBAR_HEIGHT}; |
| 9 | +use crate::annotate::ui::{ |
| 10 | + color_picker::{ColorPicker, ColorPickerResult}, |
| 11 | + Toolbar, ToolbarClickResult, TOOLBAR_HEIGHT, |
| 12 | +}; |
| 10 | 13 | |
| 11 | 14 | use gartk_core::{InputEvent, Key, KeyEvent, Modifiers, MouseButton, MouseEvent, Point}; |
| 12 | 15 | use gartk_render::Surface; |
@@ -44,6 +47,10 @@ pub struct AnnotationOverlay { |
| 44 | 47 | needs_redraw: bool, |
| 45 | 48 | /// Image offset from top (for toolbar). |
| 46 | 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>, |
| 47 | 54 | } |
| 48 | 55 | |
| 49 | 56 | impl AnnotationOverlay { |
@@ -175,6 +182,8 @@ impl AnnotationOverlay { |
| 175 | 182 | cursor_manager, |
| 176 | 183 | needs_redraw: true, |
| 177 | 184 | image_offset_y: TOOLBAR_HEIGHT as i32, |
| 185 | + color_picker: None, |
| 186 | + color_picker_surface: None, |
| 178 | 187 | }) |
| 179 | 188 | } |
| 180 | 189 | |
@@ -367,15 +376,66 @@ impl AnnotationOverlay { |
| 367 | 376 | fn handle_event(&mut self, event: InputEvent) -> Result<bool> { |
| 368 | 377 | let mut needs_redraw = false; |
| 369 | 378 | |
| 379 | + // If color picker is open, route events to it first |
| 380 | + if let Some(ref mut picker) = self.color_picker { |
| 381 | + match picker.handle_event(&event) { |
| 382 | + ColorPickerResult::Confirm(color) => { |
| 383 | + self.state.properties.color = color; |
| 384 | + self.color_picker = None; |
| 385 | + self.color_picker_surface = None; |
| 386 | + return Ok(true); |
| 387 | + } |
| 388 | + ColorPickerResult::Cancel => { |
| 389 | + self.color_picker = None; |
| 390 | + self.color_picker_surface = None; |
| 391 | + return Ok(true); |
| 392 | + } |
| 393 | + ColorPickerResult::StartEyedropper => { |
| 394 | + // Close color picker and start eyedropper |
| 395 | + let original_color = picker.current_color(); |
| 396 | + self.color_picker = None; |
| 397 | + self.color_picker_surface = None; |
| 398 | + |
| 399 | + // Run eyedropper (this blocks until user picks or cancels) |
| 400 | + match self.run_eyedropper()? { |
| 401 | + Some(color) => { |
| 402 | + self.state.properties.color = color; |
| 403 | + } |
| 404 | + None => { |
| 405 | + // Cancelled - reopen picker with original color |
| 406 | + self.open_color_picker(original_color)?; |
| 407 | + } |
| 408 | + } |
| 409 | + return Ok(true); |
| 410 | + } |
| 411 | + ColorPickerResult::Changed => { |
| 412 | + // Color changed, redraw picker only |
| 413 | + self.redraw_color_picker()?; |
| 414 | + return Ok(false); // Don't redraw the whole overlay |
| 415 | + } |
| 416 | + ColorPickerResult::None => { |
| 417 | + // No change, no redraw needed |
| 418 | + return Ok(false); |
| 419 | + } |
| 420 | + } |
| 421 | + } |
| 422 | + |
| 370 | 423 | match &event { |
| 371 | 424 | // Handle toolbar clicks |
| 372 | 425 | InputEvent::MousePress(e) if e.position.y < 0 => { |
| 373 | 426 | // Click is in toolbar area (y < 0 because of offset) |
| 374 | 427 | let toolbar_pos = Point::new(e.position.x, e.position.y + self.image_offset_y); |
| 375 | | - if let Some(tool_type) = self.toolbar.handle_click(toolbar_pos) { |
| 428 | + match self.toolbar.handle_click(toolbar_pos) { |
| 429 | + ToolbarClickResult::Tool(tool_type) => { |
| 376 | 430 | self.select_tool(tool_type)?; |
| 377 | 431 | needs_redraw = true; |
| 378 | 432 | } |
| 433 | + ToolbarClickResult::ColorPreview => { |
| 434 | + self.open_color_picker(self.state.properties.color)?; |
| 435 | + needs_redraw = true; |
| 436 | + } |
| 437 | + ToolbarClickResult::None => {} |
| 438 | + } |
| 379 | 439 | return Ok(needs_redraw); |
| 380 | 440 | } |
| 381 | 441 | |
@@ -535,6 +595,55 @@ impl AnnotationOverlay { |
| 535 | 595 | Ok(()) |
| 536 | 596 | } |
| 537 | 597 | |
| 598 | + /// Open the color picker dialog. |
| 599 | + fn open_color_picker(&mut self, initial_color: gartk_core::Color) -> Result<()> { |
| 600 | + use crate::annotate::ui::{PICKER_HEIGHT, PICKER_WIDTH}; |
| 601 | + |
| 602 | + let screen_width = self.canvas.width(); |
| 603 | + let screen_height = self.canvas.height() + TOOLBAR_HEIGHT; |
| 604 | + |
| 605 | + let picker = ColorPicker::new(initial_color, screen_width, screen_height); |
| 606 | + |
| 607 | + // Create surface for color picker if needed |
| 608 | + if self.color_picker_surface.is_none() { |
| 609 | + self.color_picker_surface = Some( |
| 610 | + Surface::new(PICKER_WIDTH, PICKER_HEIGHT) |
| 611 | + .context("Failed to create color picker surface")?, |
| 612 | + ); |
| 613 | + } |
| 614 | + |
| 615 | + self.color_picker = Some(picker); |
| 616 | + Ok(()) |
| 617 | + } |
| 618 | + |
| 619 | + /// Redraw just the color picker (not the whole overlay). |
| 620 | + fn redraw_color_picker(&mut self) -> Result<()> { |
| 621 | + if let (Some(picker), Some(surface)) = (&self.color_picker, &self.color_picker_surface) { |
| 622 | + picker.draw(surface)?; |
| 623 | + self.blit_color_picker()?; |
| 624 | + } |
| 625 | + Ok(()) |
| 626 | + } |
| 627 | + |
| 628 | + /// Run the eyedropper to sample a color. |
| 629 | + fn run_eyedropper(&mut self) -> Result<Option<gartk_core::Color>> { |
| 630 | + use crate::annotate::ui::color_picker::eyedropper::{Eyedropper, EyedropperResult}; |
| 631 | + |
| 632 | + // Temporarily release our keyboard grab so eyedropper can grab |
| 633 | + self.window.ungrab_keyboard()?; |
| 634 | + |
| 635 | + let eyedropper = Eyedropper::new()?; |
| 636 | + let result = eyedropper.run()?; |
| 637 | + |
| 638 | + // Regrab keyboard for our window |
| 639 | + self.window.grab_keyboard_with_retry(10, 50)?; |
| 640 | + |
| 641 | + match result { |
| 642 | + EyedropperResult::Color(color) => Ok(Some(color)), |
| 643 | + EyedropperResult::Cancel => Ok(None), |
| 644 | + } |
| 645 | + } |
| 646 | + |
| 538 | 647 | /// Redraw the overlay. |
| 539 | 648 | fn redraw(&mut self) -> Result<()> { |
| 540 | 649 | // Draw current tool preview |
@@ -552,6 +661,12 @@ impl AnnotationOverlay { |
| 552 | 661 | // Blit to window (toolbar + image) |
| 553 | 662 | self.blit_to_window()?; |
| 554 | 663 | |
| 664 | + // Draw color picker if open |
| 665 | + if let (Some(picker), Some(surface)) = (&self.color_picker, &self.color_picker_surface) { |
| 666 | + picker.draw(surface)?; |
| 667 | + self.blit_color_picker()?; |
| 668 | + } |
| 669 | + |
| 555 | 670 | self.needs_redraw = false; |
| 556 | 671 | |
| 557 | 672 | Ok(()) |
@@ -605,6 +720,38 @@ impl AnnotationOverlay { |
| 605 | 720 | Ok(()) |
| 606 | 721 | } |
| 607 | 722 | |
| 723 | + /// Blit color picker to window. |
| 724 | + fn blit_color_picker(&mut self) -> Result<()> { |
| 725 | + use crate::annotate::ui::{PICKER_HEIGHT, PICKER_WIDTH}; |
| 726 | + |
| 727 | + let picker = self.color_picker.as_ref().ok_or_else(|| anyhow::anyhow!("No color picker"))?; |
| 728 | + let surface = self.color_picker_surface.as_mut().ok_or_else(|| anyhow::anyhow!("No color picker surface"))?; |
| 729 | + |
| 730 | + let rect = picker.rect(); |
| 731 | + |
| 732 | + // Get picker data and convert to BGRA |
| 733 | + let data = surface.to_rgba()?; |
| 734 | + let bgra = rgba_to_bgra(&data); |
| 735 | + |
| 736 | + // Blit at picker position (adjusted for toolbar offset) |
| 737 | + self.conn.inner().put_image( |
| 738 | + ImageFormat::Z_PIXMAP, |
| 739 | + self.window.id(), |
| 740 | + self.gc, |
| 741 | + PICKER_WIDTH as u16, |
| 742 | + PICKER_HEIGHT as u16, |
| 743 | + rect.x as i16, |
| 744 | + (rect.y + self.image_offset_y) as i16, |
| 745 | + 0, |
| 746 | + 24, |
| 747 | + &bgra, |
| 748 | + )?; |
| 749 | + |
| 750 | + self.conn.inner().flush()?; |
| 751 | + |
| 752 | + Ok(()) |
| 753 | + } |
| 754 | + |
| 608 | 755 | /// Set window to full opacity to prevent compositor transparency. |
| 609 | 756 | fn set_full_opacity(&self) -> Result<()> { |
| 610 | 757 | // Intern the _NET_WM_WINDOW_OPACITY atom |