diff --git a/Cargo.lock b/Cargo.lock index 5391908..41360f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1952,6 +1952,7 @@ name = "mspaint" version = "0.1.0" dependencies = [ "iced", + "iced_core", ] [[package]] @@ -3575,9 +3576,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.2" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" diff --git a/Cargo.toml b/Cargo.toml index adcba7e..401b81d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,4 @@ edition = "2024" [dependencies] iced = {version = "0.14.0", features = ["advanced", "image"]} +iced_core = "0.14.0" diff --git a/src/image_button.rs b/src/image_button.rs index 3643f58..168d473 100644 --- a/src/image_button.rs +++ b/src/image_button.rs @@ -1,18 +1,11 @@ use iced::advanced::layout::{self, Layout}; -use iced::advanced::{renderer, Clipboard}; use iced::advanced::widget::{self, Widget}; +use iced::advanced::{Clipboard, renderer}; use iced::mouse; use iced::{Element, Event, Length, Rectangle, Size}; // We need the image renderer trait use iced::advanced::image as img; -// ---------- State ---------- - -/// Internal state tracking whether the button is currently pressed. -#[derive(Default)] -pub struct State { - is_pressed: bool, -} // ---------- Widget struct ---------- @@ -26,7 +19,11 @@ pub struct ImageButton { is_pressed: bool, } -pub fn image_button(normal: impl Into, pressed: impl Into, is_pressed: bool) -> ImageButton { +pub fn image_button( + normal: impl Into, + pressed: impl Into, + is_pressed: bool, +) -> ImageButton { ImageButton::new(normal, pressed, is_pressed) } @@ -68,22 +65,12 @@ impl ImageButton { // ---------- Widget impl ---------- impl Widget -for ImageButton + for ImageButton where Renderer: img::Renderer, Handle: Clone, Message: Clone, { - // --- Tree (internal state) --- - - fn tag(&self) -> widget::tree::Tag { - widget::tree::Tag::of::() - } - - fn state(&self) -> widget::tree::State { - widget::tree::State::new(State::default()) - } - // --- Size --- fn size(&self) -> Size { @@ -116,7 +103,7 @@ where fn update( &mut self, - tree: &mut widget::Tree, + _tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, @@ -125,7 +112,6 @@ where shell: &mut iced::advanced::Shell<'_, Message>, _viewport: &Rectangle, ) { - let state = tree.state.downcast_mut::(); let bounds = layout.bounds(); match event { @@ -163,7 +149,7 @@ where fn draw( &self, - tree: &widget::Tree, + _tree: &widget::Tree, renderer: &mut Renderer, _theme: &Theme, _style: &renderer::Style, @@ -171,7 +157,6 @@ where _cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); // Pick the correct image handle. @@ -199,7 +184,7 @@ where // ---------- Into ---------- impl<'a, Message, Theme, Renderer, Handle> From> -for Element<'a, Message, Theme, Renderer> + for Element<'a, Message, Theme, Renderer> where Renderer: img::Renderer + 'a, Handle: Clone + 'a, @@ -209,4 +194,4 @@ where fn from(widget: ImageButton) -> Self { Self::new(widget) } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 31d643d..cd9599a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,154 +1,7 @@ - - -use iced::widget::{Column, Row, button, column, text, Grid, row}; -use iced::{Border, Color, Task}; -use iced::padding; -use iced::widget::container; -use iced::Theme; - mod image_button; +mod paint; +mod mouse_area; -#[derive(Default)] -struct Paint { - tool_pressed: [bool; Tool::Count as usize], -} -#[derive(Debug, Clone, Copy)] -pub enum Message { - Decrement, - - FreeFormSelectClicked, - SelectClicked, - EraserClicked, - FillWithColorClicked, - PickColorClicked, - MagnifierClicked, - PencilClicked, - BrushClicked, - AirbrushClicked, - TextClicked, - LineClicked, - CurveClicked, - RectangleClicked, - PolygonClicked, - EllipseClicked, - RoundedRectangleClicked, -} - -impl Message { - pub fn to_tool_index(&self) -> usize { - match self { - Message::FreeFormSelectClicked => 0, - Message::SelectClicked => 1, - Message::EraserClicked => 2, - Message::FillWithColorClicked => 3, - Message::PickColorClicked => 4, - Message::MagnifierClicked => 5, - Message::PencilClicked => 6, - Message::BrushClicked => 7, - Message::AirbrushClicked => 8, - Message::TextClicked => 9, - Message::LineClicked => 10, - Message::CurveClicked => 11, - Message::RectangleClicked => 12, - Message::PolygonClicked => 13, - Message::EllipseClicked => 14, - Message::RoundedRectangleClicked => 15, - _ => usize::MAX, - } - } -} - -pub enum Tool { - FreeFormSelect, - Select, - Eraser, - FillWithColor, - PickColor, - Magnifier, - Pencil, - Brush, - Airbrush, - Text, - Line, - Curve, - Rectangle, - Polygon, - Ellipse, - RoundedRectangle, - - Count, -} - -impl Paint { - pub fn view(&self) -> Column<'_, Message> { - let mut grid = Grid::new(); - grid = grid.columns(2).width(100); - - for i in 0..(Tool::Count as usize) { - let msg = match i { - 0 => Message::FreeFormSelectClicked, - 1 => Message::SelectClicked, - 2 => Message::EraserClicked, - 3 => Message::FillWithColorClicked, - 4 => Message::PickColorClicked, - 5 => Message::MagnifierClicked, - 6 => Message::PencilClicked, - 7 => Message::BrushClicked, - 8 => Message::AirbrushClicked, - 9 => Message::TextClicked, - 10 => Message::LineClicked, - 11 => Message::CurveClicked, - 12 => Message::RectangleClicked, - 13 => Message::PolygonClicked, - 14 => Message::EllipseClicked, - 15 => Message::RoundedRectangleClicked, - _ => Message::FreeFormSelectClicked, - }; - let btn = image_button::image_button( - format!("image/normal/normal_{:02}.jpg", i + 1), - format!("image/selected/selected_{:02}.jpg", i + 1), - self.tool_pressed[i], - ).on_press(msg); - grid = grid.push(btn); - } - - // We use a column: a simple vertical layout - column![ - button("-").on_press(Message::Decrement), - - container(grid).padding(padding::left(5).right(5).bottom(100)).style(|theme: &Theme| { - let palette = theme.extended_palette(); - - container::Style { - background: Some(Color::from_rgb8(192, 192, 192).into()), - text_color: Some(palette.background.weakest.text), - border: Border { - width: 1.0, - radius: 5.0.into(), - color: palette.background.weak.color, - }, - ..container::Style::default() - } - }), - ] - } - pub fn update(&mut self, message: Message) { - self.press_tool(&message); - } - - pub fn press_tool(&mut self, message: &Message) { - let idx = message.to_tool_index(); - if idx == usize::MAX { - return; - } - let old_value = self.tool_pressed[idx]; - for i in 0..(Tool::Count as usize) { - self.tool_pressed[i] = false; - } - self.tool_pressed[idx] = !old_value; - } -} - -fn main() -> iced::Result { - iced::run(Paint::update, Paint::view) +pub fn main() -> iced::Result { + paint::main() } diff --git a/src/mouse_area.rs b/src/mouse_area.rs new file mode 100644 index 0000000..6de4fb8 --- /dev/null +++ b/src/mouse_area.rs @@ -0,0 +1,533 @@ +//! A container for capturing mouse events. +//! +//! This is a sweetened version of `iced`'s [`MouseArea`] where all event +//! handlers receive the cursor position as a [`Point`]. +//! +//! [`MouseArea`]: https://docs.iced.rs/iced/widget/struct.MouseArea.html +//! +//! # Example +//! ```no_run +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced::Element<'a, Message>; +//! use iced::Point; +//! use iced::widget::text; +//! use sweeten::widget::mouse_area; +//! +//! #[derive(Clone)] +//! enum Message { +//! Clicked(Point), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! mouse_area(text("Click me!")) +//! .on_press(Message::Clicked) +//! .into() +//! } +//! ``` +use iced_core::layout; +use iced_core::mouse; +use iced_core::overlay; +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, +}; + +/// Emit messages on mouse events. +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>>, + on_double_click: Option Message + 'a>>, + on_right_press: Option Message + 'a>>, + on_right_release: Option Message + 'a>>, + on_middle_press: Option Message + 'a>>, + on_middle_release: Option Message + 'a>>, + on_scroll: Option Message + 'a>>, + on_enter: Option Message + 'a>>, + on_move: Option Message + 'a>>, + on_exit: Option Message + 'a>>, + interaction: Option, +} + +impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { + /// Sets the message to emit on a left button press. + /// + /// The closure receives the click position as a [`Point`]. + #[must_use] + pub fn on_press(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { + self.on_press = Some(Box::new(f)); + self + } + + /// Sets the message to emit on a left button press, if `Some`. + /// + /// The closure receives the click position as a [`Point`]. + #[must_use] + pub fn on_press_maybe( + mut self, + f: Option Message + 'a>, + ) -> Self { + self.on_press = f.map(|f| Box::new(f) as _); + self + } + + /// Sets the message to emit on a left button release. + /// + /// The closure receives the release position as a [`Point`]. + #[must_use] + pub fn on_release(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { + self.on_release = Some(Box::new(f)); + self + } + + /// Sets the message to emit on a double click. + /// + /// The closure receives the click position as a [`Point`]. + /// + /// If you use this with [`on_press`]/[`on_release`], those + /// events will be emitted as normal. + /// + /// The event stream will be: on_press -> on_release -> on_press + /// -> on_double_click -> on_release -> on_press ... + /// + /// [`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 { + self.on_double_click = Some(Box::new(f)); + self + } + + /// Sets the message to emit on a right button press. + /// + /// The closure receives the click position as a [`Point`]. + #[must_use] + pub fn on_right_press(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { + self.on_right_press = Some(Box::new(f)); + self + } + + /// Sets the message to emit on a right button release. + /// + /// 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 { + self.on_right_release = Some(Box::new(f)); + self + } + + /// Sets the message to emit on a middle button press. + /// + /// 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 { + self.on_middle_press = Some(Box::new(f)); + self + } + + /// Sets the message to emit on a middle button release. + /// + /// 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 { + 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 { + self.on_scroll = Some(Box::new(on_scroll)); + self + } + + /// Sets the message to emit when the mouse enters the area. + /// + /// The closure receives the entry position as a [`Point`]. + #[must_use] + pub fn on_enter(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { + self.on_enter = Some(Box::new(f)); + self + } + + /// Sets the message to emit when the mouse moves in the area. + /// + /// The closure receives the current position as a [`Point`]. + #[must_use] + pub fn on_move(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { + self.on_move = Some(Box::new(f)); + self + } + + /// Sets the message to emit when the mouse exits the area. + /// + /// The closure receives the exit position as a [`Point`]. + #[must_use] + pub fn on_exit(mut self, f: impl Fn(Point) -> Message + 'a) -> Self { + self.on_exit = Some(Box::new(f)); + self + } + + /// The [`mouse::Interaction`] to use when hovering the area. + #[must_use] + pub fn interaction(mut self, interaction: mouse::Interaction) -> Self { + self.interaction = Some(interaction); + self + } +} + +/// Local state of the [`MouseArea`]. +#[derive(Default)] +struct State { + is_hovered: bool, + bounds: Rectangle, + cursor_position: Option, + previous_click: Option, +} + +impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { + /// Creates a [`MouseArea`] with the given content. + pub fn new( + content: impl Into>, + ) -> Self { + MouseArea { + content: content.into(), + on_press: None, + on_release: None, + on_double_click: None, + on_right_press: None, + on_right_release: None, + on_middle_press: None, + on_middle_release: None, + on_scroll: None, + on_enter: None, + on_move: None, + on_exit: None, + interaction: None, + } + } +} + +impl Widget +for MouseArea<'_, Message, Theme, Renderer> +where + Renderer: renderer::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(std::slice::from_ref(&self.content)); + } + + fn size(&self) -> Size { + self.content.as_widget().size() + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content.as_widget_mut().layout( + &mut tree.children[0], + renderer, + limits, + ) + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + + if shell.is_event_captured() { + return; + } + + update(self, tree, event, layout, cursor, shell); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let content_interaction = self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ); + + match (self.interaction, content_interaction) { + (Some(interaction), mouse::Interaction::None) + if cursor.is_over(layout.bounds()) => + { + interaction + } + _ => content_interaction, + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + renderer_style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + renderer_style, + layout, + cursor, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From> +for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: 'a + renderer::Renderer, +{ + fn from( + area: MouseArea<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(area) + } +} + +/// Processes the given [`Event`] and updates the [`State`] of an [`MouseArea`] +/// accordingly. +fn update( + widget: &mut MouseArea<'_, Message, Theme, Renderer>, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + shell: &mut Shell<'_, Message>, +) { + let state: &mut State = tree.state.downcast_mut(); + + let cursor_position = cursor.position(); + let bounds = layout.bounds(); + + if state.cursor_position != cursor_position || state.bounds != bounds { + let was_hovered = state.is_hovered; + + state.is_hovered = cursor.is_over(layout.bounds()); + state.cursor_position = cursor_position; + state.bounds = bounds; + + if let Some(position) = cursor.position_in(layout.bounds()) { + match ( + widget.on_enter.as_ref(), + widget.on_move.as_ref(), + widget.on_exit.as_ref(), + ) { + (Some(on_enter), _, _) if state.is_hovered && !was_hovered => { + shell.publish(on_enter(position)); + } + (_, Some(on_move), _) if state.is_hovered => { + shell.publish(on_move(position)); + } + (_, _, Some(on_exit)) if !state.is_hovered && was_hovered => { + shell.publish(on_exit(position)); + } + _ => {} + } + } + } + + if !cursor.is_over(layout.bounds()) { + return; + } + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(on_press) = widget.on_press.as_ref() { + if let Some(position) = cursor.position_in(layout.bounds()) { + shell.publish(on_press(position)); + shell.capture_event(); + } + } + + 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, + ); + + if new_click.kind() == mouse::click::Kind::Double { + shell.publish(on_double_click(position)); + } + + state.previous_click = Some(new_click); + + // Even if this is not a double click, but the press is nevertheless + // processed by us and should not be popup to parent widgets. + shell.capture_event(); + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + if let Some(on_release) = widget.on_release.as_ref() { + if let Some(position) = cursor.position_in(layout.bounds()) { + shell.publish(on_release(position)); + } + } + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { + if let Some(on_right_press) = widget.on_right_press.as_ref() { + if let Some(position) = cursor.position_in(layout.bounds()) { + shell.publish(on_right_press(position)); + shell.capture_event(); + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => { + if let Some(on_right_release) = widget.on_right_release.as_ref() { + if let Some(position) = cursor.position_in(layout.bounds()) { + shell.publish(on_right_release(position)); + } + } + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => { + if let Some(on_middle_press) = widget.on_middle_press.as_ref() { + if let Some(position) = cursor.position_in(layout.bounds()) { + shell.publish(on_middle_press(position)); + shell.capture_event(); + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) => { + if let Some(on_middle_release) = widget.on_middle_release.as_ref() { + if let Some(position) = cursor.position_in(layout.bounds()) { + shell.publish(on_middle_release(position)); + } + } + } + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if let Some(on_scroll) = widget.on_scroll.as_ref() { + shell.publish(on_scroll(*delta)); + shell.capture_event(); + } + } + _ => {} + } +} + +/// Creates a new [`MouseArea`] for capturing mouse events. +/// +/// This is a sweetened version of [`iced`'s `MouseArea`] where all event +/// handlers receive the cursor position as a [`Point`]. +/// +/// [`iced`'s `MouseArea`]: https://docs.iced.rs/iced/widget/struct.MouseArea.html +/// [`Point`]: crate::core::Point +pub fn mouse_area<'a, Message, Theme, Renderer>( + widget: impl Into>, +) -> MouseArea<'a, Message, Theme, Renderer> +where + Renderer: iced_core::Renderer, +{ + MouseArea::new(widget) +} diff --git a/src/paint.rs b/src/paint.rs new file mode 100644 index 0000000..17510ac --- /dev/null +++ b/src/paint.rs @@ -0,0 +1,459 @@ +use iced::Theme; +use iced::padding; +use iced::widget::container; +use iced::widget::{Column, Grid, button, column, image, row, mouse_area}; +use iced::{Border, Color, Length, Point, Task}; + + +use crate::image_button::image_button; + +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, + MouseMoved(Point), + Clear, + ChangeColor(Color), + // 内部消息:请求刷新图像 + RefreshImage, + + ClickTool(Tool), +} + +#[derive(Clone, Copy, Debug)] +pub enum Tool { + FreeFormSelect, + Select, + Eraser, + FillWithColor, + PickColor, + Magnifier, + Pencil, + Brush, + Airbrush, + Text, + Line, + Curve, + Rectangle, + Polygon, + Ellipse, + RoundedRectangle, + + Count, +} + +impl From for Tool { + fn from(tool: usize) -> Self { + match tool { + 0 => Self::FreeFormSelect, + 1 => Self::Select, + 2 => Self::Eraser, + 3 => Self::FillWithColor, + 4 => Self::PickColor, + 5 => Self::Magnifier, + 6 => Self::Pencil, + 7 => Self::Brush, + 8 => Self::Airbrush, + 9 => Self::Text, + 10 => Self::Line, + 11 => Self::Curve, + 12 => Self::Rectangle, + 13 => Self::Polygon, + 14 => Self::Ellipse, + 15 => Self::RoundedRectangle, + _ => Self::Count, + } + } +} + +impl From for usize { + fn from(tool: Tool) -> usize { + match tool { + Tool::FreeFormSelect => 0, + Tool::Select => 1, + Tool::Eraser => 2, + Tool::FillWithColor => 3, + Tool::PickColor => 4, + Tool::Magnifier => 5, + Tool::Pencil => 6, + Tool::Brush => 7, + Tool::Airbrush => 8, + Tool::Text => 9, + Tool::Line => 10, + Tool::Curve => 11, + Tool::Rectangle => 12, + Tool::Polygon => 13, + Tool::Ellipse => 14, + Tool::RoundedRectangle => 15, + Tool::Count => 16, + } + } +} + + +impl Paint { + // region iced application (new view update) + + pub fn new() -> Self { + // 初始化全白背景 (R=255, G=255, B=255, A=255) + let pixels = vec![255u8; (WIDTH * HEIGHT * 4) as usize]; + let data = pixels.clone(); + + Self { + tool_states: [false; Tool::Count as usize], + tool_selected: Tool::Count, + pixels, + pixels_bak: Vec::new(), + color: Color::BLACK, + is_drawing: false, + begin_point: None, + image_handle: image::Handle::from_rgba(WIDTH, HEIGHT, data), + dirty: false, + } + } + + pub fn view(&self) -> Column<'_, Message> { + // 创建显示图像的 Widget + // 如果 handle 还没准备好,显示一个占位符 + let image_widget = image(self.image_handle.clone()) + .width(Length::Fixed(WIDTH as f32)) + .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_move(|pos| Message::MouseMoved(pos)); + // 注意:mouse_area 的 on_move 给出的坐标通常是相对于 widget 左上角的,这正是我们需要的! + + let mut grid = Grid::new(); + grid = grid.columns(2).width(100); + + for i in 0..(Tool::Count as usize) { + let tool = Tool::from(i); + let btn = image_button( + format!("image/normal/normal_{:02}.jpg", i + 1), + format!("image/selected/selected_{:02}.jpg", i + 1), + self.tool_states[i], + ) + .on_press(Message::ClickTool(tool)); + grid = grid.push(btn); + } + + let tool_area = container(grid) + .padding(padding::top(5).left(5).right(5).bottom(100)) + .style(|theme: &Theme| { + let palette = theme.extended_palette(); + + container::Style { + background: Some(Color::from_rgb8(192, 192, 192).into()), + border: Border { + width: 1.0, + radius: 5.0.into(), + color: palette.background.weak.color, + }, + ..container::Style::default() + } + }); + + // We use a column: a simple vertical layout + column![ + button("-").on_press(Message::Decrement), + row![tool_area, canvas_area], + ] + } + + pub fn update(&mut self, message: Message) -> Task { + match self.tool_selected { + Tool::Pencil => { + self.update_with_pencil(message); + } + Tool::Line => { + self.update_with_line(message); + } + _ => {} + } + + match message { + Message::Clear => { + // 重置为白色 + self.pixels.fill(255); + self.dirty = true; + } + Message::ChangeColor(c) => { + self.color = c; + } + Message::RefreshImage => { + if self.dirty { + self.update_image_handle(); + self.dirty = false; + } + } + Message::ClickTool(tool) => { + self.update_tool_states(tool); + } + _ => {} + } + + // 如果像素被修改了,我们需要触发一次 RefreshImage 来更新 UI + // 在实际复杂应用中,可能需要防抖或异步处理,这里为了实时性直接同步触发 + if self.dirty { + // 像素变了,安排下一帧刷新图像句柄 + // 注意:频繁生成 Handle 可能消耗 CPU,生产环境建议加节流 + return Task::perform(async { Message::RefreshImage }, |msg| msg); + } + + Task::none() + } + + // endregion + + // region tool update + + pub fn update_with_fill_with_color(&mut self, message: Message) { + match message { + Message::MousePressed(_pos) => { + + } + _ => {} + } + } + + pub fn update_with_pencil(&mut self, message: Message) { + match message { + Message::MousePressed(_pos) => { + self.is_drawing = true; + } + Message::MouseReleased => { + self.is_drawing = false; + self.begin_point = None; + } + 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); + } + } + _ => {} + } + } + + pub fn update_with_line(&mut self, message: Message) { + match message { + Message::MousePressed(_pos) => { + self.is_drawing = true; + self.save_pixels(); + } + Message::MouseReleased => { + self.is_drawing = false; + self.begin_point = None; + + } + 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); + } + } + } + _ => {} + } + } + + // endregion + + pub fn update_tool_states(&mut self, tool: Tool) { + let idx = tool as usize; + if idx >= self.tool_states.len() { + return; + } + let old_value = self.tool_states[idx]; + for i in 0..(Tool::Count as usize) { + self.tool_states[i] = false; + } + self.tool_states[idx] = !old_value; + self.tool_selected = idx.into(); + } + + /// 将原始字节转换为 Iced 的图像句柄 + fn update_image_handle(&mut self) { + // 克隆数据以避免所有权问题,或者使用 Arc 如果数据量大 + // 这里为了简单直接 clone,对于 800x600 (约 2MB) 来说很快 + let data = self.pixels.clone(); + + self.image_handle = image::Handle::from_rgba(WIDTH, HEIGHT, data); + } +} + +/// draw method +impl Paint { + /// 核心绘图逻辑:直接在字节数组上操作 + fn draw_pixel_at(&mut self, pos: Point) { + let x = pos.x; + let y = pos.y; + + // 边界检查 + if x < 0 || x >= WIDTH as i32 || y < 0 || y >= HEIGHT as i32 { + return; + } + + let x = x as u32; + let y = y as u32; + + // 计算索引:(y * width + x) * 4 + 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)) + } + + fn draw_lines(&mut self, points: &[Point]) { + if points.is_empty() { + return; + } + if points.len() == 1 { + self.draw_pixel_at1(points[0]); + return; + } + + let mut begin = points[0]; + for point in points.iter().skip(1) { + self.draw_line(begin, point.clone()); + begin = point.clone(); + } + } + + /// 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 dx = (x2 - x1) as i32; + let dy = (y2 - y1) as i32; + let dx1 = dx.abs(); + let dy1 = dy.abs(); + let mut px = 2 * dy1 - dx1; + let mut py = 2 * dx1 - dy1; + + let mut x; + let mut y; + let xe; + let ye; + + if dy1 <= dx1 { + if dx >= 0 { + x = x1 as i32; + y = y1 as i32; + xe = x2 as i32; + } else { + x = x2 as i32; + y = y2 as i32; + xe = x1 as i32; + } + let point = Point::new(x, y); + self.draw_pixel_at(point); + while x < xe { + x += 1; + if px < 0 { + px = px + 2 * dy1; + } else { + if (dx < 0 && dy < 0) || (dx > 0 && dy > 0) { + y = y + 1; + } else { + y = y - 1; + } + px = px + 2 * (dy1 - dx1); + } + let point = Point::new(x, y); + self.draw_pixel_at(point); + } + } else { + if dy >= 0 { + x = x1 as i32; + y = y1 as i32; + ye = y2 as i32; + } else { + x = x2 as i32; + y = y2 as i32; + ye = y1 as i32; + } + let point = Point::new(x, y); + self.draw_pixel_at(point); + while y < ye { + y = y + 1; + if py <= 0 { + py = py + 2 * dx1; + } else { + if (dx < 0 && dy < 0) || (dx > 0 && dy > 0) { + x = x + 1; + } else { + x = x - 1; + } + py = py + 2 * (dx1 - dy1); + } + let point = Point::new(x, y); + self.draw_pixel_at(point); + } + } + } + + fn save_pixels(&mut self) { + self.pixels_bak = self.pixels.clone(); + } + + fn restore_pixels(&mut self) { + self.pixels = self.pixels_bak.clone(); + } +} + +pub fn main() -> iced::Result { + iced::application(Paint::new, Paint::update, Paint::view) + .theme(Theme::CatppuccinMocha) + .run() +}