diff --git a/hcie-engine-api/src/stroke_brush.rs b/hcie-engine-api/src/stroke_brush.rs index 286b025..9cfba9f 100644 --- a/hcie-engine-api/src/stroke_brush.rs +++ b/hcie-engine-api/src/stroke_brush.rs @@ -117,17 +117,25 @@ impl Engine { if mask.len() != full_size { let mut full_mask = vec![layer.mask_default_color; full_size]; if let Some(mb) = layer.mask_bounds { - let (m_top, m_left, m_bottom, m_right) = - (mb[0].max(0) as u32, mb[1].max(0) as u32, mb[2].max(0) as u32, mb[3].max(0) as u32); + let (m_top, m_left, m_bottom, m_right) = ( + mb[0].max(0) as u32, + mb[1].max(0) as u32, + mb[2].max(0) as u32, + mb[3].max(0) as u32, + ); let mw = (m_right - m_left) as usize; let mh = (m_bottom - m_top) as usize; if mw > 0 && mh > 0 && mask.len() == mw * mh { for y in 0..mh { let gy = m_top as usize + y; - if gy >= ch as usize { break; } + if gy >= ch as usize { + break; + } for x in 0..mw { let gx = m_left as usize + x; - if gx >= cw as usize { break; } + if gx >= cw as usize { + break; + } full_mask[gy * cw as usize + gx] = mask[y * mw + x]; } } @@ -181,7 +189,9 @@ impl Engine { let cw = self.document.canvas_width as usize; let ch = self.document.canvas_height as usize; let mask_len = cw * ch; - if mask_len == 0 { return; } + if mask_len == 0 { + return; + } if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) { if layer.mask_pixels.is_none() { @@ -294,7 +304,13 @@ impl Engine { self.draw_target_pixels_or_mask( layer_id, - |pixels, lw, lh, mask_ref, active_stroke_mask, stroke_before_buf, sketch_history| { + |pixels, + lw, + lh, + mask_ref, + active_stroke_mask, + stroke_before_buf, + sketch_history| { let sketch_hist = if tip.style == BrushStyle::Sketch { sketch_history } else { @@ -361,8 +377,15 @@ impl Engine { if self.editing_mask { self.draw_target_pixels_or_mask( layer_id, - |pixels, lw, lh, mask_ref, active_stroke_mask, stroke_before_buf, _sketch_history| { - let mut temp_layer = hcie_protocol::Layer::from_rgba("temp", lw, lh, pixels.to_vec()); + |pixels, + lw, + lh, + mask_ref, + active_stroke_mask, + stroke_before_buf, + _sketch_history| { + let mut temp_layer = + hcie_protocol::Layer::from_rgba("temp", lw, lh, pixels.to_vec()); draw_brush_stroke( &mut temp_layer, &[(x, y, pressure)], diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs index 1152b9f..44feba2 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/app.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/app.rs @@ -2901,11 +2901,14 @@ impl HcieIcedApp { let paint_tool = matches!(tool, Tool::Pen | Tool::Brush | Tool::Eraser | Tool::Spray); if paint_tool && (self.modifiers.control() || self.modifiers.logo()) { - let cx = canvas_x as u32; - let cy = canvas_y as u32; let width = self.documents[self.active_doc].engine.canvas_width(); - if cx < width && cy < self.documents[self.active_doc].engine.canvas_height() { - let index = ((cy * width + cx) * 4) as usize; + let height = self.documents[self.active_doc].engine.canvas_height(); + if canvas_x >= 0.0 + && canvas_y >= 0.0 + && (canvas_x as u32) < width + && (canvas_y as u32) < height + { + let index = ((canvas_y as u32 * width + canvas_x as u32) * 4) as usize; if index + 3 < self.documents[self.active_doc].composite_raw.len() { let pixels = &self.documents[self.active_doc].composite_raw; let color = [ @@ -3247,13 +3250,11 @@ impl HcieIcedApp { let is_ctrl = self.modifiers.control() || self.modifiers.logo(); if is_ctrl { // Pick color - let cx = x as u32; - let cy = y as u32; let w = self.documents[self.active_doc].engine.canvas_width(); let h = self.documents[self.active_doc].engine.canvas_height(); - if cx < w && cy < h { + if x >= 0.0 && y >= 0.0 && (x as u32) < w && (y as u32) < h { let pixels = &self.documents[self.active_doc].composite_raw; - let idx = ((cy * w + cx) * 4) as usize; + let idx = ((y as u32 * w + x as u32) * 4) as usize; if idx + 3 < pixels.len() { let color = [ pixels[idx], @@ -3719,7 +3720,9 @@ impl HcieIcedApp { } Message::LayerSetEditMask(editing) => { - self.documents[self.active_doc].engine.set_editing_mask(editing); + self.documents[self.active_doc] + .engine + .set_editing_mask(editing); self.documents[self.active_doc].cached_layers = self.documents[self.active_doc].engine.layer_infos(); } @@ -6711,10 +6714,8 @@ impl HcieIcedApp { self.settings.tool_settings.vector_angle = angle.to_degrees(); // Track snap state for visual overlay feedback - let is_rotate = matches!( - session.handle, - hcie_engine_api::VectorEditHandle::Rotate - ); + let is_rotate = + matches!(session.handle, hcie_engine_api::VectorEditHandle::Rotate); let is_resize = matches!( session.handle, hcie_engine_api::VectorEditHandle::TopLeft @@ -7784,7 +7785,8 @@ impl HcieIcedApp { } Message::PasteImage => { // Check internal clipboard first - if let Some(transform) = self.documents[self.active_doc].internal_clipboard.clone() { + if let Some(transform) = self.documents[self.active_doc].internal_clipboard.clone() + { // Enter transform mode with the clipboard content let mut placed = transform; placed.pos.x += 10.0; @@ -8481,12 +8483,10 @@ impl HcieIcedApp { return self.update(Message::OpenFileRfd); } - Message::FileOpened(Ok(path)) => { - match self.open_path_in_new_tab(path, true) { - Ok(_) => return Task::perform(async {}, |_| Message::CompositeRefresh), - Err(error) => log::error!("Failed to open file: {error}"), - } - } + Message::FileOpened(Ok(path)) => match self.open_path_in_new_tab(path, true) { + Ok(_) => return Task::perform(async {}, |_| Message::CompositeRefresh), + Err(error) => log::error!("Failed to open file: {error}"), + }, Message::FileOpened(Err(e)) => { log::error!("Failed to open file: {}", e); @@ -9153,9 +9153,7 @@ impl HcieIcedApp { | crate::theme::ThemePreset::ProDark | crate::theme::ThemePreset::Amoled => crate::theme::ThemePreset::ProLight, crate::theme::ThemePreset::PhotoshopLight - | crate::theme::ThemePreset::ProLight => { - crate::theme::ThemePreset::Photopea - } + | crate::theme::ThemePreset::ProLight => crate::theme::ThemePreset::Photopea, }; self.theme_state.set_preset(new_preset); self.settings.theme_preset = new_preset; @@ -10308,10 +10306,8 @@ mod cycle_one_ux_tests { .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos(); - let path = std::env::temp_dir().join(format!( - "hcie-open-tab-{}-{unique}.png", - std::process::id() - )); + let path = + std::env::temp_dir().join(format!("hcie-open-tab-{}-{unique}.png", std::process::id())); image::save_buffer( &path, &[255, 0, 0, 255, 0, 255, 0, 255], @@ -10328,7 +10324,10 @@ mod cycle_one_ux_tests { assert_eq!(app.active_doc, 1); assert_eq!(app.documents[0].name, original_name); assert!(app.documents[0].source_path.is_none()); - assert_eq!(app.documents[1].source_path.as_deref(), Some(path.as_path())); + assert_eq!( + app.documents[1].source_path.as_deref(), + Some(path.as_path()) + ); assert_eq!(app.documents[1].engine.canvas_width(), 2); assert_eq!(app.documents[1].engine.canvas_height(), 1); diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/shader_canvas.rs b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/shader_canvas.rs index f9c8afb..0f3f786 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/shader_canvas.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/shader_canvas.rs @@ -985,8 +985,7 @@ fn should_publish_cursor_status( current: (u32, u32), elapsed: Option, ) -> bool { - previous != Some(current) - && elapsed.map_or(true, |duration| duration >= CURSOR_STATUS_INTERVAL) + previous != Some(current) && elapsed.map_or(true, |duration| duration >= CURSOR_STATUS_INTERVAL) } /// Canvas shader program — implements `iced::widget::shader::Program`. diff --git a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/viewport.rs b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/viewport.rs index e8485d3..9d46fb8 100644 --- a/hcie-iced-app/crates/hcie-iced-gui/src/canvas/viewport.rs +++ b/hcie-iced-app/crates/hcie-iced-gui/src/canvas/viewport.rs @@ -110,23 +110,44 @@ impl Program for CanvasViewportProgram { let mut frame = Frame::new(bounds.size()); // Draw checkerboard background (tiled to cover display area) - let checker_pattern = canvas::Pattern::new(&cfg.checker_handle).mode(canvas::PatternMode::Repeat); - let checker_path = Path::rectangle(Point::new(origin_x, origin_y), Size::new(display_w, display_h)); + let checker_pattern = + canvas::Pattern::new(&cfg.checker_handle).mode(canvas::PatternMode::Repeat); + let checker_path = Path::rectangle( + Point::new(origin_x, origin_y), + Size::new(display_w, display_h), + ); frame.fill(&checker_path, checker_pattern); // Draw composite texture - let composite_image = canvas::Image::new(&cfg.composite_handle) - .destination_rectangle(Rectangle::new(Point::new(origin_x, origin_y), Size::new(display_w, display_h))); + let composite_image = + canvas::Image::new(&cfg.composite_handle).destination_rectangle(Rectangle::new( + Point::new(origin_x, origin_y), + Size::new(display_w, display_h), + )); frame.draw_image(composite_image); // Draw canvas border - let border_path = Path::rectangle(Point::new(origin_x, origin_y), Size::new(display_w, display_h)); - frame.stroke(&border_path, Stroke::default().with_color(Color::from_rgb(0.35, 0.35, 0.35)).with_width(1.0)); + let border_path = Path::rectangle( + Point::new(origin_x, origin_y), + Size::new(display_w, display_h), + ); + frame.stroke( + &border_path, + Stroke::default() + .with_color(Color::from_rgb(0.35, 0.35, 0.35)) + .with_width(1.0), + ); vec![frame.into_geometry()] } - fn update(&self, state: &mut (), event: iced::Event, bounds: Rectangle, cursor: Cursor) -> (iced::widget::canvas::Status, Option) { + fn update( + &self, + state: &mut (), + event: iced::Event, + bounds: Rectangle, + cursor: Cursor, + ) -> (iced::widget::canvas::Status, Option) { let mut program_state = self.state.lock().unwrap(); match event { @@ -140,17 +161,22 @@ impl Program for CanvasViewportProgram { iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left) => { if let Some(pos) = program_state.last_cursor { if bounds.contains(pos) { - let (cx, cy) = viewport_to_canvas(pos, bounds, &program_state.config); + let (cx, cy) = + viewport_to_canvas(pos, bounds, &program_state.config); if let Some((cx, cy)) = (cx, cy) { - return (iced::widget::canvas::Status::Captured, Some(Message::CanvasPointerPressed { x: cx, y: cy })); + return ( + iced::widget::canvas::Status::Captured, + Some(Message::CanvasPointerPressed { x: cx, y: cy }), + ); } } } (iced::widget::canvas::Status::Captured, None) } - iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left) => { - (iced::widget::canvas::Status::Captured, Some(Message::CanvasPointerReleased)) - } + iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left) => ( + iced::widget::canvas::Status::Captured, + Some(Message::CanvasPointerReleased), + ), iced::mouse::Event::Scrolled { delta } => { if program_state.is_hovered { let scroll_delta = match delta { @@ -164,8 +190,13 @@ impl Program for CanvasViewportProgram { let new_zoom = (old_zoom * factor).clamp(0.1, 10.0); if let Some(cursor_pos) = program_state.last_cursor { - let (canvas_x, canvas_y) = viewport_to_canvas(cursor_pos, bounds, &program_state.config).unwrap_or((0.0, 0.0)); - + let (canvas_x, canvas_y) = viewport_to_canvas( + cursor_pos, + bounds, + &program_state.config, + ) + .unwrap_or((0.0, 0.0)); + // Calculate new pan_offset to keep cursor over same canvas point let display_w = program_state.config.engine_w as f32 * new_zoom; let display_h = program_state.config.engine_h as f32 * new_zoom; @@ -173,12 +204,19 @@ impl Program for CanvasViewportProgram { let new_origin_y = cursor_pos.y - canvas_y * new_zoom; let new_default_origin_x = (bounds.width - display_w) / 2.0; let new_default_origin_y = (bounds.height - display_h) / 2.0; - - program_state.config.pan_offset.x = new_origin_x - new_default_origin_x; - program_state.config.pan_offset.y = new_origin_y - new_default_origin_y; + + program_state.config.pan_offset.x = + new_origin_x - new_default_origin_x; + program_state.config.pan_offset.y = + new_origin_y - new_default_origin_y; } program_state.config.zoom = new_zoom; - return (iced::widget::canvas::Status::Captured, Some(Message::CanvasZoom { delta: scroll_delta })); + return ( + iced::widget::canvas::Status::Captured, + Some(Message::CanvasZoom { + delta: scroll_delta, + }), + ); } } (iced::widget::canvas::Status::Ignored, None) @@ -190,7 +228,12 @@ impl Program for CanvasViewportProgram { } } - fn mouse_interaction(&self, _state: &(), bounds: Rectangle, cursor: Cursor) -> iced::mouse::Interaction { + fn mouse_interaction( + &self, + _state: &(), + bounds: Rectangle, + cursor: Cursor, + ) -> iced::mouse::Interaction { if cursor.is_over(bounds) { iced::mouse::Interaction::Crosshair } else { @@ -203,7 +246,11 @@ impl Program for CanvasViewportProgram { /// /// Returns signed canvas coordinates even when the cursor is outside the canvas /// image so brush strokes can start or continue beyond the visible edges. -fn viewport_to_canvas(viewport_pos: Point, bounds: Rectangle, config: &CanvasViewportConfig) -> Option<(f32, f32)> { +fn viewport_to_canvas( + viewport_pos: Point, + bounds: Rectangle, + config: &CanvasViewportConfig, +) -> Option<(f32, f32)> { let display_w = config.engine_w as f32 * config.zoom; let display_h = config.engine_h as f32 * config.zoom; @@ -234,4 +281,4 @@ pub fn canvas_viewport<'a>( ..Default::default() }) .into() -} \ No newline at end of file +}