Add tool panel

This commit is contained in:
2026-02-24 13:52:06 +08:00
parent 887ed90a14
commit b567ef1fed
4 changed files with 1069 additions and 29 deletions

View File

@@ -0,0 +1,212 @@
use iced::advanced::layout::{self, Layout};
use iced::advanced::{renderer, Clipboard};
use iced::advanced::widget::{self, Widget};
use iced::mouse;
use iced::{Element, Event, Length, Rectangle, Size};
// We need the image renderer trait
use iced::advanced::image as img;
// ---------- State ----------
/// Internal state tracking whether the button is currently pressed.
#[derive(Default)]
pub struct State {
is_pressed: bool,
}
// ---------- Widget struct ----------
/// 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,
{
// --- 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 ---
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 state = tree.state.downcast_mut::<State>();
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 state = tree.state.downcast_ref::<State>();
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)
}
}

View File

@@ -1,41 +1,154 @@
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;
use iced::widget::{Column, button, column, text};
#[derive(Default)]
struct Counter {
value: i32,
struct Paint {
tool_pressed: [bool; Tool::Count as usize],
}
#[derive(Debug, Clone, Copy)]
pub enum Message {
Increment,
Decrement,
FreeFormSelectClicked,
SelectClicked,
EraserClicked,
FillWithColorClicked,
PickColorClicked,
MagnifierClicked,
PencilClicked,
BrushClicked,
AirbrushClicked,
TextClicked,
LineClicked,
CurveClicked,
RectangleClicked,
PolygonClicked,
EllipseClicked,
RoundedRectangleClicked,
}
impl Counter {
pub fn view(&self) -> Column<Message> {
// We use a column: a simple vertical layout
column![
// The increment button. We tell it to produce an
// `Increment` message when pressed
button("+").on_press(Message::Increment),
// We show the value of the counter here
text(self.value).size(50),
// The decrement button. We tell it to produce a
// `Decrement` message when pressed
button("-").on_press(Message::Decrement),
]
}
pub fn update(&mut self, message: Message) {
match message {
Message::Increment => {
self.value += 1;
}
Message::Decrement => {
self.value -= 1;
}
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,
}
}
}
fn main() -> iced::Result {
iced::run(Counter::update, Counter::view)
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)
}