Compare commits

...

6 Commits

Author SHA1 Message Date
44652c6b15 feat: Remove unused code 2026-02-25 19:26:56 +08:00
95a06c6ea5 Add image resource 2026-02-25 11:51:15 +08:00
e3153bdf59 Implement fill with color 2026-02-25 11:51:03 +08:00
4e999ed459 Implement pencil and line tool 2026-02-24 22:20:06 +08:00
b567ef1fed Add tool panel 2026-02-24 13:52:06 +08:00
887ed90a14 Basic code 2026-02-23 17:43:03 +08:00
44 changed files with 6321 additions and 1 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@@ -0,0 +1,34 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true">
<option name="myValues">
<value>
<list size="2">
<item index="0" class="java.lang.String" itemvalue="data-action" />
<item index="1" class="java.lang.String" itemvalue="type" />
</list>
</value>
</option>
<option name="myCustomValuesEnabled" value="true" />
</inspection_tool>
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="E501" />
</list>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N803" />
<option value="N802" />
<option value="N806" />
</list>
</option>
</inspection_tool>
<inspection_tool class="SqlDialectInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SqlNoDataSourceInspection" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/mspaint.iml" filepath="$PROJECT_DIR$/.idea/mspaint.iml" />
</modules>
</component>
</project>

11
.idea/mspaint.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

4834
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

8
Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "mspaint"
version = "0.1.0"
edition = "2024"
[dependencies]
iced = {version = "0.14.0", features = ["advanced", "image"]}
iced_core = "0.14.0"

View File

@@ -1,3 +1,5 @@
# mspaint # mspaint
微软画图程序实现 微软画图程序实现
实现参考 [jspaint](https://jspaint.app/)

BIN
image/normal/normal_01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

BIN
image/normal/normal_02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

BIN
image/normal/normal_03.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
image/normal/normal_04.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
image/normal/normal_05.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
image/normal/normal_06.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
image/normal/normal_07.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
image/normal/normal_08.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
image/normal/normal_09.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
image/normal/normal_10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

BIN
image/normal/normal_11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

BIN
image/normal/normal_12.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

BIN
image/normal/normal_13.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 B

BIN
image/normal/normal_14.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

BIN
image/normal/normal_15.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

BIN
image/normal/normal_16.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

197
src/image_button.rs Normal file
View File

@@ -0,0 +1,197 @@
use iced::advanced::layout::{self, Layout};
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;
// ---------- Widget struct ----------
/// A button that displays one image when idle and another when pressed.
pub struct ImageButton<Handle, Message> {
normal: Handle,
pressed: Handle,
width: Length,
height: Length,
on_press: Option<Message>,
is_pressed: bool,
}
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)
}
impl<Handle, Message> ImageButton<Handle, Message> {
/// Create a new [`ImageButton`].
///
/// * `normal` image shown in the default / hover state
/// * `pressed` image shown while the left mouse button is held
pub fn new(normal: impl Into<Handle>, pressed: impl Into<Handle>, is_pressed: bool) -> Self {
Self {
normal: normal.into(),
pressed: pressed.into(),
width: Length::Shrink,
height: Length::Shrink,
on_press: None,
is_pressed,
}
}
/// The message to emit when the button is clicked (press + release).
pub fn on_press(mut self, message: Message) -> Self {
self.on_press = Some(message);
self
}
/// Override the widget width.
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Override the widget height.
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
}
// ---------- Widget impl ----------
impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
for ImageButton<Handle, Message>
where
Renderer: img::Renderer<Handle = Handle>,
Handle: Clone,
Message: Clone,
{
// --- Size ---
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
// --- Layout ---
fn layout(
&mut self,
_tree: &mut widget::Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
// Use the natural dimensions of the normal image if the renderer
// can measure it; otherwise fall back to the limits maximum.
let size = renderer
.measure_image(&self.normal)
.map(|s| Size::new(s.width as f32, s.height as f32))
.unwrap_or_else(|| limits.max());
let size = limits.resolve(self.width, self.height, size);
layout::Node::new(size)
}
// --- Events ---
fn update(
&mut self,
_tree: &mut widget::Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut iced::advanced::Shell<'_, Message>,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
if cursor.is_over(bounds) {
shell.capture_event();
shell.request_redraw();
if let Some(on_press) = self.on_press.clone() {
shell.publish(on_press);
}
}
}
_ => {}
}
}
// --- Cursor ---
fn mouse_interaction(
&self,
_tree: &widget::Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) && self.on_press.is_some() {
mouse::Interaction::Pointer
} else {
mouse::Interaction::None
}
}
// --- Draw ---
fn draw(
&self,
_tree: &widget::Tree,
renderer: &mut Renderer,
_theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
// Pick the correct image handle.
let handle = if self.is_pressed {
&self.pressed
} else {
&self.normal
};
renderer.draw_image(
img::Image {
handle: handle.clone(),
border_radius: 0.0.into(),
filter_method: img::FilterMethod::Linear,
rotation: iced::Radians(0.0),
opacity: 1.0,
snap: false,
},
bounds, // drawing bounds
bounds, // clip bounds
);
}
}
// ---------- Into<Element> ----------
impl<'a, Message, Theme, Renderer, Handle> From<ImageButton<Handle, Message>>
for Element<'a, Message, Theme, Renderer>
where
Renderer: img::Renderer<Handle = Handle> + 'a,
Handle: Clone + 'a,
Message: Clone + 'a,
Theme: 'a,
{
fn from(widget: ImageButton<Handle, Message>) -> Self {
Self::new(widget)
}
}

