Compare commits
33 Commits
9239b03230
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2631fec53c | |||
| 6db8941b67 | |||
| d694a69723 | |||
| 51055b8ea8 | |||
| de76574654 | |||
| 09d29473bb | |||
| 849cfb247d | |||
| a4adcd42c8 | |||
| 71c4de08a6 | |||
| 8ca7c52cb1 | |||
| 1174019eff | |||
| 02c18c7387 | |||
| a261d975c1 | |||
| a9cf91ace7 | |||
| e8d72dd72f | |||
| 62be61c574 | |||
| d7f4571217 | |||
| 9337f15da1 | |||
| 25c906278c | |||
| a9243c498f | |||
| 3ed3c12f47 | |||
| ae095946f0 | |||
| 6f6e6f326b | |||
| 1f8a2859f9 | |||
| da99cfc765 | |||
| 4216a6f507 | |||
| 17cb31841f | |||
| 44652c6b15 | |||
| 95a06c6ea5 | |||
| e3153bdf59 | |||
| 4e999ed459 | |||
| b567ef1fed | |||
| 887ed90a14 |
2
.gitignore
vendored
@@ -16,3 +16,5 @@ target/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
mspaint.png
|
||||
|
||||
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
34
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
4974
Cargo.lock
generated
Normal file
10
Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "mspaint"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
iced = {version = "0.14.0", features = ["advanced", "image", "smol", "canvas"]}
|
||||
iced_core = "0.14.0"
|
||||
image = "0.25.9"
|
||||
rand = "0.10.0"
|
||||
@@ -1,3 +1,5 @@
|
||||
# mspaint
|
||||
|
||||
微软画图程序实现
|
||||
微软画图程序实现
|
||||
|
||||
实现参考 [jspaint](https://jspaint.app/)
|
||||
|
||||
BIN
image/normal/normal_01.jpg
Normal file
|
After Width: | Height: | Size: 984 B |
BIN
image/normal/normal_02.jpg
Normal file
|
After Width: | Height: | Size: 952 B |
BIN
image/normal/normal_03.jpg
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
image/normal/normal_04.jpg
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
image/normal/normal_05.jpg
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
image/normal/normal_06.jpg
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
image/normal/normal_07.jpg
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
image/normal/normal_08.jpg
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
image/normal/normal_09.jpg
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
image/normal/normal_10.jpg
Normal file
|
After Width: | Height: | Size: 957 B |
BIN
image/normal/normal_11.jpg
Normal file
|
After Width: | Height: | Size: 799 B |
BIN
image/normal/normal_12.jpg
Normal file
|
After Width: | Height: | Size: 800 B |
BIN
image/normal/normal_13.jpg
Normal file
|
After Width: | Height: | Size: 724 B |
BIN
image/normal/normal_14.jpg
Normal file
|
After Width: | Height: | Size: 938 B |
BIN
image/normal/normal_15.jpg
Normal file
|
After Width: | Height: | Size: 872 B |
BIN
image/normal/normal_16.jpg
Normal file
|
After Width: | Height: | Size: 814 B |
BIN
image/selected/selected_01.jpg
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
image/selected/selected_02.jpg
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
image/selected/selected_03.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
image/selected/selected_04.jpg
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
image/selected/selected_05.jpg
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
image/selected/selected_06.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
image/selected/selected_07.jpg
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
image/selected/selected_08.jpg
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
image/selected/selected_09.jpg
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
image/selected/selected_10.jpg
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
image/selected/selected_11.jpg
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
image/selected/selected_12.jpg
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
image/selected/selected_13.jpg
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
image/selected/selected_14.jpg
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
image/selected/selected_15.jpg
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
image/selected/selected_16.jpg
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
246
src/color_box.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
use iced::color;
|
||||
use iced::mouse;
|
||||
use iced::widget::canvas::{self, Cache, Geometry};
|
||||
use iced::{Color, Point, Rectangle, Renderer, Size, Theme};
|
||||
|
||||
use crate::mscanvas::MSColor;
|
||||
|
||||
pub struct CurrentColorBox {
|
||||
cache: Cache,
|
||||
foreground_color: MSColor,
|
||||
background_color: MSColor,
|
||||
}
|
||||
|
||||
impl CurrentColorBox {
|
||||
pub fn new(foreground_color: MSColor, background_color: MSColor) -> Self {
|
||||
Self {
|
||||
cache: Cache::default(),
|
||||
foreground_color,
|
||||
background_color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> canvas::Program<Message> for CurrentColorBox {
|
||||
type State = ();
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Self::State,
|
||||
renderer: &Renderer,
|
||||
_theme: &Theme,
|
||||
bounds: Rectangle,
|
||||
_cursor: mouse::Cursor,
|
||||
) -> Vec<Geometry> {
|
||||
let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
|
||||
// let palette = theme.palette();
|
||||
|
||||
let gray1 = color!(0x808080);
|
||||
let gray2 = color!(0xc0c0c0);
|
||||
let Size { width, height } = frame.size();
|
||||
|
||||
frame.fill_rectangle(Point::ORIGIN, Size::new(width - 1.0, 1.0), gray1);
|
||||
frame.fill_rectangle(Point::ORIGIN, Size::new(1.0, height - 1.0), gray1);
|
||||
frame.fill_rectangle(
|
||||
Point::new(width - 2.0, 1.0),
|
||||
Size::new(1.0, height - 2.0),
|
||||
gray2,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(1.0, height - 2.0),
|
||||
Size::new(width - 2.0, 1.0),
|
||||
gray2,
|
||||
);
|
||||
|
||||
frame.fill_rectangle(
|
||||
Point::new(1.0, 1.0),
|
||||
Size::new(width - 3.0, 1.0),
|
||||
Color::BLACK,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(1.0, 1.0),
|
||||
Size::new(1.0, height - 3.0),
|
||||
Color::BLACK,
|
||||
);
|
||||
|
||||
frame.fill_rectangle(
|
||||
Point::new(width - 1.0, 0.0),
|
||||
Size::new(1.0, height),
|
||||
Color::WHITE,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(0.0, height - 1.0),
|
||||
Size::new(width, 1.0),
|
||||
Color::WHITE,
|
||||
);
|
||||
|
||||
let tile_colors = [Color::WHITE, gray2];
|
||||
let mut tile_idx = 0;
|
||||
for y in 2..(height - 2.0) as i32 {
|
||||
for x in 2..(width - 2.0) as i32 {
|
||||
frame.fill_rectangle(
|
||||
Point::new(x as f32, y as f32),
|
||||
Size::new(1.0, 1.0),
|
||||
tile_colors[tile_idx],
|
||||
);
|
||||
tile_idx += 1;
|
||||
if tile_idx >= tile_colors.len() {
|
||||
tile_idx = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let box_size: f32 = 20.0;
|
||||
let fore_x: f32 = 6.0;
|
||||
let fore_y: f32 = 8.0;
|
||||
let back_x: f32 = fore_x + box_size / 2.0 + 2.0;
|
||||
let back_y: f32 = fore_y + box_size / 2.0 + 2.0;
|
||||
|
||||
// 背景颜色展示框
|
||||
frame.fill_rectangle(
|
||||
Point::new(back_x, back_y),
|
||||
Size::new(box_size, 1.0),
|
||||
Color::WHITE,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(back_x, back_y),
|
||||
Size::new(1.0, box_size),
|
||||
Color::WHITE,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(back_x + box_size - 1.0, back_y + 1.0),
|
||||
Size::new(1.0, box_size - 1.0),
|
||||
gray1,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(back_x + 1.0, back_y + box_size - 1.0),
|
||||
Size::new(box_size - 1.0, 1.0),
|
||||
gray1,
|
||||
);
|
||||
|
||||
frame.fill_rectangle(
|
||||
Point::new(back_x + 1.0, back_y + 1.0),
|
||||
Size::new(box_size - 2.0, box_size - 2.0),
|
||||
gray2,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(back_x + 2.0, back_y + 2.0),
|
||||
Size::new(box_size - 4.0, box_size - 4.0),
|
||||
self.background_color.into_color(),
|
||||
);
|
||||
|
||||
// 前景颜色展示框
|
||||
frame.fill_rectangle(
|
||||
Point::new(fore_x, fore_y),
|
||||
Size::new(box_size, 1.0),
|
||||
Color::WHITE,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(fore_x, fore_y),
|
||||
Size::new(1.0, box_size),
|
||||
Color::WHITE,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(fore_x + box_size - 1.0, fore_y + 1.0),
|
||||
Size::new(1.0, box_size - 1.0),
|
||||
gray1,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(fore_x + 1.0, fore_y + box_size - 1.0),
|
||||
Size::new(box_size - 1.0, 1.0),
|
||||
gray1,
|
||||
);
|
||||
|
||||
frame.fill_rectangle(
|
||||
Point::new(fore_x + 1.0, fore_y + 1.0),
|
||||
Size::new(box_size - 2.0, box_size - 2.0),
|
||||
gray2,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(fore_x + 2.0, fore_y + 2.0),
|
||||
Size::new(box_size - 4.0, box_size - 4.0),
|
||||
self.foreground_color.into_color(),
|
||||
);
|
||||
});
|
||||
|
||||
vec![geometry]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ColorSelectionBox {
|
||||
cache: Cache,
|
||||
color: MSColor,
|
||||
}
|
||||
|
||||
impl ColorSelectionBox {
|
||||
pub fn new(color: MSColor) -> ColorSelectionBox {
|
||||
Self {
|
||||
cache: Cache::default(),
|
||||
color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> canvas::Program<Message> for ColorSelectionBox {
|
||||
type State = ();
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &Self::State,
|
||||
renderer: &Renderer,
|
||||
_theme: &Theme,
|
||||
bounds: Rectangle,
|
||||
_cursor: mouse::Cursor,
|
||||
) -> Vec<Geometry> {
|
||||
let geometry = self.cache.draw(renderer, bounds.size(), |frame| {
|
||||
// let palette = theme.palette();
|
||||
|
||||
let gray1 = color!(0x808080);
|
||||
let gray2 = color!(0xc0c0c0);
|
||||
let Size { width, height } = frame.size();
|
||||
|
||||
frame.fill_rectangle(Point::ORIGIN, Size::new(width - 1.0, 1.0), gray1);
|
||||
frame.fill_rectangle(Point::ORIGIN, Size::new(1.0, height - 1.0), gray1);
|
||||
frame.fill_rectangle(
|
||||
Point::new(width - 2.0, 1.0),
|
||||
Size::new(1.0, height - 2.0),
|
||||
gray2,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(1.0, height - 2.0),
|
||||
Size::new(width - 2.0, 1.0),
|
||||
gray2,
|
||||
);
|
||||
|
||||
frame.fill_rectangle(
|
||||
Point::new(1.0, 1.0),
|
||||
Size::new(width - 3.0, 1.0),
|
||||
Color::BLACK,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(1.0, 1.0),
|
||||
Size::new(1.0, height - 3.0),
|
||||
Color::BLACK,
|
||||
);
|
||||
|
||||
frame.fill_rectangle(
|
||||
Point::new(width - 1.0, 0.0),
|
||||
Size::new(1.0, height),
|
||||
Color::WHITE,
|
||||
);
|
||||
frame.fill_rectangle(
|
||||
Point::new(0.0, height - 1.0),
|
||||
Size::new(width, 1.0),
|
||||
Color::WHITE,
|
||||
);
|
||||
|
||||
frame.fill_rectangle(
|
||||
Point::new(2.0, 2.0),
|
||||
Size::new(width - 4.0, height - 4.0),
|
||||
self.color.into_color(),
|
||||
);
|
||||
});
|
||||
|
||||
vec![geometry]
|
||||
}
|
||||
}
|
||||
197
src/image_button.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
9
src/main.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod color_box;
|
||||
mod image_button;
|
||||
mod mouse_area;
|
||||
mod mscanvas;
|
||||
mod paint;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
paint::main()
|
||||
}
|
||||
498
src/mouse_area.rs
Normal 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)
|
||||
}
|
||||