Implement pencil and line tool
This commit is contained in:
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -1952,6 +1952,7 @@ name = "mspaint"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iced",
|
"iced",
|
||||||
|
"iced_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3575,9 +3576,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
version = "0.2.2"
|
version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced = {version = "0.14.0", features = ["advanced", "image"]}
|
iced = {version = "0.14.0", features = ["advanced", "image"]}
|
||||||
|
iced_core = "0.14.0"
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
use iced::advanced::layout::{self, Layout};
|
use iced::advanced::layout::{self, Layout};
|
||||||
use iced::advanced::{renderer, Clipboard};
|
|
||||||
use iced::advanced::widget::{self, Widget};
|
use iced::advanced::widget::{self, Widget};
|
||||||
|
use iced::advanced::{Clipboard, renderer};
|
||||||
use iced::mouse;
|
use iced::mouse;
|
||||||
use iced::{Element, Event, Length, Rectangle, Size};
|
use iced::{Element, Event, Length, Rectangle, Size};
|
||||||
|
|
||||||
// We need the image renderer trait
|
// We need the image renderer trait
|
||||||
use iced::advanced::image as img;
|
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 ----------
|
// ---------- Widget struct ----------
|
||||||
|
|
||||||
@@ -26,7 +19,11 @@ pub struct ImageButton<Handle, Message> {
|
|||||||
is_pressed: bool,
|
is_pressed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn image_button<Handle, Message>(normal: impl Into<Handle>, pressed: impl Into<Handle>, is_pressed: bool) -> ImageButton<Handle, Message> {
|
pub fn image_button<Handle, Message>(
|
||||||
|
normal: impl Into<Handle>,
|
||||||
|
pressed: impl Into<Handle>,
|
||||||
|
is_pressed: bool,
|
||||||
|
) -> ImageButton<Handle, Message> {
|
||||||
ImageButton::new(normal, pressed, is_pressed)
|
ImageButton::new(normal, pressed, is_pressed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,22 +65,12 @@ impl<Handle, Message> ImageButton<Handle, Message> {
|
|||||||
// ---------- Widget impl ----------
|
// ---------- Widget impl ----------
|
||||||
|
|
||||||
impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
|
impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
|
||||||
for ImageButton<Handle, Message>
|
for ImageButton<Handle, Message>
|
||||||
where
|
where
|
||||||
Renderer: img::Renderer<Handle = Handle>,
|
Renderer: img::Renderer<Handle = Handle>,
|
||||||
Handle: Clone,
|
Handle: Clone,
|
||||||
Message: Clone,
|
Message: Clone,
|
||||||
{
|
{
|
||||||
// --- Tree (internal state) ---
|
|
||||||
|
|
||||||
fn tag(&self) -> widget::tree::Tag {
|
|
||||||
widget::tree::Tag::of::<State>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn state(&self) -> widget::tree::State {
|
|
||||||
widget::tree::State::new(State::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Size ---
|
// --- Size ---
|
||||||
|
|
||||||
fn size(&self) -> Size<Length> {
|
fn size(&self) -> Size<Length> {
|
||||||
@@ -116,7 +103,7 @@ where
|
|||||||
|
|
||||||
fn update(
|
fn update(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: &mut widget::Tree,
|
_tree: &mut widget::Tree,
|
||||||
event: &Event,
|
event: &Event,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
@@ -125,7 +112,6 @@ where
|
|||||||
shell: &mut iced::advanced::Shell<'_, Message>,
|
shell: &mut iced::advanced::Shell<'_, Message>,
|
||||||
_viewport: &Rectangle,
|
_viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
let state = tree.state.downcast_mut::<State>();
|
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
@@ -163,7 +149,7 @@ where
|
|||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
tree: &widget::Tree,
|
_tree: &widget::Tree,
|
||||||
renderer: &mut Renderer,
|
renderer: &mut Renderer,
|
||||||
_theme: &Theme,
|
_theme: &Theme,
|
||||||
_style: &renderer::Style,
|
_style: &renderer::Style,
|
||||||
@@ -171,7 +157,6 @@ where
|
|||||||
_cursor: mouse::Cursor,
|
_cursor: mouse::Cursor,
|
||||||
_viewport: &Rectangle,
|
_viewport: &Rectangle,
|
||||||
) {
|
) {
|
||||||
let state = tree.state.downcast_ref::<State>();
|
|
||||||
let bounds = layout.bounds();
|
let bounds = layout.bounds();
|
||||||
|
|
||||||
// Pick the correct image handle.
|
// Pick the correct image handle.
|
||||||
@@ -199,7 +184,7 @@ where
|
|||||||
// ---------- Into<Element> ----------
|
// ---------- Into<Element> ----------
|
||||||
|
|
||||||
impl<'a, Message, Theme, Renderer, Handle> From<ImageButton<Handle, Message>>
|
impl<'a, Message, Theme, Renderer, Handle> From<ImageButton<Handle, Message>>
|
||||||
for Element<'a, Message, Theme, Renderer>
|
for Element<'a, Message, Theme, Renderer>
|
||||||
where
|
where
|
||||||
Renderer: img::Renderer<Handle = Handle> + 'a,
|
Renderer: img::Renderer<Handle = Handle> + 'a,
|
||||||
Handle: Clone + 'a,
|
Handle: Clone + 'a,
|
||||||
@@ -209,4 +194,4 @@ where
|
|||||||
fn from(widget: ImageButton<Handle, Message>) -> Self {
|
fn from(widget: ImageButton<Handle, Message>) -> Self {
|
||||||
Self::new(widget)
|
Self::new(widget)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
155
src/main.rs
155
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 image_button;
|
||||||
|
mod paint;
|
||||||
|
mod mouse_area;
|
||||||
|
|
||||||
#[derive(Default)]
|
pub fn main() -> iced::Result {
|
||||||
struct Paint {
|
paint::main()
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
533
src/mouse_area.rs
Normal file
533
src/mouse_area.rs
Normal file
@@ -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<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||||
|
on_release: Option<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||||
|
on_double_click: Option<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||||
|
on_right_press: Option<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||||
|
on_right_release: Option<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||||
|
on_middle_press: Option<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||||
|
on_middle_release: Option<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||||
|
on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta) -> Message + 'a>>,
|
||||||
|
on_enter: Option<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||||
|
on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||||
|
on_exit: Option<Box<dyn Fn(Point) -> Message + 'a>>,
|
||||||
|
interaction: Option<mouse::Interaction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<impl Fn(Point) -> 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<Point>,
|
||||||
|
previous_click: Option<mouse::Click>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> {
|
||||||
|
/// Creates a [`MouseArea`] with the given content.
|
||||||
|
pub fn new(
|
||||||
|
content: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> 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<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
for MouseArea<'_, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: renderer::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> tree::Tag {
|
||||||
|
tree::Tag::of::<State>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> tree::State {
|
||||||
|
tree::State::new(State::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn children(&self) -> Vec<Tree> {
|
||||||
|
vec![Tree::new(&self.content)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diff(&self, tree: &mut Tree) {
|
||||||
|
tree.diff_children(std::slice::from_ref(&self.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
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<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||||
|
self.content.as_widget_mut().overlay(
|
||||||
|
&mut tree.children[0],
|
||||||
|
layout,
|
||||||
|
renderer,
|
||||||
|
viewport,
|
||||||
|
translation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Message, Theme, Renderer> From<MouseArea<'a, Message, Theme, Renderer>>
|
||||||
|
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<Message, Theme, Renderer>(
|
||||||
|
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<Element<'a, Message, Theme, Renderer>>,
|
||||||
|
) -> MouseArea<'a, Message, Theme, Renderer>
|
||||||
|
where
|
||||||
|
Renderer: iced_core::Renderer,
|
||||||
|
{
|
||||||
|
MouseArea::new(widget)
|
||||||
|
}
|
||||||
459
src/paint.rs
Normal file
459
src/paint.rs
Normal file
@@ -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<u8>,
|
||||||
|
pixels_bak: Vec<u8>,
|
||||||
|
|
||||||
|
// 当前画笔颜色
|
||||||
|
color: Color,
|
||||||
|
|
||||||
|
// 是否正在绘制
|
||||||
|
is_drawing: bool,
|
||||||
|
|
||||||
|
begin_point: Option<Point>,
|
||||||
|
|
||||||
|
// 用于显示的图像句柄缓存
|
||||||
|
// 每次像素变化后需要重新生成
|
||||||
|
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<usize> 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<Tool> 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<Message> {
|
||||||
|
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<i32>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user