7
src/main.rs Normal file
View File

@@ -0,0 +1,7 @@
mod image_button;
mod mouse_area;
mod paint;
pub fn main() -> iced::Result {
paint::main()
}

498
src/mouse_area.rs Normal file
View File

@@ -0,0 +1,498 @@
//! 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
//! 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)
}

707
src/paint.rs Normal file
View File

@@ -0,0 +1,707 @@
use iced::Theme;
use iced::padding;
use iced::widget::container;
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;
#[derive(Debug, Clone, Copy)]
pub enum Message {
MousePressed(Point),
MouseReleased(Point),
MouseMoved(Point),
Clear,
// 内部消息:请求刷新图像
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,
}
}
}
#[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<u8>,
pixels_bak: Vec<u8>,
// 当前画笔颜色
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)
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: Point::ORIGIN,
image_handle: image::Handle::from_rgba(WIDTH, HEIGHT, data),
brush_radius: 1,
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(|pos| Message::MousePressed(pos)) // 占位,实际逻辑在 on_drag 或自定义
.on_release(|pos| Message::MouseReleased(pos))
.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("CLEAR").on_press(Message::Clear),
row![tool_area, canvas_area],
]
}
pub fn update(&mut self, message: Message) -> Task<Message> {
match self.tool_selected {
Tool::FillWithColor => {
self.update_with_fill_with_color(message);
}
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::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) => {
self.fill_scanline(pos);
}
_ => {}
}
}
pub fn update_with_pencil(&mut self, message: Message) {
match message {
Message::MousePressed(pos) => {
println!("pressed: {:?}", pos);
self.is_drawing = true;
self.draw_pixel_at1(pos);
self.begin_point = pos;
}
Message::MouseReleased(pos) => {
self.is_drawing = false;
self.begin_point = pos;
}
Message::MouseMoved(pos) => {
if self.is_drawing {
self.draw_line(self.begin_point, pos);
self.begin_point = pos;
}
}
_ => {}
}
}
pub fn update_with_line(&mut self, message: Message) {
match message {
Message::MousePressed(pos) => {
self.is_drawing = true;
self.save_pixels();
self.begin_point = pos;
}
Message::MouseReleased(pos) => {
self.is_drawing = false;
self.begin_point = pos;
}
Message::MouseMoved(pos) => {
if self.is_drawing {
self.restore_pixels();
self.draw_line(self.begin_point, 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
#[allow(unused)]
impl Paint {
fn pixel_at(&self, x: i32, y: i32) -> ColorU8 {
// 边界检查
if x < 0 || x >= WIDTH as i32 || y < 0 || y >= HEIGHT as i32 {
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;
// 写入 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_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(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 as i32;
let y1 = begin.y as i32;
let x2 = end.x as i32;
let y2 = end.y 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;
let mut py = 2 * dx1 - dy1;
let mut x;
let mut y;
let xe;
let ye;
if dy1 <= dx1 {
if dx >= 0 {
x = x1;
y = y1;
xe = x2;
} else {
x = x2;
y = y2;
xe = x1;
}
self.draw_pixel_at(x, y);
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);
}
self.draw_pixel_at(x, y);
}
} else {
if dy >= 0 {
x = x1;
y = y1;
ye = y2;
} else {
x = x2;
y = y2;
ye = y1;
}
self.draw_pixel_at(x, y);
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);
}
self.draw_pixel_at(x, y);
}
}
}
fn save_pixels(&mut self) {
self.pixels_bak = self.pixels.clone();
}
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 {
iced::application(Paint::new, Paint::update, Paint::view)
.theme(Theme::CatppuccinMocha)
.run()
}