[MIRROR] https://codeberg.org/naomi/nanel
at main 417 lines 13 kB view raw
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}