From e3153bdf596436b99bc8e922836efec56cf1aaf4 Mon Sep 17 00:00:00 2001 From: yeqing Date: Wed, 25 Feb 2026 11:51:03 +0800 Subject: [PATCH] Implement fill with color --- src/main.rs | 2 +- src/mouse_area.rs | 81 +++------ src/paint.rs | 434 +++++++++++++++++++++++++++++++++++++--------- 3 files changed, 372 insertions(+), 145 deletions(-) diff --git a/src/main.rs b/src/main.rs index cd9599a..49606c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ mod image_button; -mod paint; mod mouse_area; +mod paint; pub fn main() -> iced::Result { paint::main() diff --git a/src/mouse_area.rs b/src/mouse_area.rs index 6de4fb8..4819859 100644 --- a/src/mouse_area.rs +++ b/src/mouse_area.rs @@ -1,3 +1,4 @@ +//! code copy from: https://github.com/airstrike/sweeten/blob/master/src/widget/mouse_area.rs //! A container for capturing mouse events. //! //! This is a sweetened version of `iced`'s [`MouseArea`] where all event @@ -31,17 +32,11 @@ use iced_core::renderer; use iced_core::touch; use iced_core::widget::{Operation, Tree, tree}; use iced_core::{ - Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size, - Vector, Widget, + Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, }; /// Emit messages on mouse events. -pub struct MouseArea< - 'a, - Message, - Theme = iced_core::Theme, - Renderer = iced::Renderer, -> { +pub struct MouseArea<'a, Message, Theme = iced_core::Theme, Renderer = iced::Renderer> { content: Element<'a, Message, Theme, Renderer>, on_press: Option Message + 'a>>, on_release: Option Message + 'a>>, @@ -71,10 +66,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { /// /// The closure receives the click position as a [`Point`]. #[must_use] - pub fn on_press_maybe( - mut self, - f: Option Message + 'a>, - ) -> Self { + pub fn on_press_maybe(mut self, f: Option Message + 'a>) -> Self { self.on_press = f.map(|f| Box::new(f) as _); self } @@ -101,10 +93,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { /// [`on_press`]: Self::on_press /// [`on_release`]: Self::on_release #[must_use] - pub fn on_double_click( - mut self, - f: impl Fn(Point) -> Message + 'a, - ) -> Self { + pub fn on_double_click(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { self.on_double_click = Some(Box::new(f)); self } @@ -122,10 +111,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { /// /// The closure receives the release position as a [`Point`]. #[must_use] - pub fn on_right_release( - mut self, - f: impl Fn(Point) -> Message + 'a, - ) -> Self { + pub fn on_right_release(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { self.on_right_release = Some(Box::new(f)); self } @@ -134,10 +120,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { /// /// The closure receives the click position as a [`Point`]. #[must_use] - pub fn on_middle_press( - mut self, - f: impl Fn(Point) -> Message + 'a, - ) -> Self { + pub fn on_middle_press(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { self.on_middle_press = Some(Box::new(f)); self } @@ -146,20 +129,14 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { /// /// The closure receives the release position as a [`Point`]. #[must_use] - pub fn on_middle_release( - mut self, - f: impl Fn(Point) -> Message + 'a, - ) -> Self { + pub fn on_middle_release(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { self.on_middle_release = Some(Box::new(f)); self } /// Sets the message to emit when the scroll wheel is used. #[must_use] - pub fn on_scroll( - mut self, - on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a, - ) -> Self { + pub fn on_scroll(mut self, on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a) -> Self { self.on_scroll = Some(Box::new(on_scroll)); self } @@ -210,9 +187,7 @@ struct State { impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { /// Creates a [`MouseArea`] with the given content. - pub fn new( - content: impl Into>, - ) -> Self { + pub fn new(content: impl Into>) -> Self { MouseArea { content: content.into(), on_press: None, @@ -232,7 +207,7 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { } impl Widget -for MouseArea<'_, Message, Theme, Renderer> + for MouseArea<'_, Message, Theme, Renderer> where Renderer: renderer::Renderer, { @@ -262,11 +237,9 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - self.content.as_widget_mut().layout( - &mut tree.children[0], - renderer, - limits, - ) + self.content + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits) } fn operate( @@ -276,12 +249,9 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - self.content.as_widget_mut().operate( - &mut tree.children[0], - layout, - renderer, - operation, - ); + self.content + .as_widget_mut() + .operate(&mut tree.children[0], layout, renderer, operation); } fn update( @@ -330,11 +300,9 @@ where ); match (self.interaction, content_interaction) { - (Some(interaction), mouse::Interaction::None) - if cursor.is_over(layout.bounds()) => - { - interaction - } + (Some(interaction), mouse::Interaction::None) if cursor.is_over(layout.bounds()) => { + interaction + } _ => content_interaction, } } @@ -379,7 +347,7 @@ where } impl<'a, Message, Theme, Renderer> From> -for Element<'a, Message, Theme, Renderer> + for Element<'a, Message, Theme, Renderer> where Message: 'a, Theme: 'a, @@ -451,11 +419,8 @@ fn update( if let Some(position) = cursor.position_in(layout.bounds()) && let Some(on_double_click) = widget.on_double_click.as_ref() { - let new_click = mouse::Click::new( - position, - mouse::Button::Left, - state.previous_click, - ); + let new_click = + mouse::Click::new(position, mouse::Button::Left, state.previous_click); if new_click.kind() == mouse::click::Kind::Double { shell.publish(on_double_click(position)); diff --git a/src/paint.rs b/src/paint.rs index 17510ac..4f3f725 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -1,44 +1,21 @@ +use std::time::Instant; + use iced::Theme; use iced::padding; use iced::widget::container; -use iced::widget::{Column, Grid, button, column, image, row, mouse_area}; +use iced::widget::{Column, Grid, button, column, image, row}; use iced::{Border, Color, Length, Point, Task}; - use crate::image_button::image_button; +use crate::mouse_area::mouse_area; const WIDTH: u32 = 800; const HEIGHT: u32 = 600; -struct Paint { - tool_states: [bool; Tool::Count as usize], - tool_selected: Tool, - - // 原始像素数据:RGBA 格式 - // 长度 = WIDTH * HEIGHT * 4 - pixels: Vec, - pixels_bak: Vec, - - // 当前画笔颜色 - color: Color, - - // 是否正在绘制 - is_drawing: bool, - - begin_point: Option, - - // 用于显示的图像句柄缓存 - // 每次像素变化后需要重新生成 - image_handle: image::Handle, - - // 标记像素是否被修改,用于优化图像句柄的生成 - dirty: bool, -} #[derive(Debug, Clone, Copy)] pub enum Message { - Decrement, MousePressed(Point), - MouseReleased, + MouseReleased(Point), MouseMoved(Point), Clear, ChangeColor(Color), @@ -118,6 +95,54 @@ impl From for usize { } } +#[derive(Debug, Clone, Copy, PartialEq)] +struct ColorU8 { + r: u8, + g: u8, + b: u8, + a: u8, +} + +impl ColorU8 { + fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a } + } + + fn is_same(&self, color: Color) -> bool { + self.r == (color.r * 255.0) as u8 + && self.g == (color.g * 255.0) as u8 + && self.b == (color.b * 255.0) as u8 + && self.a == (color.a * 255.0) as u8 + } +} + +struct Paint { + tool_states: [bool; Tool::Count as usize], + tool_selected: Tool, + + // 原始像素数据:RGBA 格式 + // 长度 = WIDTH * HEIGHT * 4 + pixels: Vec, + pixels_bak: Vec, + + // 当前画笔颜色 + color: Color, + + // 是否正在绘制 + is_drawing: bool, + + begin_point: Point, + + // 用于显示的图像句柄缓存 + // 每次像素变化后需要重新生成 + image_handle: image::Handle, + + // brush 大小 + brush_radius: i32, + + // 标记像素是否被修改,用于优化图像句柄的生成 + dirty: bool, +} impl Paint { // region iced application (new view update) @@ -134,8 +159,9 @@ impl Paint { pixels_bak: Vec::new(), color: Color::BLACK, is_drawing: false, - begin_point: None, + begin_point: Point::ORIGIN, image_handle: image::Handle::from_rgba(WIDTH, HEIGHT, data), + brush_radius: 1, dirty: false, } } @@ -148,8 +174,8 @@ impl Paint { .height(Length::Fixed(HEIGHT as f32)); let canvas_area = mouse_area(image_widget) - .on_press(Message::MousePressed(Point::ORIGIN)) // 占位,实际逻辑在 on_drag 或自定义 - .on_release(Message::MouseReleased) + .on_press(|pos| Message::MousePressed(pos)) // 占位,实际逻辑在 on_drag 或自定义 + .on_release(|pos| Message::MouseReleased(pos)) .on_move(|pos| Message::MouseMoved(pos)); // 注意:mouse_area 的 on_move 给出的坐标通常是相对于 widget 左上角的,这正是我们需要的! @@ -185,13 +211,16 @@ impl Paint { // We use a column: a simple vertical layout column![ - button("-").on_press(Message::Decrement), + button("CLEAR").on_press(Message::Clear), row![tool_area, canvas_area], ] } pub fn update(&mut self, message: Message) -> Task { match self.tool_selected { + Tool::FillWithColor => { + self.update_with_fill_with_color(message); + } Tool::Pencil => { self.update_with_pencil(message); } @@ -239,8 +268,16 @@ impl Paint { pub fn update_with_fill_with_color(&mut self, message: Message) { match message { - Message::MousePressed(_pos) => { - + Message::MousePressed(pos) => { + // let start = Instant::now(); + // let (iter_count, fill_count) = self.fill_scanline(pos); + // let elapsed = start.elapsed(); + // println!( + // "fill_scanline: {:?}, iter_count: {iter_count}, fill_count: {fill_count}, ratio: {}", + // elapsed, + // (iter_count as f32 / fill_count as f32) + // ); + self.fill_scanline(pos); } _ => {} } @@ -248,21 +285,20 @@ impl Paint { pub fn update_with_pencil(&mut self, message: Message) { match message { - Message::MousePressed(_pos) => { + Message::MousePressed(pos) => { + println!("pressed: {:?}", pos); self.is_drawing = true; + self.draw_pixel_at1(pos); + self.begin_point = pos; } - Message::MouseReleased => { + Message::MouseReleased(pos) => { self.is_drawing = false; - self.begin_point = None; + self.begin_point = pos; } Message::MouseMoved(pos) => { if self.is_drawing { - if let Some(begin_point) = self.begin_point { - self.draw_line(begin_point, pos); - } else { - self.draw_pixel_at1(pos); - } - self.begin_point = Some(pos); + self.draw_line(self.begin_point, pos); + self.begin_point = pos; } } _ => {} @@ -271,23 +307,19 @@ impl Paint { pub fn update_with_line(&mut self, message: Message) { match message { - Message::MousePressed(_pos) => { + Message::MousePressed(pos) => { self.is_drawing = true; self.save_pixels(); + self.begin_point = pos; } - Message::MouseReleased => { + Message::MouseReleased(pos) => { self.is_drawing = false; - self.begin_point = None; - + self.begin_point = pos; } Message::MouseMoved(pos) => { if self.is_drawing { - if let Some(begin_point) = self.begin_point { - self.restore_pixels(); - self.draw_line(begin_point, pos); - } else { - self.begin_point = Some(pos); - } + self.restore_pixels(); + self.draw_line(self.begin_point, pos); } } _ => {} @@ -320,20 +352,55 @@ impl Paint { } /// draw method +#[allow(unused)] impl Paint { - /// 核心绘图逻辑:直接在字节数组上操作 - fn draw_pixel_at(&mut self, pos: Point) { - let x = pos.x; - let y = pos.y; - + fn pixel_at(&self, x: i32, y: i32) -> ColorU8 { // 边界检查 if x < 0 || x >= WIDTH as i32 || y < 0 || y >= HEIGHT as i32 { - return; + return ColorU8::new(0, 0, 0, 0); } let x = x as u32; let y = y as u32; + let index = ((y * WIDTH + x) * 4) as usize; + + ColorU8::new( + self.pixels[index], + self.pixels[index + 1], + self.pixels[index + 2], + self.pixels[index + 3], + ) + } + + fn pixel_at_raw(&self, x: i32, y: i32) -> ColorU8 { + let x = x as u32; + let y = y as u32; + let index = ((y * WIDTH + x) * 4) as usize; + ColorU8::new( + self.pixels[index], + self.pixels[index + 1], + self.pixels[index + 2], + self.pixels[index + 3], + ) + } + + fn pixel_at1(&self, point: Point) -> ColorU8 { + self.pixel_at(point.x as i32, point.y as i32) + } + + fn draw_brush_at(&mut self, center_x: i32, center_y: i32) { + let r = self.brush_radius; + for dy in -r..=r { + for dx in -r..=r { + if dx * dx + dy * dy <= r * r { + self.draw_pixel_at(center_x + dx, center_y + dy); + } + } + } + } + + fn draw_pixel_at_raw(&mut self, x: u32, y: u32) { // 计算索引:(y * width + x) * 4 let index = ((y * WIDTH + x) * 4) as usize; @@ -347,8 +414,39 @@ impl Paint { self.dirty = true; } + /// 核心绘图逻辑:直接在字节数组上操作 + fn draw_pixel_at(&mut self, x: i32, y: i32) { + // 边界检查 + if x < 0 || x >= WIDTH as i32 || y < 0 || y >= HEIGHT as i32 { + return; + } + self.draw_pixel_at_raw(x as u32, y as u32); + } + + fn draw_pixel_row(&mut self, xs: i32, xe: i32, y: i32) { + if y < 0 || y >= HEIGHT as i32 { + return; + } + + let xs = xs.clamp(0, WIDTH as i32 - 1) as u32; + let xe = xe.clamp(0, WIDTH as i32) as u32; + let y = y as u32; + for x in xs..xe { + let index = ((y * WIDTH + x) * 4) as usize; + + // 写入 RGBA 数据 + // 注意:Color 的 r, g, b, a 是 0.0 - 1.0,需要转为 0 - 255 + self.pixels[index] = (self.color.r * 255.0) as u8; // R + self.pixels[index + 1] = (self.color.g * 255.0) as u8; // G + self.pixels[index + 2] = (self.color.b * 255.0) as u8; // B + self.pixels[index + 3] = (self.color.a * 255.0) as u8; // A + } + + self.dirty = true; + } + fn draw_pixel_at1(&mut self, pos: Point) { - self.draw_pixel_at(Point::new(pos.x as i32, pos.y as i32)) + self.draw_pixel_at(pos.x as i32, pos.y as i32) } fn draw_lines(&mut self, points: &[Point]) { @@ -369,13 +467,17 @@ impl Paint { /// Bresenham's line drawing algorithm fn draw_line(&mut self, begin: Point, end: Point) { - let x1 = begin.x; - let y1 = begin.y; - let x2 = end.x; - let y2 = end.y; + let x1 = begin.x as i32; + let y1 = begin.y as i32; + let x2 = end.x as i32; + let y2 = end.y as i32; - let dx = (x2 - x1) as i32; - let dy = (y2 - y1) as i32; + // draw start end point, 防止多条线段在连接点出现断开(比如 ab bc) + self.draw_pixel_at(x1, y1); + self.draw_pixel_at(x2, y2); + + let dx = (x2 - x1); + let dy = (y2 - y1); let dx1 = dx.abs(); let dy1 = dy.abs(); let mut px = 2 * dy1 - dx1; @@ -388,16 +490,15 @@ impl Paint { if dy1 <= dx1 { if dx >= 0 { - x = x1 as i32; - y = y1 as i32; - xe = x2 as i32; + x = x1; + y = y1; + xe = x2; } else { - x = x2 as i32; - y = y2 as i32; - xe = x1 as i32; + x = x2; + y = y2; + xe = x1; } - let point = Point::new(x, y); - self.draw_pixel_at(point); + self.draw_pixel_at(x, y); while x < xe { x += 1; if px < 0 { @@ -410,21 +511,19 @@ impl Paint { } px = px + 2 * (dy1 - dx1); } - let point = Point::new(x, y); - self.draw_pixel_at(point); + self.draw_pixel_at(x, y); } } else { if dy >= 0 { - x = x1 as i32; - y = y1 as i32; - ye = y2 as i32; + x = x1; + y = y1; + ye = y2; } else { - x = x2 as i32; - y = y2 as i32; - ye = y1 as i32; + x = x2; + y = y2; + ye = y1; } - let point = Point::new(x, y); - self.draw_pixel_at(point); + self.draw_pixel_at(x, y); while y < ye { y = y + 1; if py <= 0 { @@ -437,8 +536,7 @@ impl Paint { } py = py + 2 * (dx1 - dy1); } - let point = Point::new(x, y); - self.draw_pixel_at(point); + self.draw_pixel_at(x, y); } } } @@ -450,6 +548,170 @@ impl Paint { fn restore_pixels(&mut self) { self.pixels = self.pixels_bak.clone(); } + + fn fill_slow(&mut self, begin: Point) -> (i32, i32) { + let start_x = begin.x as i32; + let start_y = begin.y as i32; + let target_color = self.pixel_at(start_x, start_y); + if target_color.is_same(self.color) { + return (0, 0); + } + + let mut scan_points = vec![(start_x, start_y)]; + let width = WIDTH as i32; + let height = HEIGHT as i32; + let mut iter_count = 0; + let mut fill_count = 0; + while let Some((x, y)) = scan_points.pop() { + iter_count += 1; + + if x < 0 || x >= width || y < 0 || y >= height { + continue; + } + if self.pixel_at_raw(x, y) == target_color { + self.draw_pixel_at_raw(x as u32, y as u32); + fill_count += 1; + + let p1 = (x - 1, y); + let p2 = (x + 1, y); + let p3 = (x, y - 1); + let p4 = (x, y + 1); + scan_points.push(p1); + scan_points.push(p2); + scan_points.push(p3); + scan_points.push(p4); + } + } + (iter_count, fill_count) + } + + fn fill_less_slow(&mut self, begin: Point) -> (i32, i32) { + let start_x = begin.x as i32; + let start_y = begin.y as i32; + let width = WIDTH as i32; + let height = HEIGHT as i32; + + if start_x < 0 || start_x >= width || start_y < 0 || start_y >= height { + return (0, 0); + } + + let target_color = self.pixel_at(start_x, start_y); + if target_color.is_same(self.color) { + return (0, 0); + } + + let mut stack = vec![(start_x, start_y)]; + let mut iter_count = 0; + let mut fill_count = 0; + + while let Some((x, y)) = stack.pop() { + iter_count += 1; + + if x < 0 || x >= width || y < 0 || y >= height { + continue; + } + + if self.pixel_at_raw(x, y) != target_color { + continue; + } + + self.draw_pixel_at_raw(x as u32, y as u32); + fill_count += 1; + + // 提前检查边界再入栈 + if x > 0 { + stack.push((x - 1, y)); + } + if x + 1 < width { + stack.push((x + 1, y)); + } + if y > 0 { + stack.push((x, y - 1)); + } + if y + 1 < height { + stack.push((x, y + 1)); + } + } + + (iter_count, fill_count) + } + + fn fill_scanline(&mut self, begin: Point) -> (i32, i32) { + let start_x = begin.x as i32; + let start_y = begin.y as i32; + let width = WIDTH as i32; + let height = HEIGHT as i32; + + // 边界检查 + if start_x < 0 || start_x >= width || start_y < 0 || start_y >= height { + return (0, 0); + } + + let target_color = self.pixel_at(start_x, start_y); + if target_color.is_same(self.color) { + return (0, 0); + } + + // 栈中存储 (y, x1, x2):表示第 y 行从 x1 到 x2(含)需要向上/下扫描 + let mut stack = vec![(start_y, start_x, start_x)]; + let mut iter_count = 0; + let mut fill_count = 0; + + while let Some((y, mut lx, mut rx)) = stack.pop() { + iter_count += 1; + + // 向左扩展 lx + while lx - 1 >= 0 && self.pixel_at_raw(lx - 1, y) == target_color { + lx -= 1; + } + // 向右扩展 rx + while rx + 1 < width && self.pixel_at_raw(rx + 1, y) == target_color { + rx += 1; + } + + // 填充当前行 [lx, rx] + for x in lx..=rx { + self.draw_pixel_at_raw(x as u32, y as u32); + fill_count += 1; + } + + // 检查上一行 (y - 1) + if y - 1 >= 0 { + let mut x = lx; + while x <= rx { + if self.pixel_at_raw(x, y - 1) == target_color { + let span_start = x; + // 跳过连续的目标色块 + while x <= rx && self.pixel_at_raw(x, y - 1) == target_color { + x += 1; + } + // 将这个 span 入栈(用于后续处理上一行的上一行) + stack.push((y - 1, span_start, x - 1)); + } else { + x += 1; + } + } + } + + // 检查下一行 (y + 1) + if y + 1 < height { + let mut x = lx; + while x <= rx { + if self.pixel_at_raw(x, y + 1) == target_color { + let span_start = x; + while x <= rx && self.pixel_at_raw(x, y + 1) == target_color { + x += 1; + } + stack.push((y + 1, span_start, x - 1)); + } else { + x += 1; + } + } + } + } + + (iter_count, fill_count) + } } pub fn main() -> iced::Result {