[MIRROR] https://codeberg.org/naomi/nanel
1use iced::{
2 Background, Color, Element, Event, Length, Padding, Point, Rectangle, Size, Vector,
3 event::Status,
4 overlay,
5 widget::button::{self, Catalog, Style, StyleFn},
6};
7use iced_core::{
8 Clipboard, Shell, Widget,
9 layout::{self, Layout, Limits},
10 mouse::{self, Cursor},
11 renderer::{self, Quad},
12 widget::{
13 Operation,
14 tree::{self, Tag, Tree},
15 },
16};
17
18#[derive(Debug, Clone, Copy)]
19pub struct ButtonInfo {
20 pub position: Point,
21 pub viewport: (f32, f32),
22}
23
24enum OnPress<'a, Message> {
25 Direct(Message),
26 Closure(Box<dyn Fn(ButtonInfo) -> Message + 'a>),
27}
28
29pub struct ButtonWithPosition<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer>
30where
31 Renderer: iced_core::Renderer,
32 Theme: Catalog,
33{
34 content: Element<'a, Message, Theme, Renderer>,
35 on_press: Option<OnPress<'a, Message>>,
36 width: Length,
37 height: Length,
38 padding: Padding,
39 clip: bool,
40 class: Theme::Class<'a>,
41}
42
43impl<'a, Message, Theme, Renderer> ButtonWithPosition<'a, Message, Theme, Renderer>
44where
45 Renderer: iced_core::Renderer,
46 Theme: Catalog,
47{
48 pub fn new(content: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self {
49 let content = content.into();
50 let size = content.as_widget().size_hint();
51
52 Self {
53 content,
54 on_press: None,
55 width: size.width.fluid(),
56 height: size.height.fluid(),
57 padding: DEFAULT_PADDING,
58 clip: false,
59 class: Theme::default(),
60 }
61 }
62
63 /// Sets the width of the [`ButtonWithPosition`].
64 pub fn width(mut self, width: impl Into<Length>) -> Self {
65 self.width = width.into();
66 self
67 }
68
69 /// Sets the height of the [`ButtonWithPosition`].
70 pub fn height(mut self, height: impl Into<Length>) -> Self {
71 self.height = height.into();
72 self
73 }
74
75 /// Sets the [`Padding`] of the [`ButtonWithPosition`].
76 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
77 self.padding = padding.into();
78 self
79 }
80
81 /// Sets the message that will be produced when the [`ButtonWithPosition`] is pressed.
82 ///
83 /// Unless `on_press` is called, the [`ButtonWithPosition`] will be disabled.
84 pub fn on_press(mut self, on_press: Message) -> Self {
85 self.on_press = Some(OnPress::Direct(on_press));
86 self
87 }
88
89 /// Sets the message that will be produced when the [`ButtonWithPosition`] is pressed.
90 ///
91 /// This custom widget also has the position included in the closure.
92 ///
93 /// This is analogous to [`ButtonWithPosition::on_press`], but using a closure to produce
94 /// the message.
95 ///
96 /// This closure will only be called when the [`ButtonWithPosition`] is actually pressed and,
97 /// therefore, this method is useful to reduce overhead if creating the resulting
98 /// message is slow.
99 pub fn on_press_with(mut self, on_press: impl Fn(ButtonInfo) -> Message + 'a) -> Self {
100 self.on_press = Some(OnPress::Closure(Box::new(on_press)));
101 self
102 }
103
104 pub fn on_press_with_maybe(
105 mut self,
106 on_press: Option<impl Fn(ButtonInfo) -> Message + 'a>,
107 ) -> Self {
108 self.on_press = match on_press {
109 Some(on_press) => Some(OnPress::Closure(Box::new(on_press))),
110 None => None,
111 };
112 self
113 }
114
115 /// Sets whether the contents of the [`ButtonWithPosition`] should be clipped on
116 /// overflow.
117 pub fn clip(mut self, clip: bool) -> Self {
118 self.clip = clip;
119 self
120 }
121
122 /// Sets the style of the [`ButtonWithPosition`].
123 #[must_use]
124 pub fn style(mut self, style: impl Fn(&Theme, button::Status) -> Style + 'a) -> Self
125 where
126 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
127 {
128 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
129 self
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
134struct State {
135 is_hovered: bool,
136 is_pressed: bool,
137 is_focused: bool,
138}
139
140impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
141 for ButtonWithPosition<'a, Message, Theme, Renderer>
142where
143 Message: 'a + Clone,
144 Renderer: 'a + iced_core::Renderer,
145 Theme: Catalog,
146{
147 fn tag(&self) -> Tag {
148 Tag::of::<State>()
149 }
150
151 fn state(&self) -> tree::State {
152 tree::State::new(State::default())
153 }
154
155 fn children(&self) -> Vec<Tree> {
156 vec![tree::Tree::new(&self.content)]
157 }
158
159 fn diff(&self, tree: &mut Tree) {
160 tree.diff_children(std::slice::from_ref(&self.content));
161 }
162
163 fn size(&self) -> Size<Length> {
164 Size {
165 width: self.width,
166 height: self.height,
167 }
168 }
169
170 fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> layout::Node {
171 layout::padded(limits, self.width, self.height, self.padding, |limits| {
172 self.content
173 .as_widget()
174 .layout(&mut tree.children[0], renderer, limits)
175 })
176 }
177
178 fn operate(
179 &self,
180 tree: &mut Tree,
181 layout: Layout<'_>,
182 renderer: &Renderer,
183 operation: &mut dyn Operation,
184 ) {
185 operation.container(None, layout.bounds(), &mut |operation| {
186 self.content.as_widget().operate(
187 &mut tree.children[0],
188 layout.children().next().unwrap(),
189 renderer,
190 operation,
191 );
192 });
193 }
194
195 fn on_event(
196 &mut self,
197 tree: &mut Tree,
198 event: Event,
199 layout: Layout<'_>,
200 cursor: Cursor,
201 renderer: &Renderer,
202 clipboard: &mut dyn Clipboard,
203 shell: &mut Shell<'_, Message>,
204 viewport: &Rectangle,
205 ) -> Status {
206 if let Status::Captured = self.content.as_widget_mut().on_event(
207 &mut tree.children[0],
208 event.clone(),
209 layout.children().next().unwrap(),
210 cursor,
211 renderer,
212 clipboard,
213 shell,
214 viewport,
215 ) {
216 return Status::Captured;
217 }
218
219 match event {
220 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
221 | Event::Touch(iced::touch::Event::FingerPressed { .. }) => {
222 if self.on_press.is_some() {
223 let bounds = layout.bounds();
224
225 if cursor.is_over(bounds) {
226 let state = tree.state.downcast_mut::<State>();
227 state.is_pressed = true;
228 return Status::Captured;
229 }
230 }
231 }
232 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
233 | Event::Touch(iced::touch::Event::FingerLifted { .. }) => {
234 if let Some(on_press) = self.on_press.as_ref() {
235 let state = tree.state.downcast_mut::<State>();
236
237 if state.is_pressed {
238 state.is_pressed = false;
239 let bounds = layout.bounds();
240
241 if cursor.is_over(bounds) {
242 match on_press {
243 OnPress::Direct(msg) => shell.publish(msg.clone()),
244 OnPress::Closure(on_press) => shell.publish(on_press(ButtonInfo {
245 position: Point::new(
246 layout.bounds().width / 2.0 + layout.position().x,
247 layout.bounds().height / 2.0 + layout.position().y,
248 ),
249 viewport: (viewport.width, viewport.height),
250 })),
251 }
252 }
253 }
254 return Status::Captured;
255 }
256 }
257 Event::Keyboard(iced::keyboard::Event::KeyPressed { key, .. }) => {
258 if let Some(on_press) = self.on_press.as_ref() {
259 let state = tree.state.downcast_mut::<State>();
260
261 if state.is_focused
262 && matches!(
263 key,
264 iced::keyboard::Key::Named(iced::keyboard::key::Named::Enter)
265 )
266 {
267 state.is_pressed = true;
268 match on_press {
269 OnPress::Direct(msg) => shell.publish(msg.clone()),
270 OnPress::Closure(on_press) => shell.publish(on_press(ButtonInfo {
271 position: Point::new(
272 layout.bounds().width / 2.0 + layout.position().x,
273 layout.bounds().height / 2.0 + layout.position().y,
274 ),
275 viewport: (viewport.width, viewport.height),
276 })),
277 }
278 }
279 return Status::Captured;
280 }
281 }
282 Event::Touch(iced::touch::Event::FingerLost { .. })
283 | Event::Mouse(mouse::Event::CursorLeft) => {
284 let state = tree.state.downcast_mut::<State>();
285 state.is_hovered = false;
286 state.is_pressed = false;
287 }
288 _ => {}
289 }
290
291 Status::Ignored
292 }
293
294 fn draw(
295 &self,
296 tree: &Tree,
297 renderer: &mut Renderer,
298 theme: &Theme,
299 _renderer_style: &iced_core::renderer::Style,
300 layout: iced_core::Layout<'_>,
301 cursor: Cursor,
302 viewport: &Rectangle,
303 ) {
304 let bounds = layout.bounds();
305 let content_layout = layout.children().next().unwrap();
306 let is_mouse_over = cursor.is_over(bounds);
307
308 let status = if self.on_press.is_none() {
309 button::Status::Disabled
310 } else if is_mouse_over {
311 let state = tree.state.downcast_ref::<State>();
312
313 if state.is_pressed {
314 button::Status::Pressed
315 } else {
316 button::Status::Hovered
317 }
318 } else {
319 button::Status::Active
320 };
321
322 let style = theme.style(&self.class, status);
323
324 if style.background.is_some() || style.border.width > 0.0 || style.shadow.color.a > 0.0 {
325 renderer.fill_quad(
326 Quad {
327 bounds,
328 border: style.border,
329 shadow: style.shadow,
330 },
331 style
332 .background
333 .unwrap_or(Background::Color(Color::TRANSPARENT)),
334 );
335 }
336
337 let viewport = if self.clip {
338 bounds.intersection(viewport).unwrap_or(*viewport)
339 } else {
340 *viewport
341 };
342
343 self.content.as_widget().draw(
344 &tree.children[0],
345 renderer,
346 theme,
347 &renderer::Style {
348 text_color: style.text_color,
349 },
350 content_layout,
351 cursor,
352 &viewport,
353 );
354 }
355
356 fn mouse_interaction(
357 &self,
358 _tree: &Tree,
359 layout: Layout<'_>,
360 cursor: mouse::Cursor,
361 _viewport: &Rectangle,
362 _renderer: &Renderer,
363 ) -> mouse::Interaction {
364 let is_mouse_over = cursor.is_over(layout.bounds());
365
366 if is_mouse_over && self.on_press.is_some() {
367 mouse::Interaction::Pointer
368 } else {
369 mouse::Interaction::default()
370 }
371 }
372
373 fn overlay<'b>(
374 &'b mut self,
375 tree: &'b mut Tree,
376 layout: Layout<'_>,
377 renderer: &Renderer,
378 translation: Vector,
379 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
380 self.content.as_widget_mut().overlay(
381 &mut tree.children[0],
382 layout.children().next().unwrap(),
383 renderer,
384 translation,
385 )
386 }
387}
388
389impl<'a, Message, Theme, Renderer> From<ButtonWithPosition<'a, Message, Theme, Renderer>>
390 for Element<'a, Message, Theme, Renderer>
391where
392 Message: Clone + 'a,
393 Theme: Catalog + 'a,
394 Renderer: iced_core::Renderer + 'a,
395{
396 fn from(button: ButtonWithPosition<'a, Message, Theme, Renderer>) -> Self {
397 Self::new(button)
398 }
399}
400
401/// The default [`Padding`] of a [`Button`].
402pub(crate) const DEFAULT_PADDING: Padding = Padding {
403 top: 5.0,
404 bottom: 5.0,
405 right: 10.0,
406 left: 10.0,
407};
408
409pub fn button_with_position<'a, Message, Theme, Renderer>(
410 content: impl Into<Element<'a, Message, Theme, Renderer>>,
411) -> ButtonWithPosition<'a, Message, Theme, Renderer>
412where
413 Theme: Catalog + 'a,
414 Renderer: iced_core::Renderer,
415{
416 ButtonWithPosition::new(content)
417}