diff options
Diffstat (limited to 'native/src/widget')
-rw-r--r-- | native/src/widget/button.rs | 293 | ||||
-rw-r--r-- | native/src/widget/checkbox.rs | 4 | ||||
-rw-r--r-- | native/src/widget/container.rs | 58 | ||||
-rw-r--r-- | native/src/widget/image.rs | 74 | ||||
-rw-r--r-- | native/src/widget/pick_list.rs | 551 | ||||
-rw-r--r-- | native/src/widget/radio.rs | 2 | ||||
-rw-r--r-- | native/src/widget/scrollable.rs | 889 | ||||
-rw-r--r-- | native/src/widget/slider.rs | 384 | ||||
-rw-r--r-- | native/src/widget/text_input.rs | 901 | ||||
-rw-r--r-- | native/src/widget/toggler.rs | 4 |
10 files changed, 1789 insertions, 1371 deletions
diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index 57fdd7d4..b03d9f27 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -61,8 +61,6 @@ pub struct Button<'a, Message, Renderer> { on_press: Option<Message>, width: Length, height: Length, - min_width: u32, - min_height: u32, padding: Padding, style_sheet: Box<dyn StyleSheet + 'a>, } @@ -84,8 +82,6 @@ where on_press: None, width: Length::Shrink, height: Length::Shrink, - min_width: 0, - min_height: 0, padding: Padding::new(5), style_sheet: Default::default(), } @@ -103,18 +99,6 @@ where self } - /// Sets the minimum width of the [`Button`]. - pub fn min_width(mut self, min_width: u32) -> Self { - self.min_width = min_width; - self - } - - /// Sets the minimum height of the [`Button`]. - pub fn min_height(mut self, min_height: u32) -> Self { - self.min_height = min_height; - self - } - /// Sets the [`Padding`] of the [`Button`]. pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { self.padding = padding.into(); @@ -151,6 +135,153 @@ impl State { } } +/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] +/// accordingly. +pub fn update<'a, Message: Clone>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + on_press: &Option<Message>, + state: impl FnOnce() -> &'a mut State, +) -> event::Status { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if on_press.is_some() { + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + let state = state(); + + state.is_pressed = true; + + return event::Status::Captured; + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + if let Some(on_press) = on_press.clone() { + let state = state(); + + if state.is_pressed { + state.is_pressed = false; + + let bounds = layout.bounds(); + + if bounds.contains(cursor_position) { + shell.publish(on_press); + } + + return event::Status::Captured; + } + } + } + Event::Touch(touch::Event::FingerLost { .. }) => { + let state = state(); + + state.is_pressed = false; + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`Button`]. +pub fn draw<'a, Renderer: crate::Renderer>( + renderer: &mut Renderer, + bounds: Rectangle, + cursor_position: Point, + is_enabled: bool, + style_sheet: &dyn StyleSheet, + state: impl FnOnce() -> &'a State, +) -> Style { + let is_mouse_over = bounds.contains(cursor_position); + + let styling = if !is_enabled { + style_sheet.disabled() + } else if is_mouse_over { + let state = state(); + + if state.is_pressed { + style_sheet.pressed() + } else { + style_sheet.hovered() + } + } else { + style_sheet.active() + }; + + if styling.background.is_some() || styling.border_width > 0.0 { + if styling.shadow_offset != Vector::default() { + // TODO: Implement proper shadow support + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + styling.shadow_offset.x, + y: bounds.y + styling.shadow_offset.y, + ..bounds + }, + border_radius: styling.border_radius, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color([0.0, 0.0, 0.0, 0.5].into()), + ); + } + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: styling.border_radius, + border_width: styling.border_width, + border_color: styling.border_color, + }, + styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + } + + styling +} + +/// Computes the layout of a [`Button`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + padding: Padding, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height).pad(padding); + + let mut content = layout_content(renderer, &limits); + content.move_to(Point::new(padding.left.into(), padding.top.into())); + + let size = limits.resolve(content.size()).pad(padding); + + layout::Node::with_children(size, vec![content]) +} + +/// Returns the [`mouse::Interaction`] of a [`Button`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, + is_enabled: bool, +) -> mouse::Interaction { + let is_mouse_over = layout.bounds().contains(cursor_position); + + if is_mouse_over && is_enabled { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } +} + impl<'a, Message, Renderer> Widget<Message, Renderer> for Button<'a, Message, Renderer> where @@ -170,22 +301,14 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .min_width(self.min_width) - .min_height(self.min_height) - .width(self.width) - .height(self.height) - .pad(self.padding); - - let mut content = self.content.layout(renderer, &limits); - content.move_to(Point::new( - self.padding.left.into(), - self.padding.top.into(), - )); - - let size = limits.resolve(content.size()).pad(self.padding); - - layout::Node::with_children(size, vec![content]) + layout( + renderer, + limits, + self.width, + self.height, + self.padding, + |renderer, limits| self.content.layout(renderer, limits), + ) } fn on_event( @@ -208,42 +331,14 @@ where return event::Status::Captured; } - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if self.on_press.is_some() { - let bounds = layout.bounds(); - - if bounds.contains(cursor_position) { - self.state.is_pressed = true; - - return event::Status::Captured; - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = self.on_press.clone() { - let bounds = layout.bounds(); - - if self.state.is_pressed { - self.state.is_pressed = false; - - if bounds.contains(cursor_position) { - shell.publish(on_press); - } - - return event::Status::Captured; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - self.state.is_pressed = false; - } - _ => {} - } - - event::Status::Ignored + update( + event, + layout, + cursor_position, + shell, + &self.on_press, + || &mut self.state, + ) } fn mouse_interaction( @@ -253,14 +348,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let is_mouse_over = layout.bounds().contains(cursor_position); - let is_disabled = self.on_press.is_none(); - - if is_mouse_over && !is_disabled { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } + mouse_interaction(layout, cursor_position, self.on_press.is_some()) } fn draw( @@ -274,51 +362,14 @@ where let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); - let is_mouse_over = bounds.contains(cursor_position); - let is_disabled = self.on_press.is_none(); - - let styling = if is_disabled { - self.style_sheet.disabled() - } else if is_mouse_over { - if self.state.is_pressed { - self.style_sheet.pressed() - } else { - self.style_sheet.hovered() - } - } else { - self.style_sheet.active() - }; - - if styling.background.is_some() || styling.border_width > 0.0 { - if styling.shadow_offset != Vector::default() { - // TODO: Implement proper shadow support - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + styling.shadow_offset.x, - y: bounds.y + styling.shadow_offset.y, - ..bounds - }, - border_radius: styling.border_radius, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color([0.0, 0.0, 0.0, 0.5].into()), - ); - } - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: styling.border_radius, - border_width: styling.border_width, - border_color: styling.border_color, - }, - styling - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } + let styling = draw( + renderer, + bounds, + cursor_position, + self.on_press.is_some(), + self.style_sheet.as_ref(), + || &self.state, + ); self.content.draw( renderer, diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 15cbf93a..122c5e52 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -34,7 +34,7 @@ pub use iced_style::checkbox::{Style, StyleSheet}; #[allow(missing_debug_implementations)] pub struct Checkbox<'a, Message, Renderer: text::Renderer> { is_checked: bool, - on_toggle: Box<dyn Fn(bool) -> Message>, + on_toggle: Box<dyn Fn(bool) -> Message + 'a>, label: String, width: Length, size: u16, @@ -61,7 +61,7 @@ impl<'a, Message, Renderer: text::Renderer> Checkbox<'a, Message, Renderer> { /// `Message`. pub fn new<F>(is_checked: bool, label: impl Into<String>, f: F) -> Self where - F: 'static + Fn(bool) -> Message, + F: 'a + Fn(bool) -> Message, { Checkbox { is_checked, diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index ca85a425..0e7c301e 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -116,6 +116,32 @@ where } } +/// Computes the layout of a [`Container`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + padding: Padding, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.loose().width(width).height(height).pad(padding); + + let mut content = layout_content(renderer, &limits.loose()); + let size = limits.resolve(content.size()); + + content.move_to(Point::new(padding.left.into(), padding.top.into())); + content.align( + Alignment::from(horizontal_alignment), + Alignment::from(vertical_alignment), + size, + ); + + layout::Node::with_children(size.pad(padding), vec![content]) +} + impl<'a, Message, Renderer> Widget<Message, Renderer> for Container<'a, Message, Renderer> where @@ -134,28 +160,16 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let limits = limits - .loose() - .max_width(self.max_width) - .max_height(self.max_height) - .width(self.width) - .height(self.height) - .pad(self.padding); - - let mut content = self.content.layout(renderer, &limits.loose()); - let size = limits.resolve(content.size()); - - content.move_to(Point::new( - self.padding.left.into(), - self.padding.top.into(), - )); - content.align( - Alignment::from(self.horizontal_alignment), - Alignment::from(self.vertical_alignment), - size, - ); - - layout::Node::with_children(size.pad(self.padding), vec![content]) + layout( + renderer, + limits, + self.width, + self.height, + self.padding, + self.horizontal_alignment, + self.vertical_alignment, + |renderer, limits| self.content.layout(renderer, limits), + ) } fn on_event( diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index de0ffbc0..8e7a28e5 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -65,6 +65,46 @@ impl<Handle> Image<Handle> { } } +/// Computes the layout of an [`Image`]. +pub fn layout<Renderer, Handle>( + renderer: &Renderer, + limits: &layout::Limits, + handle: &Handle, + width: Length, + height: Length, + content_fit: ContentFit, +) -> layout::Node +where + Renderer: image::Renderer<Handle = Handle>, +{ + // The raw w/h of the underlying image + let image_size = { + let (width, height) = renderer.dimensions(handle); + + Size::new(width as f32, height as f32) + }; + + // The size to be available to the widget prior to `Shrink`ing + let raw_size = limits.width(width).height(height).resolve(image_size); + + // The uncropped size of the image when fit to the bounds above + let full_size = content_fit.fit(image_size, raw_size); + + // Shrink the widget to fit the resized image, if requested + let final_size = Size { + width: match width { + Length::Shrink => f32::min(raw_size.width, full_size.width), + _ => raw_size.width, + }, + height: match height { + Length::Shrink => f32::min(raw_size.height, full_size.height), + _ => raw_size.height, + }, + }; + + layout::Node::new(final_size) +} + impl<Message, Renderer, Handle> Widget<Message, Renderer> for Image<Handle> where Renderer: image::Renderer<Handle = Handle>, @@ -83,32 +123,14 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - // The raw w/h of the underlying image - let (width, height) = renderer.dimensions(&self.handle); - let image_size = Size::new(width as f32, height as f32); - - // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits - .width(self.width) - .height(self.height) - .resolve(image_size); - - // The uncropped size of the image when fit to the bounds above - let full_size = self.content_fit.fit(image_size, raw_size); - - // Shrink the widget to fit the resized image, if requested - let final_size = Size { - width: match self.width { - Length::Shrink => f32::min(raw_size.width, full_size.width), - _ => raw_size.width, - }, - height: match self.height { - Length::Shrink => f32::min(raw_size.height, full_size.height), - _ => raw_size.height, - }, - }; - - layout::Node::new(final_size) + layout( + renderer, + limits, + &self.handle, + self.width, + self.height, + self.content_fit, + ) } fn draw( diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 3be6c20c..e050ada5 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -23,11 +23,7 @@ pub struct PickList<'a, T, Message, Renderer: text::Renderer> where [T]: ToOwned<Owned = Vec<T>>, { - menu: &'a mut menu::State, - keyboard_modifiers: &'a mut keyboard::Modifiers, - is_open: &'a mut bool, - hovered_option: &'a mut Option<usize>, - last_selection: &'a mut Option<T>, + state: &'a mut State<T>, on_selected: Box<dyn Fn(T) -> Message>, options: Cow<'a, [T]>, placeholder: Option<String>, @@ -49,8 +45,9 @@ pub struct State<T> { last_selection: Option<T>, } -impl<T> Default for State<T> { - fn default() -> Self { +impl<T> State<T> { + /// Creates a new [`State`] for a [`PickList`]. + pub fn new() -> Self { Self { menu: menu::State::default(), keyboard_modifiers: keyboard::Modifiers::default(), @@ -61,6 +58,12 @@ impl<T> Default for State<T> { } } +impl<T> Default for State<T> { + fn default() -> Self { + Self::new() + } +} + impl<'a, T: 'a, Message, Renderer: text::Renderer> PickList<'a, T, Message, Renderer> where @@ -79,20 +82,8 @@ where selected: Option<T>, on_selected: impl Fn(T) -> Message + 'static, ) -> Self { - let State { - menu, - keyboard_modifiers, - is_open, - hovered_option, - last_selection, - } = state; - Self { - menu, - keyboard_modifiers, - is_open, - hovered_option, - last_selection, + state, on_selected: Box::new(on_selected), options: options.into(), placeholder: None, @@ -145,128 +136,118 @@ where } } -impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> - for PickList<'a, T, Message, Renderer> +/// Computes the layout of a [`PickList`]. +pub fn layout<Renderer, T>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + text_size: Option<u16>, + font: &Renderer::Font, + placeholder: Option<&str>, + options: &[T], +) -> layout::Node where - T: Clone + ToString + Eq, - [T]: ToOwned<Owned = Vec<T>>, - Message: 'static, - Renderer: text::Renderer + 'a, + Renderer: text::Renderer, + T: ToString, { - fn width(&self) -> Length { - self.width - } + use std::f32; - fn height(&self) -> Length { - Length::Shrink - } + let limits = limits.width(width).height(Length::Shrink).pad(padding); - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - use std::f32; - - let limits = limits - .width(self.width) - .height(Length::Shrink) - .pad(self.padding); - - let text_size = self.text_size.unwrap_or(renderer.default_size()); - let font = self.font.clone(); - - let max_width = match self.width { - Length::Shrink => { - let measure = |label: &str| -> u32 { - let (width, _) = renderer.measure( - label, - text_size, - font.clone(), - Size::new(f32::INFINITY, f32::INFINITY), - ); - - width.round() as u32 - }; + let text_size = text_size.unwrap_or(renderer.default_size()); - let labels = self.options.iter().map(ToString::to_string); + let max_width = match width { + Length::Shrink => { + let measure = |label: &str| -> u32 { + let (width, _) = renderer.measure( + label, + text_size, + font.clone(), + Size::new(f32::INFINITY, f32::INFINITY), + ); - let labels_width = - labels.map(|label| measure(&label)).max().unwrap_or(100); + width.round() as u32 + }; - let placeholder_width = self - .placeholder - .as_ref() - .map(String::as_str) - .map(measure) - .unwrap_or(100); + let labels = options.iter().map(ToString::to_string); - labels_width.max(placeholder_width) - } - _ => 0, - }; + let labels_width = + labels.map(|label| measure(&label)).max().unwrap_or(100); - let size = { - let intrinsic = Size::new( - max_width as f32 - + f32::from(text_size) - + f32::from(self.padding.left), - f32::from(text_size), - ); + let placeholder_width = placeholder.map(measure).unwrap_or(100); - limits.resolve(intrinsic).pad(self.padding) - }; + labels_width.max(placeholder_width) + } + _ => 0, + }; - layout::Node::new(size) - } + let size = { + let intrinsic = Size::new( + max_width as f32 + f32::from(text_size) + f32::from(padding.left), + f32::from(text_size), + ); - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let event_status = if *self.is_open { - // TODO: Encode cursor availability in the type system - *self.is_open = - cursor_position.x < 0.0 || cursor_position.y < 0.0; - - event::Status::Captured - } else if layout.bounds().contains(cursor_position) { - let selected = self.selected.as_ref(); - - *self.is_open = true; - *self.hovered_option = self - .options - .iter() - .position(|option| Some(option) == selected); - - event::Status::Captured - } else { - event::Status::Ignored - }; + limits.resolve(intrinsic).pad(padding) + }; + + layout::Node::new(size) +} - if let Some(last_selection) = self.last_selection.take() { - shell.publish((self.on_selected)(last_selection)); +/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] +/// accordingly. +pub fn update<'a, T, Message>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + on_selected: &dyn Fn(T) -> Message, + selected: Option<&T>, + options: &[T], + state: impl FnOnce() -> &'a mut State<T>, +) -> event::Status +where + T: PartialEq + Clone + 'a, +{ + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state(); - *self.is_open = false; + let event_status = if state.is_open { + // TODO: Encode cursor availability in the type system + state.is_open = + cursor_position.x < 0.0 || cursor_position.y < 0.0; - event::Status::Captured - } else { - event_status - } + event::Status::Captured + } else if layout.bounds().contains(cursor_position) { + state.is_open = true; + state.hovered_option = + options.iter().position(|option| Some(option) == selected); + + event::Status::Captured + } else { + event::Status::Ignored + }; + + if let Some(last_selection) = state.last_selection.take() { + shell.publish((on_selected)(last_selection)); + + state.is_open = false; + + event::Status::Captured + } else { + event_status } - Event::Mouse(mouse::Event::WheelScrolled { - delta: mouse::ScrollDelta::Lines { y, .. }, - }) if self.keyboard_modifiers.command() + } + Event::Mouse(mouse::Event::WheelScrolled { + delta: mouse::ScrollDelta::Lines { y, .. }, + }) => { + let state = state(); + + if state.keyboard_modifiers.command() && layout.bounds().contains(cursor_position) - && !*self.is_open => + && !state.is_open { fn find_next<'a, T: PartialEq>( selected: &'a T, @@ -278,34 +259,219 @@ where } let next_option = if y < 0.0 { - if let Some(selected) = self.selected.as_ref() { - find_next(selected, self.options.iter()) + if let Some(selected) = selected { + find_next(selected, options.iter()) } else { - self.options.first() + options.first() } } else if y > 0.0 { - if let Some(selected) = self.selected.as_ref() { - find_next(selected, self.options.iter().rev()) + if let Some(selected) = selected { + find_next(selected, options.iter().rev()) } else { - self.options.last() + options.last() } } else { None }; if let Some(next_option) = next_option { - shell.publish((self.on_selected)(next_option.clone())); + shell.publish((on_selected)(next_option.clone())); } event::Status::Captured - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - *self.keyboard_modifiers = modifiers; - + } else { event::Status::Ignored } - _ => event::Status::Ignored, } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); + + state.keyboard_modifiers = modifiers; + + event::Status::Ignored + } + _ => event::Status::Ignored, + } +} + +/// Returns the current [`mouse::Interaction`] of a [`PickList`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if is_mouse_over { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } +} + +/// Returns the current overlay of a [`PickList`]. +pub fn overlay<'a, T, Message, Renderer>( + layout: Layout<'_>, + state: &'a mut State<T>, + padding: Padding, + text_size: Option<u16>, + font: Renderer::Font, + options: &'a [T], + style_sheet: &dyn StyleSheet, +) -> Option<overlay::Element<'a, Message, Renderer>> +where + Message: 'a, + Renderer: text::Renderer + 'a, + T: Clone + ToString, +{ + if state.is_open { + let bounds = layout.bounds(); + + let mut menu = Menu::new( + &mut state.menu, + options, + &mut state.hovered_option, + &mut state.last_selection, + ) + .width(bounds.width.round() as u16) + .padding(padding) + .font(font) + .style(style_sheet.menu()); + + if let Some(text_size) = text_size { + menu = menu.text_size(text_size); + } + + Some(menu.overlay(layout.position(), bounds.height)) + } else { + None + } +} + +/// Draws a [`PickList`]. +pub fn draw<T, Renderer>( + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + padding: Padding, + text_size: Option<u16>, + font: &Renderer::Font, + placeholder: Option<&str>, + selected: Option<&T>, + style_sheet: &dyn StyleSheet, +) where + Renderer: text::Renderer, + T: ToString, +{ + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + let is_selected = selected.is_some(); + + let style = if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_color: style.border_color, + border_width: style.border_width, + border_radius: style.border_radius, + }, + style.background, + ); + + renderer.fill_text(Text { + content: &Renderer::ARROW_DOWN_ICON.to_string(), + font: Renderer::ICON_FONT, + size: bounds.height * style.icon_size, + bounds: Rectangle { + x: bounds.x + bounds.width - f32::from(padding.horizontal()), + y: bounds.center_y(), + ..bounds + }, + color: style.text_color, + horizontal_alignment: alignment::Horizontal::Right, + vertical_alignment: alignment::Vertical::Center, + }); + + let label = selected.map(ToString::to_string); + + if let Some(label) = + label.as_ref().map(String::as_str).or_else(|| placeholder) + { + renderer.fill_text(Text { + content: label, + size: f32::from(text_size.unwrap_or(renderer.default_size())), + font: font.clone(), + color: is_selected + .then(|| style.text_color) + .unwrap_or(style.placeholder_color), + bounds: Rectangle { + x: bounds.x + f32::from(padding.left), + y: bounds.center_y(), + ..bounds + }, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + }) + } +} + +impl<'a, T: 'a, Message, Renderer> Widget<Message, Renderer> + for PickList<'a, T, Message, Renderer> +where + T: Clone + ToString + Eq, + [T]: ToOwned<Owned = Vec<T>>, + Message: 'static, + Renderer: text::Renderer + 'a, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + self.width, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + &self.options, + ) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + shell, + self.on_selected.as_ref(), + self.selected.as_ref(), + &self.options, + || &mut self.state, + ) } fn mouse_interaction( @@ -315,14 +481,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if is_mouse_over { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } + mouse_interaction(layout, cursor_position) } fn draw( @@ -333,66 +492,17 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - let is_selected = self.selected.is_some(); - - let style = if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_color: style.border_color, - border_width: style.border_width, - border_radius: style.border_radius, - }, - style.background, - ); - - renderer.fill_text(Text { - content: &Renderer::ARROW_DOWN_ICON.to_string(), - font: Renderer::ICON_FONT, - size: bounds.height * style.icon_size, - bounds: Rectangle { - x: bounds.x + bounds.width - - f32::from(self.padding.horizontal()), - y: bounds.center_y(), - ..bounds - }, - color: style.text_color, - horizontal_alignment: alignment::Horizontal::Right, - vertical_alignment: alignment::Vertical::Center, - }); - - if let Some(label) = self - .selected - .as_ref() - .map(ToString::to_string) - .as_ref() - .or_else(|| self.placeholder.as_ref()) - { - renderer.fill_text(Text { - content: label, - size: f32::from( - self.text_size.unwrap_or(renderer.default_size()), - ), - font: self.font.clone(), - color: is_selected - .then(|| style.text_color) - .unwrap_or(style.placeholder_color), - bounds: Rectangle { - x: bounds.x + f32::from(self.padding.left), - y: bounds.center_y(), - ..bounds - }, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }) - } + draw( + renderer, + layout, + cursor_position, + self.padding, + self.text_size, + &self.font, + self.placeholder.as_ref().map(String::as_str), + self.selected.as_ref(), + self.style_sheet.as_ref(), + ) } fn overlay( @@ -400,28 +510,15 @@ where layout: Layout<'_>, _renderer: &Renderer, ) -> Option<overlay::Element<'_, Message, Renderer>> { - if *self.is_open { - let bounds = layout.bounds(); - - let mut menu = Menu::new( - &mut self.menu, - &self.options, - &mut self.hovered_option, - &mut self.last_selection, - ) - .width(bounds.width.round() as u16) - .padding(self.padding) - .font(self.font.clone()) - .style(self.style_sheet.menu()); - - if let Some(text_size) = self.text_size { - menu = menu.text_size(text_size); - } - - Some(menu.overlay(layout.position(), bounds.height)) - } else { - None - } + overlay( + layout, + &mut self.state, + self.padding, + self.text_size, + self.font.clone(), + &self.options, + self.style_sheet.as_ref(), + ) } } diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index fed2925b..657ae786 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -79,7 +79,7 @@ where ) -> Self where V: Eq + Copy, - F: 'static + Fn(V) -> Message, + F: FnOnce(V) -> Message, { Radio { is_selected: Some(value) == selected, diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index ce734ad8..3752fd71 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -139,235 +139,201 @@ impl<'a, Message, Renderer: crate::Renderer> Scrollable<'a, Message, Renderer> { self.content = self.content.push(child); self } - - fn notify_on_scroll( - &self, - bounds: Rectangle, - content_bounds: Rectangle, - shell: &mut Shell<'_, Message>, - ) { - if content_bounds.height <= bounds.height { - return; - } - - if let Some(on_scroll) = &self.on_scroll { - shell.publish(on_scroll( - self.state.offset.absolute(bounds, content_bounds) - / (content_bounds.height - bounds.height), - )); - } - } - - fn scrollbar( - &self, - bounds: Rectangle, - content_bounds: Rectangle, - ) -> Option<Scrollbar> { - let offset = self.state.offset(bounds, content_bounds); - - if content_bounds.height > bounds.height { - let outer_width = self.scrollbar_width.max(self.scroller_width) - + 2 * self.scrollbar_margin; - - let outer_bounds = Rectangle { - x: bounds.x + bounds.width - outer_width as f32, - y: bounds.y, - width: outer_width as f32, - height: bounds.height, - }; - - let scrollbar_bounds = Rectangle { - x: bounds.x + bounds.width - - f32::from(outer_width / 2 + self.scrollbar_width / 2), - y: bounds.y, - width: self.scrollbar_width as f32, - height: bounds.height, - }; - - let ratio = bounds.height / content_bounds.height; - let scroller_height = bounds.height * ratio; - let y_offset = offset as f32 * ratio; - - let scroller_bounds = Rectangle { - x: bounds.x + bounds.width - - f32::from(outer_width / 2 + self.scroller_width / 2), - y: scrollbar_bounds.y + y_offset, - width: self.scroller_width as f32, - height: scroller_height, - }; - - Some(Scrollbar { - outer_bounds, - bounds: scrollbar_bounds, - scroller: Scroller { - bounds: scroller_bounds, - }, - }) - } else { - None - } - } } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for Scrollable<'a, Message, Renderer> -where - Renderer: crate::Renderer, -{ - fn width(&self) -> Length { - Widget::<Message, Renderer>::width(&self.content) - } - - fn height(&self) -> Length { - self.height - } +/// Computes the layout of a [`Scrollable`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { + let limits = limits.width(width).height(height); - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let limits = limits - .max_height(self.max_height) - .width(Widget::<Message, Renderer>::width(&self.content)) - .height(self.height); - - let child_limits = layout::Limits::new( - Size::new(limits.min().width, 0.0), - Size::new(limits.max().width, f32::INFINITY), - ); + let child_limits = layout::Limits::new( + Size::new(limits.min().width, 0.0), + Size::new(limits.max().width, f32::INFINITY), + ); - let content = self.content.layout(renderer, &child_limits); - let size = limits.resolve(content.size()); + let content = layout_content(renderer, &child_limits); + let size = limits.resolve(content.size()); - layout::Node::with_children(size, vec![content]) - } + layout::Node::with_children(size, vec![content]) +} - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - let content = layout.children().next().unwrap(); - let content_bounds = content.bounds(); - - let scrollbar = self.scrollbar(bounds, content_bounds); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); - - let event_status = { - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new( - cursor_position.x, - cursor_position.y - + self.state.offset(bounds, content_bounds) as f32, - ) - } else { - // TODO: Make `cursor_position` an `Option<Point>` so we can encode - // cursor availability. - // This will probably happen naturally once we add multi-window - // support. - Point::new(cursor_position.x, -1.0) - }; - - self.content.on_event( - event.clone(), - content, - cursor_position, - renderer, - clipboard, - shell, +/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] +/// accordingly. +pub fn update<Message>( + state: &mut State, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + on_scroll: &Option<Box<dyn Fn(f32) -> Message>>, + update_content: impl FnOnce( + Event, + Layout<'_>, + Point, + &mut dyn Clipboard, + &mut Shell<'_, Message>, + ) -> event::Status, +) -> event::Status { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + let content = layout.children().next().unwrap(); + let content_bounds = content.bounds(); + + let scrollbar = scrollbar( + state, + scrollbar_width, + scrollbar_margin, + scroller_width, + bounds, + content_bounds, + ); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let event_status = { + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new( + cursor_position.x, + cursor_position.y + state.offset(bounds, content_bounds) as f32, ) + } else { + // TODO: Make `cursor_position` an `Option<Point>` so we can encode + // cursor availability. + // This will probably happen naturally once we add multi-window + // support. + Point::new(cursor_position.x, -1.0) }; - if let event::Status::Captured = event_status { - return event::Status::Captured; - } + update_content( + event.clone(), + content, + cursor_position, + clipboard, + shell, + ) + }; + + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + + if is_mouse_over { + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + match delta { + mouse::ScrollDelta::Lines { y, .. } => { + // TODO: Configurable speed (?) + state.scroll(y * 60.0, bounds, content_bounds); + } + mouse::ScrollDelta::Pixels { y, .. } => { + state.scroll(y, bounds, content_bounds); + } + } - if is_mouse_over { - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - match delta { - mouse::ScrollDelta::Lines { y, .. } => { - // TODO: Configurable speed (?) - self.state.scroll(y * 60.0, bounds, content_bounds); - } - mouse::ScrollDelta::Pixels { y, .. } => { - self.state.scroll(y, bounds, content_bounds); - } + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); + + return event::Status::Captured; + } + Event::Touch(event) => { + match event { + touch::Event::FingerPressed { .. } => { + state.scroll_box_touched_at = Some(cursor_position); } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_box_touched_at + { + let delta = + cursor_position.y - scroll_box_touched_at.y; - self.notify_on_scroll(bounds, content_bounds, shell); + state.scroll(delta, bounds, content_bounds); - return event::Status::Captured; - } - Event::Touch(event) => { - match event { - touch::Event::FingerPressed { .. } => { - self.state.scroll_box_touched_at = - Some(cursor_position); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - self.state.scroll_box_touched_at - { - let delta = - cursor_position.y - scroll_box_touched_at.y; - - self.state.scroll( - delta, - bounds, - content_bounds, - ); - - self.state.scroll_box_touched_at = - Some(cursor_position); - - self.notify_on_scroll( - bounds, - content_bounds, - shell, - ); - } - } - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } => { - self.state.scroll_box_touched_at = None; + state.scroll_box_touched_at = Some(cursor_position); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); } } - - return event::Status::Captured; + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + state.scroll_box_touched_at = None; + } } - _ => {} + + return event::Status::Captured; } + _ => {} } + } + + if state.is_scroller_grabbed() { + match event { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state.scroller_grabbed_at = None; - if self.state.is_scroller_grabbed() { - match event { - Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - self.state.scroller_grabbed_at = None; + return event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let (Some(scrollbar), Some(scroller_grabbed_at)) = + (scrollbar, state.scroller_grabbed_at) + { + state.scroll_to( + scrollbar.scroll_percentage( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); return event::Status::Captured; } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let (Some(scrollbar), Some(scroller_grabbed_at)) = - (scrollbar, self.state.scroller_grabbed_at) + } + _ => {} + } + } else if is_mouse_over_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(scrollbar) = scrollbar { + if let Some(scroller_grabbed_at) = + scrollbar.grab_scroller(cursor_position) { - self.state.scroll_to( + state.scroll_to( scrollbar.scroll_percentage( scroller_grabbed_at, cursor_position, @@ -376,50 +342,329 @@ where content_bounds, ); - self.notify_on_scroll(bounds, content_bounds, shell); + state.scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + on_scroll, + bounds, + content_bounds, + shell, + ); return event::Status::Captured; } } - _ => {} } - } else if is_mouse_over_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(scrollbar) = scrollbar { - if let Some(scroller_grabbed_at) = - scrollbar.grab_scroller(cursor_position) - { - self.state.scroll_to( - scrollbar.scroll_percentage( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); + _ => {} + } + } - self.state.scroller_grabbed_at = - Some(scroller_grabbed_at); + event::Status::Ignored +} - self.notify_on_scroll( - bounds, - content_bounds, - shell, - ); +/// Computes the current [`mouse::Interaction`] of a [`Scrollable`]. +pub fn mouse_interaction( + state: &State, + layout: Layout<'_>, + cursor_position: Point, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + content_interaction: impl FnOnce( + Layout<'_>, + Point, + &Rectangle, + ) -> mouse::Interaction, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let scrollbar = scrollbar( + state, + scrollbar_width, + scrollbar_margin, + scroller_width, + bounds, + content_bounds, + ); + + let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + if is_mouse_over_scrollbar || state.is_scroller_grabbed() { + mouse::Interaction::Idle + } else { + let offset = state.offset(bounds, content_bounds); - return event::Status::Captured; - } - } + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new(cursor_position.x, cursor_position.y + offset as f32) + } else { + Point::new(cursor_position.x, -1.0) + }; + + content_interaction( + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, + ) + } +} + +/// Draws a [`Scrollable`]. +pub fn draw<Renderer>( + state: &State, + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + style_sheet: &dyn StyleSheet, + draw_content: impl FnOnce(&mut Renderer, Layout<'_>, Point, &Rectangle), +) where + Renderer: crate::Renderer, +{ + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + let offset = state.offset(bounds, content_bounds); + let scrollbar = scrollbar( + state, + scrollbar_width, + scrollbar_margin, + scroller_width, + bounds, + content_bounds, + ); + + let is_mouse_over = bounds.contains(cursor_position); + let is_mouse_over_scrollbar = scrollbar + .as_ref() + .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) + .unwrap_or(false); + + let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { + Point::new(cursor_position.x, cursor_position.y + offset as f32) + } else { + Point::new(cursor_position.x, -1.0) + }; + + if let Some(scrollbar) = scrollbar { + renderer.with_layer(bounds, |renderer| { + renderer.with_translation( + Vector::new(0.0, -(offset as f32)), + |renderer| { + draw_content( + renderer, + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, + ); + }, + ); + }); + + let style = if state.is_scroller_grabbed() { + style_sheet.dragging() + } else if is_mouse_over_scrollbar { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let is_scrollbar_visible = + style.background.is_some() || style.border_width > 0.0; + + renderer.with_layer( + Rectangle { + width: bounds.width + 2.0, + height: bounds.height + 2.0, + ..bounds + }, + |renderer| { + if is_scrollbar_visible { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.bounds, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }, + style + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); } - _ => {} - } - } - event::Status::Ignored + if is_mouse_over + || state.is_scroller_grabbed() + || is_scrollbar_visible + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.scroller.bounds, + border_radius: style.scroller.border_radius, + border_width: style.scroller.border_width, + border_color: style.scroller.border_color, + }, + style.scroller.color, + ); + } + }, + ); + } else { + draw_content( + renderer, + content_layout, + cursor_position, + &Rectangle { + y: bounds.y + offset as f32, + ..bounds + }, + ); + } +} + +fn scrollbar( + state: &State, + scrollbar_width: u16, + scrollbar_margin: u16, + scroller_width: u16, + bounds: Rectangle, + content_bounds: Rectangle, +) -> Option<Scrollbar> { + let offset = state.offset(bounds, content_bounds); + + if content_bounds.height > bounds.height { + let outer_width = + scrollbar_width.max(scroller_width) + 2 * scrollbar_margin; + + let outer_bounds = Rectangle { + x: bounds.x + bounds.width - outer_width as f32, + y: bounds.y, + width: outer_width as f32, + height: bounds.height, + }; + + let scrollbar_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(outer_width / 2 + scrollbar_width / 2), + y: bounds.y, + width: scrollbar_width as f32, + height: bounds.height, + }; + + let ratio = bounds.height / content_bounds.height; + let scroller_height = bounds.height * ratio; + let y_offset = offset as f32 * ratio; + + let scroller_bounds = Rectangle { + x: bounds.x + bounds.width + - f32::from(outer_width / 2 + scroller_width / 2), + y: scrollbar_bounds.y + y_offset, + width: scroller_width as f32, + height: scroller_height, + }; + + Some(Scrollbar { + outer_bounds, + bounds: scrollbar_bounds, + scroller: Scroller { + bounds: scroller_bounds, + }, + }) + } else { + None + } +} + +fn notify_on_scroll<Message>( + state: &State, + on_scroll: &Option<Box<dyn Fn(f32) -> Message>>, + bounds: Rectangle, + content_bounds: Rectangle, + shell: &mut Shell<'_, Message>, +) { + if content_bounds.height <= bounds.height { + return; + } + + if let Some(on_scroll) = on_scroll { + shell.publish(on_scroll( + state.offset.absolute(bounds, content_bounds) + / (content_bounds.height - bounds.height), + )); + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for Scrollable<'a, Message, Renderer> +where + Renderer: crate::Renderer, +{ + fn width(&self) -> Length { + Widget::<Message, Renderer>::width(&self.content) + } + + fn height(&self) -> Length { + self.height + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + renderer, + limits, + Widget::<Message, Renderer>::width(self), + self.height, + |renderer, limits| self.content.layout(renderer, limits), + ) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + &mut self.state, + event, + layout, + cursor_position, + clipboard, + shell, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + &self.on_scroll, + |event, layout, cursor_position, clipboard, shell| { + self.content.on_event( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + }, + ) } fn mouse_interaction( @@ -429,38 +674,22 @@ where _viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - let scrollbar = self.scrollbar(bounds, content_bounds); - - let is_mouse_over = bounds.contains(cursor_position); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); - - if is_mouse_over_scrollbar || self.state.is_scroller_grabbed() { - mouse::Interaction::Idle - } else { - let offset = self.state.offset(bounds, content_bounds); - - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new(cursor_position.x, cursor_position.y + offset as f32) - } else { - Point::new(cursor_position.x, -1.0) - }; - - self.content.mouse_interaction( - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset as f32, - ..bounds - }, - renderer, - ) - } + mouse_interaction( + &self.state, + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + |layout, cursor_position, viewport| { + self.content.mouse_interaction( + layout, + cursor_position, + viewport, + renderer, + ) + }, + ) } fn draw( @@ -471,103 +700,25 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - let offset = self.state.offset(bounds, content_bounds); - let scrollbar = self.scrollbar(bounds, content_bounds); - - let is_mouse_over = bounds.contains(cursor_position); - let is_mouse_over_scrollbar = scrollbar - .as_ref() - .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) - .unwrap_or(false); - - let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar { - Point::new(cursor_position.x, cursor_position.y + offset as f32) - } else { - Point::new(cursor_position.x, -1.0) - }; - - if let Some(scrollbar) = scrollbar { - renderer.with_layer(bounds, |renderer| { - renderer.with_translation( - Vector::new(0.0, -(offset as f32)), - |renderer| { - self.content.draw( - renderer, - style, - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset as f32, - ..bounds - }, - ); - }, - ); - }); - - let style = if self.state.is_scroller_grabbed() { - self.style_sheet.dragging() - } else if is_mouse_over_scrollbar { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - let is_scrollbar_visible = - style.background.is_some() || style.border_width > 0.0; - - renderer.with_layer( - Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, - ..bounds - }, - |renderer| { - if is_scrollbar_visible { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.bounds, - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, - }, - style.background.unwrap_or(Background::Color( - Color::TRANSPARENT, - )), - ); - } - - if is_mouse_over - || self.state.is_scroller_grabbed() - || is_scrollbar_visible - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border_radius: style.scroller.border_radius, - border_width: style.scroller.border_width, - border_color: style.scroller.border_color, - }, - style.scroller.color, - ); - } - }, - ); - } else { - self.content.draw( - renderer, - style, - content_layout, - cursor_position, - &Rectangle { - y: bounds.y + offset as f32, - ..bounds - }, - ); - } + draw( + &self.state, + renderer, + layout, + cursor_position, + self.scrollbar_width, + self.scrollbar_margin, + self.scroller_width, + self.style_sheet.as_ref(), + |renderer, layout, cursor_position, viewport| { + self.content.draw( + renderer, + style, + layout, + cursor_position, + viewport, + ) + }, + ) } fn overlay( diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 289f75f5..4c56083e 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -142,6 +142,207 @@ where } } +/// Processes an [`Event`] and updates the [`State`] of a [`Slider`] +/// accordingly. +pub fn update<Message, T>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + shell: &mut Shell<'_, Message>, + state: &mut State, + value: &mut T, + range: &RangeInclusive<T>, + step: T, + on_change: &dyn Fn(T) -> Message, + on_release: &Option<Message>, +) -> event::Status +where + T: Copy + Into<f64> + num_traits::FromPrimitive, + Message: Clone, +{ + let is_dragging = state.is_dragging; + + let mut change = || { + let bounds = layout.bounds(); + let new_value = if cursor_position.x <= bounds.x { + *range.start() + } else if cursor_position.x >= bounds.x + bounds.width { + *range.end() + } else { + let step = step.into(); + let start = (*range.start()).into(); + let end = (*range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + if let Some(value) = T::from_f64(value) { + value + } else { + return; + } + }; + + if ((*value).into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((on_change)(new_value)); + + *value = new_value; + } + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if layout.bounds().contains(cursor_position) { + change(); + state.is_dragging = true; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if is_dragging { + if let Some(on_release) = on_release.clone() { + shell.publish(on_release); + } + state.is_dragging = false; + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if is_dragging { + change(); + + return event::Status::Captured; + } + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws a [`Slider`]. +pub fn draw<T>( + renderer: &mut impl crate::Renderer, + layout: Layout<'_>, + cursor_position: Point, + state: &State, + value: T, + range: &RangeInclusive<T>, + style_sheet: &dyn StyleSheet, +) where + T: Into<f64> + Copy, +{ + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + let style = if state.is_dragging { + style_sheet.dragging() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + let rail_y = bounds.y + (bounds.height / 2.0).round(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style.rail_colors.0, + ); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: rail_y + 2.0, + width: bounds.width, + height: 2.0, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Background::Color(style.rail_colors.1), + ); + + let (handle_width, handle_height, handle_border_radius) = match style + .handle + .shape + { + HandleShape::Circle { radius } => (radius * 2.0, radius * 2.0, radius), + HandleShape::Rectangle { + width, + border_radius, + } => (f32::from(width), f32::from(bounds.height), border_radius), + }; + + let value = value.into() as f32; + let (range_start, range_end) = { + let (start, end) = range.clone().into_inner(); + + (start.into() as f32, end.into() as f32) + }; + + let handle_offset = if range_start >= range_end { + 0.0 + } else { + (bounds.width - handle_width) * (value - range_start) + / (range_end - range_start) + }; + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + handle_offset.round(), + y: rail_y - handle_height / 2.0, + width: handle_width, + height: handle_height, + }, + border_radius: handle_border_radius, + border_width: style.handle.border_width, + border_color: style.handle.border_color, + }, + style.handle.color, + ); +} + +/// Computes the current [`mouse::Interaction`] of a [`Slider`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, + state: &State, +) -> mouse::Interaction { + let bounds = layout.bounds(); + let is_mouse_over = bounds.contains(cursor_position); + + if state.is_dragging { + mouse::Interaction::Grabbing + } else if is_mouse_over { + mouse::Interaction::Grab + } else { + mouse::Interaction::default() + } +} + /// The local state of a [`Slider`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct State { @@ -192,73 +393,18 @@ where _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> event::Status { - let is_dragging = self.state.is_dragging; - - let mut change = || { - let bounds = layout.bounds(); - let new_value = if cursor_position.x <= bounds.x { - *self.range.start() - } else if cursor_position.x >= bounds.x + bounds.width { - *self.range.end() - } else { - let step = self.step.into(); - let start = (*self.range.start()).into(); - let end = (*self.range.end()).into(); - - let percent = f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width); - - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; - - if let Some(value) = T::from_f64(value) { - value - } else { - return; - } - }; - - if (self.value.into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((self.on_change)(new_value)); - - self.value = new_value; - } - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if layout.bounds().contains(cursor_position) { - change(); - self.state.is_dragging = true; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if is_dragging { - if let Some(on_release) = self.on_release.clone() { - shell.publish(on_release); - } - self.state.is_dragging = false; - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - change(); - - return event::Status::Captured; - } - } - _ => {} - } - - event::Status::Ignored + update( + event, + layout, + cursor_position, + shell, + &mut self.state, + &mut self.value, + &self.range, + self.step, + self.on_change.as_ref(), + &self.on_release, + ) } fn draw( @@ -269,90 +415,15 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - let style = if self.state.is_dragging { - self.style_sheet.dragging() - } else if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - let rail_y = bounds.y + (bounds.height / 2.0).round(); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y, - width: bounds.width, - height: 2.0, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - style.rail_colors.0, - ); - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: rail_y + 2.0, - width: bounds.width, - height: 2.0, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - Background::Color(style.rail_colors.1), - ); - - let (handle_width, handle_height, handle_border_radius) = match style - .handle - .shape - { - HandleShape::Circle { radius } => { - (radius * 2.0, radius * 2.0, radius) - } - HandleShape::Rectangle { - width, - border_radius, - } => (f32::from(width), f32::from(bounds.height), border_radius), - }; - - let value = self.value.into() as f32; - let (range_start, range_end) = { - let (start, end) = self.range.clone().into_inner(); - - (start.into() as f32, end.into() as f32) - }; - - let handle_offset = if range_start >= range_end { - 0.0 - } else { - (bounds.width - handle_width) * (value - range_start) - / (range_end - range_start) - }; - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + handle_offset.round(), - y: rail_y - handle_height / 2.0, - width: handle_width, - height: handle_height, - }, - border_radius: handle_border_radius, - border_width: style.handle.border_width, - border_color: style.handle.border_color, - }, - style.handle.color, - ); + draw( + renderer, + layout, + cursor_position, + &self.state, + self.value, + &self.range, + self.style_sheet.as_ref(), + ) } fn mouse_interaction( @@ -362,16 +433,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - let bounds = layout.bounds(); - let is_mouse_over = bounds.contains(cursor_position); - - if self.state.is_dragging { - mouse::Interaction::Grabbing - } else if is_mouse_over { - mouse::Interaction::Grab - } else { - mouse::Interaction::default() - } + mouse_interaction(layout, cursor_position, &self.state) } } diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index e30e2343..d13d6ef1 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -24,8 +24,6 @@ use crate::{ Shell, Size, Vector, Widget, }; -use std::u32; - pub use iced_style::text_input::{Style, StyleSheet}; /// A field that can be filled with text. @@ -61,10 +59,9 @@ pub struct TextInput<'a, Message, Renderer: text::Renderer> { is_secure: bool, font: Renderer::Font, width: Length, - max_width: u32, padding: Padding, size: Option<u16>, - on_change: Box<dyn Fn(String) -> Message>, + on_change: Box<dyn Fn(String) -> Message + 'a>, on_submit: Option<Message>, style_sheet: Box<dyn StyleSheet + 'a>, } @@ -88,7 +85,7 @@ where on_change: F, ) -> Self where - F: 'static + Fn(String) -> Message, + F: 'a + Fn(String) -> Message, { TextInput { state, @@ -97,7 +94,6 @@ where is_secure: false, font: Default::default(), width: Length::Fill, - max_width: u32::MAX, padding: Padding::ZERO, size: None, on_change: Box::new(on_change), @@ -126,12 +122,6 @@ where self } - /// Sets the maximum width of the [`TextInput`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.max_width = max_width; - self - } - /// Sets the [`Padding`] of the [`TextInput`]. pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self { self.padding = padding.into(); @@ -166,520 +156,313 @@ where } } -impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> +/// Computes the layout of a [`TextInput`]. +pub fn layout<Renderer>( + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + padding: Padding, + size: Option<u16>, +) -> layout::Node where Renderer: text::Renderer, { - /// Draws the [`TextInput`] with the given [`Renderer`], overriding its - /// [`Value`] if provided. - pub fn draw( - &self, - renderer: &mut Renderer, - layout: Layout<'_>, - cursor_position: Point, - value: Option<&Value>, - ) { - let value = value.unwrap_or(&self.value); - let secure_value = self.is_secure.then(|| value.secure()); - let value = secure_value.as_ref().unwrap_or(&value); + let text_size = size.unwrap_or(renderer.default_size()); - let bounds = layout.bounds(); - let text_bounds = layout.children().next().unwrap().bounds(); + let limits = limits + .pad(padding) + .width(width) + .height(Length::Units(text_size)); - let is_mouse_over = bounds.contains(cursor_position); + let mut text = layout::Node::new(limits.resolve(Size::ZERO)); + text.move_to(Point::new(padding.left.into(), padding.top.into())); - let style = if self.state.is_focused() { - self.style_sheet.focused() - } else if is_mouse_over { - self.style_sheet.hovered() - } else { - self.style_sheet.active() - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: style.border_radius, - border_width: style.border_width, - border_color: style.border_color, - }, - style.background, - ); - - let text = value.to_string(); - let size = self.size.unwrap_or(renderer.default_size()); - - let (cursor, offset) = if self.state.is_focused() { - match self.state.cursor.state(&value) { - cursor::State::Index(position) => { - let (text_value_width, offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - &value, - size, - position, - self.font.clone(), - ); - - ( - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + text_value_width, - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - self.style_sheet.value_color(), - )), - offset, - ) - } - cursor::State::Selection { start, end } => { - let left = start.min(end); - let right = end.max(start); - - let (left_position, left_offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - &value, - size, - left, - self.font.clone(), - ); - - let (right_position, right_offset) = - measure_cursor_and_scroll_offset( - renderer, - text_bounds, - &value, - size, - right, - self.font.clone(), - ); - - let width = right_position - left_position; - - ( - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + left_position, - y: text_bounds.y, - width, - height: text_bounds.height, - }, - border_radius: 0.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - self.style_sheet.selection_color(), - )), - if end == right { - right_offset - } else { - left_offset - }, - ) - } - } - } else { - (None, 0.0) - }; - - let text_width = renderer.measure_width( - if text.is_empty() { - &self.placeholder - } else { - &text - }, - size, - self.font.clone(), - ); - - let render = |renderer: &mut Renderer| { - if let Some((cursor, color)) = cursor { - renderer.fill_quad(cursor, color); - } - - renderer.fill_text(Text { - content: if text.is_empty() { - &self.placeholder - } else { - &text - }, - color: if text.is_empty() { - self.style_sheet.placeholder_color() - } else { - self.style_sheet.value_color() - }, - font: self.font.clone(), - bounds: Rectangle { - y: text_bounds.center_y(), - width: f32::INFINITY, - ..text_bounds - }, - size: f32::from(size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - }); - }; - - if text_width > text_bounds.width { - renderer.with_layer(text_bounds, |renderer| { - renderer.with_translation(Vector::new(-offset, 0.0), render) - }); - } else { - render(renderer); - } - } + layout::Node::with_children(text.size().pad(padding), vec![text]) } -impl<'a, Message, Renderer> Widget<Message, Renderer> - for TextInput<'a, Message, Renderer> +/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] +/// accordingly. +pub fn update<'a, Message, Renderer>( + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + value: &mut Value, + size: Option<u16>, + font: &Renderer::Font, + is_secure: bool, + on_change: &dyn Fn(String) -> Message, + on_submit: &Option<Message>, + state: impl FnOnce() -> &'a mut State, +) -> event::Status where Message: Clone, Renderer: text::Renderer, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - Length::Shrink - } - - fn layout( - &self, - renderer: &Renderer, - limits: &layout::Limits, - ) -> layout::Node { - let text_size = self.size.unwrap_or(renderer.default_size()); - - let limits = limits - .pad(self.padding) - .width(self.width) - .max_width(self.max_width) - .height(Length::Units(text_size)); - - let mut text = layout::Node::new(limits.resolve(Size::ZERO)); - text.move_to(Point::new( - self.padding.left.into(), - self.padding.top.into(), - )); - - layout::Node::with_children(text.size().pad(self.padding), vec![text]) - } - - fn on_event( - &mut self, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> event::Status { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let is_clicked = layout.bounds().contains(cursor_position); - - self.state.is_focused = is_clicked; - - if is_clicked { - let text_layout = layout.children().next().unwrap(); - let target = cursor_position.x - text_layout.bounds().x; - - let click = mouse::Click::new( - cursor_position, - self.state.last_click, - ); - - match click.kind() { - click::Kind::Single => { - let position = if target > 0.0 { - let value = if self.is_secure { - self.value.secure() - } else { - self.value.clone() - }; - - find_cursor_position( - renderer, - text_layout.bounds(), - self.font.clone(), - self.size, - &value, - &self.state, - target, - ) + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state(); + let is_clicked = layout.bounds().contains(cursor_position); + + state.is_focused = is_clicked; + + if is_clicked { + let text_layout = layout.children().next().unwrap(); + let target = cursor_position.x - text_layout.bounds().x; + + let click = + mouse::Click::new(cursor_position, state.last_click); + + match click.kind() { + click::Kind::Single => { + let position = if target > 0.0 { + let value = if is_secure { + value.secure() } else { - None + value.clone() }; - self.state.cursor.move_to(position.unwrap_or(0)); - self.state.is_dragging = true; - } - click::Kind::Double => { - if self.is_secure { - self.state.cursor.select_all(&self.value); - } else { - let position = find_cursor_position( - renderer, - text_layout.bounds(), - self.font.clone(), - self.size, - &self.value, - &self.state, - target, - ) - .unwrap_or(0); - - self.state.cursor.select_range( - self.value.previous_start_of_word(position), - self.value.next_end_of_word(position), - ); - } + find_cursor_position( + renderer, + text_layout.bounds(), + font.clone(), + size, + &value, + state, + target, + ) + } else { + None + }; - self.state.is_dragging = false; - } - click::Kind::Triple => { - self.state.cursor.select_all(&self.value); - self.state.is_dragging = false; + state.cursor.move_to(position.unwrap_or(0)); + state.is_dragging = true; + } + click::Kind::Double => { + if is_secure { + state.cursor.select_all(value); + } else { + let position = find_cursor_position( + renderer, + text_layout.bounds(), + font.clone(), + size, + value, + state, + target, + ) + .unwrap_or(0); + + state.cursor.select_range( + value.previous_start_of_word(position), + value.next_end_of_word(position), + ); } + + state.is_dragging = false; + } + click::Kind::Triple => { + state.cursor.select_all(value); + state.is_dragging = false; } + } - self.state.last_click = Some(click); + state.last_click = Some(click); - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - self.state.is_dragging = false; + return event::Status::Captured; } - Event::Mouse(mouse::Event::CursorMoved { position }) - | Event::Touch(touch::Event::FingerMoved { position, .. }) => { - if self.state.is_dragging { - let text_layout = layout.children().next().unwrap(); - let target = position.x - text_layout.bounds().x; - - let value = if self.is_secure { - self.value.secure() - } else { - self.value.clone() - }; + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state().is_dragging = false; + } + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { + let state = state(); - let position = find_cursor_position( - renderer, - text_layout.bounds(), - self.font.clone(), - self.size, - &value, - &self.state, - target, - ) - .unwrap_or(0); + if state.is_dragging { + let text_layout = layout.children().next().unwrap(); + let target = position.x - text_layout.bounds().x; - self.state.cursor.select_range( - self.state.cursor.start(&value), - position, - ); + let value = if is_secure { + value.secure() + } else { + value.clone() + }; + + let position = find_cursor_position( + renderer, + text_layout.bounds(), + font.clone(), + size, + &value, + state, + target, + ) + .unwrap_or(0); + + state + .cursor + .select_range(state.cursor.start(&value), position); - return event::Status::Captured; - } + return event::Status::Captured; } - Event::Keyboard(keyboard::Event::CharacterReceived(c)) - if self.state.is_focused - && self.state.is_pasting.is_none() - && !self.state.keyboard_modifiers.command() - && !c.is_control() => + } + Event::Keyboard(keyboard::Event::CharacterReceived(c)) => { + let state = state(); + + if state.is_focused + && state.is_pasting.is_none() + && !state.keyboard_modifiers.command() + && !c.is_control() { - let mut editor = - Editor::new(&mut self.value, &mut self.state.cursor); + let mut editor = Editor::new(value, &mut state.cursor); editor.insert(c); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); return event::Status::Captured; } - Event::Keyboard(keyboard::Event::KeyPressed { - key_code, .. - }) if self.state.is_focused => { - let modifiers = self.state.keyboard_modifiers; + } + Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. }) => { + let state = state(); + + if state.is_focused { + let modifiers = state.keyboard_modifiers; match key_code { keyboard::KeyCode::Enter | keyboard::KeyCode::NumpadEnter => { - if let Some(on_submit) = self.on_submit.clone() { + if let Some(on_submit) = on_submit.clone() { shell.publish(on_submit); } } keyboard::KeyCode::Backspace => { if platform::is_jump_modifier_pressed(modifiers) - && self - .state - .cursor - .selection(&self.value) - .is_none() + && state.cursor.selection(value).is_none() { - if self.is_secure { - let cursor_pos = - self.state.cursor.end(&self.value); - self.state.cursor.select_range(0, cursor_pos); + if is_secure { + let cursor_pos = state.cursor.end(value); + state.cursor.select_range(0, cursor_pos); } else { - self.state - .cursor - .select_left_by_words(&self.value); + state.cursor.select_left_by_words(value); } } - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); - + let mut editor = Editor::new(value, &mut state.cursor); editor.backspace(); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); } keyboard::KeyCode::Delete => { if platform::is_jump_modifier_pressed(modifiers) - && self - .state - .cursor - .selection(&self.value) - .is_none() + && state.cursor.selection(value).is_none() { - if self.is_secure { - let cursor_pos = - self.state.cursor.end(&self.value); - self.state + if is_secure { + let cursor_pos = state.cursor.end(value); + state .cursor - .select_range(cursor_pos, self.value.len()); + .select_range(cursor_pos, value.len()); } else { - self.state - .cursor - .select_right_by_words(&self.value); + state.cursor.select_right_by_words(value); } } - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); - + let mut editor = Editor::new(value, &mut state.cursor); editor.delete(); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); } keyboard::KeyCode::Left => { if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure + && !is_secure { if modifiers.shift() { - self.state - .cursor - .select_left_by_words(&self.value); + state.cursor.select_left_by_words(value); } else { - self.state - .cursor - .move_left_by_words(&self.value); + state.cursor.move_left_by_words(value); } } else if modifiers.shift() { - self.state.cursor.select_left(&self.value) + state.cursor.select_left(value) } else { - self.state.cursor.move_left(&self.value); + state.cursor.move_left(value); } } keyboard::KeyCode::Right => { if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure + && !is_secure { if modifiers.shift() { - self.state - .cursor - .select_right_by_words(&self.value); + state.cursor.select_right_by_words(value); } else { - self.state - .cursor - .move_right_by_words(&self.value); + state.cursor.move_right_by_words(value); } } else if modifiers.shift() { - self.state.cursor.select_right(&self.value) + state.cursor.select_right(value) } else { - self.state.cursor.move_right(&self.value); + state.cursor.move_right(value); } } keyboard::KeyCode::Home => { if modifiers.shift() { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - 0, - ); + state + .cursor + .select_range(state.cursor.start(value), 0); } else { - self.state.cursor.move_to(0); + state.cursor.move_to(0); } } keyboard::KeyCode::End => { if modifiers.shift() { - self.state.cursor.select_range( - self.state.cursor.start(&self.value), - self.value.len(), + state.cursor.select_range( + state.cursor.start(value), + value.len(), ); } else { - self.state.cursor.move_to(self.value.len()); + state.cursor.move_to(value.len()); } } keyboard::KeyCode::C - if self.state.keyboard_modifiers.command() => + if state.keyboard_modifiers.command() => { - match self.state.cursor.selection(&self.value) { + match state.cursor.selection(value) { Some((start, end)) => { clipboard.write( - self.value.select(start, end).to_string(), + value.select(start, end).to_string(), ); } None => {} } } keyboard::KeyCode::X - if self.state.keyboard_modifiers.command() => + if state.keyboard_modifiers.command() => { - match self.state.cursor.selection(&self.value) { + match state.cursor.selection(value) { Some((start, end)) => { clipboard.write( - self.value.select(start, end).to_string(), + value.select(start, end).to_string(), ); } None => {} } - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); - + let mut editor = Editor::new(value, &mut state.cursor); editor.delete(); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); } keyboard::KeyCode::V => { - if self.state.keyboard_modifiers.command() { - let content = match self.state.is_pasting.take() { + if state.keyboard_modifiers.command() { + let content = match state.is_pasting.take() { Some(content) => content, None => { let content: String = clipboard @@ -693,32 +476,30 @@ where } }; - let mut editor = Editor::new( - &mut self.value, - &mut self.state.cursor, - ); + let mut editor = + Editor::new(value, &mut state.cursor); editor.paste(content.clone()); - let message = (self.on_change)(editor.contents()); + let message = (on_change)(editor.contents()); shell.publish(message); - self.state.is_pasting = Some(content); + state.is_pasting = Some(content); } else { - self.state.is_pasting = None; + state.is_pasting = None; } } keyboard::KeyCode::A - if self.state.keyboard_modifiers.command() => + if state.keyboard_modifiers.command() => { - self.state.cursor.select_all(&self.value); + state.cursor.select_all(value); } keyboard::KeyCode::Escape => { - self.state.is_focused = false; - self.state.is_dragging = false; - self.state.is_pasting = None; + state.is_focused = false; + state.is_dragging = false; + state.is_pasting = None; - self.state.keyboard_modifiers = + state.keyboard_modifiers = keyboard::Modifiers::default(); } keyboard::KeyCode::Tab @@ -728,15 +509,17 @@ where } _ => {} } - - return event::Status::Captured; } - Event::Keyboard(keyboard::Event::KeyReleased { - key_code, .. - }) if self.state.is_focused => { + + return event::Status::Captured; + } + Event::Keyboard(keyboard::Event::KeyReleased { key_code, .. }) => { + let state = state(); + + if state.is_focused { match key_code { keyboard::KeyCode::V => { - self.state.is_pasting = None; + state.is_pasting = None; } keyboard::KeyCode::Tab | keyboard::KeyCode::Up @@ -748,15 +531,246 @@ where return event::Status::Captured; } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) - if self.state.is_focused => - { - self.state.keyboard_modifiers = modifiers; + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state(); + + if state.is_focused { + state.keyboard_modifiers = modifiers; + } + } + _ => {} + } + + event::Status::Ignored +} + +/// Draws the [`TextInput`] with the given [`Renderer`], overriding its +/// [`Value`] if provided. +pub fn draw<Renderer>( + renderer: &mut Renderer, + layout: Layout<'_>, + cursor_position: Point, + state: &State, + value: &Value, + placeholder: &str, + size: Option<u16>, + font: &Renderer::Font, + is_secure: bool, + style_sheet: &dyn StyleSheet, +) where + Renderer: text::Renderer, +{ + let secure_value = is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(&value); + + let bounds = layout.bounds(); + let text_bounds = layout.children().next().unwrap().bounds(); + + let is_mouse_over = bounds.contains(cursor_position); + + let style = if state.is_focused() { + style_sheet.focused() + } else if is_mouse_over { + style_sheet.hovered() + } else { + style_sheet.active() + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border_radius: style.border_radius, + border_width: style.border_width, + border_color: style.border_color, + }, + style.background, + ); + + let text = value.to_string(); + let size = size.unwrap_or(renderer.default_size()); + + let (cursor, offset) = if state.is_focused() { + match state.cursor.state(&value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + &value, + size, + position, + font.clone(), + ); + + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style_sheet.value_color(), + )), + offset, + ) + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + + let (left_position, left_offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + &value, + size, + left, + font.clone(), + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + renderer, + text_bounds, + &value, + size, + right, + font.clone(), + ); + + let width = right_position - left_position; + + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + left_position, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + border_radius: 0.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + style_sheet.selection_color(), + )), + if end == right { + right_offset + } else { + left_offset + }, + ) } - _ => {} } + } else { + (None, 0.0) + }; + + let text_width = renderer.measure_width( + if text.is_empty() { placeholder } else { &text }, + size, + font.clone(), + ); + + let render = |renderer: &mut Renderer| { + if let Some((cursor, color)) = cursor { + renderer.fill_quad(cursor, color); + } + + renderer.fill_text(Text { + content: if text.is_empty() { placeholder } else { &text }, + color: if text.is_empty() { + style_sheet.placeholder_color() + } else { + style_sheet.value_color() + }, + font: font.clone(), + bounds: Rectangle { + y: text_bounds.center_y(), + width: f32::INFINITY, + ..text_bounds + }, + size: f32::from(size), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + }); + }; + + if text_width > text_bounds.width { + renderer.with_layer(text_bounds, |renderer| { + renderer.with_translation(Vector::new(-offset, 0.0), render) + }); + } else { + render(renderer); + } +} - event::Status::Ignored +/// Computes the current [`mouse::Interaction`] of the [`TextInput`]. +pub fn mouse_interaction( + layout: Layout<'_>, + cursor_position: Point, +) -> mouse::Interaction { + if layout.bounds().contains(cursor_position) { + mouse::Interaction::Text + } else { + mouse::Interaction::default() + } +} + +impl<'a, Message, Renderer> Widget<Message, Renderer> + for TextInput<'a, Message, Renderer> +where + Message: Clone, + Renderer: text::Renderer, +{ + fn width(&self) -> Length { + self.width + } + + fn height(&self) -> Length { + Length::Shrink + } + + fn layout( + &self, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout(renderer, limits, self.width, self.padding, self.size) + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + update( + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + &mut self.value, + self.size, + &self.font, + self.is_secure, + self.on_change.as_ref(), + &self.on_submit, + || &mut self.state, + ) } fn mouse_interaction( @@ -766,11 +780,7 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - if layout.bounds().contains(cursor_position) { - mouse::Interaction::Text - } else { - mouse::Interaction::default() - } + mouse_interaction(layout, cursor_position) } fn draw( @@ -781,7 +791,18 @@ where cursor_position: Point, _viewport: &Rectangle, ) { - self.draw(renderer, layout, cursor_position, None) + draw( + renderer, + layout, + cursor_position, + &self.state, + &self.value, + &self.placeholder, + self.size, + &self.font, + self.is_secure, + self.style_sheet.as_ref(), + ) } } diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index 48237edb..a7847871 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -32,7 +32,7 @@ pub use iced_style::toggler::{Style, StyleSheet}; #[allow(missing_debug_implementations)] pub struct Toggler<'a, Message, Renderer: text::Renderer> { is_active: bool, - on_toggle: Box<dyn Fn(bool) -> Message>, + on_toggle: Box<dyn Fn(bool) -> Message + 'a>, label: Option<String>, width: Length, size: u16, @@ -61,7 +61,7 @@ impl<'a, Message, Renderer: text::Renderer> Toggler<'a, Message, Renderer> { f: F, ) -> Self where - F: 'static + Fn(bool) -> Message, + F: 'a + Fn(bool) -> Message, { Toggler { is_active, |