diff options
| author | 2022-03-23 17:11:14 +0700 | |
|---|---|---|
| committer | 2022-03-23 17:11:14 +0700 | |
| commit | 0eef527fa5b04be74141c75b076677473320e321 (patch) | |
| tree | 5062a9ce2c370632de87a01471526da1176e0a60 /native/src | |
| parent | 4aece6b77617f4a37af8208d8ddb1618bf9052d3 (diff) | |
| parent | ef4c79ea23e86fec9a8ad0fb27463296c14400e5 (diff) | |
| download | iced-0eef527fa5b04be74141c75b076677473320e321.tar.gz iced-0eef527fa5b04be74141c75b076677473320e321.tar.bz2 iced-0eef527fa5b04be74141c75b076677473320e321.zip  | |
Merge pull request #1284 from iced-rs/virtual-widgets
Stateless widgets
Diffstat (limited to '')
| -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/pane_grid.rs | 781 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/axis.rs | 7 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/content.rs | 39 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/draggable.rs | 12 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/state.rs | 114 | ||||
| -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/rule.rs | 12 | ||||
| -rw-r--r-- | native/src/widget/scrollable.rs | 896 | ||||
| -rw-r--r-- | native/src/widget/slider.rs | 384 | ||||
| -rw-r--r-- | native/src/widget/text_input.rs | 899 | ||||
| -rw-r--r-- | native/src/widget/toggler.rs | 7 | 
16 files changed, 2355 insertions, 1778 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/pane_grid.rs b/native/src/widget/pane_grid.rs index 8ad63cf1..2093886e 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -11,16 +11,19 @@ mod axis;  mod configuration;  mod content;  mod direction; +mod draggable;  mod node;  mod pane;  mod split; -mod state;  mod title_bar; +pub mod state; +  pub use axis::Axis;  pub use configuration::Configuration;  pub use content::Content;  pub use direction::Direction; +pub use draggable::Draggable;  pub use node::Node;  pub use pane::Pane;  pub use split::Split; @@ -92,6 +95,7 @@ pub use iced_style::pane_grid::{Line, StyleSheet};  #[allow(missing_debug_implementations)]  pub struct PaneGrid<'a, Message, Renderer> {      state: &'a mut state::Internal, +    action: &'a mut state::Action,      elements: Vec<(Pane, Content<'a, Message, Renderer>)>,      width: Length,      height: Length, @@ -124,6 +128,7 @@ where          Self {              state: &mut state.internal, +            action: &mut state.action,              elements,              width: Length::Fill,              height: Length::Fill, @@ -197,80 +202,407 @@ where      }  } -impl<'a, Message, Renderer> PaneGrid<'a, Message, Renderer> -where -    Renderer: crate::Renderer, -{ -    fn click_pane( -        &mut self, -        layout: Layout<'_>, -        cursor_position: Point, -        shell: &mut Shell<'_, Message>, -    ) { -        let mut clicked_region = -            self.elements.iter().zip(layout.children()).filter( -                |(_, layout)| layout.bounds().contains(cursor_position), +/// Calculates the [`Layout`] of a [`PaneGrid`]. +pub fn layout<Renderer, T>( +    renderer: &Renderer, +    limits: &layout::Limits, +    state: &state::Internal, +    width: Length, +    height: Length, +    spacing: u16, +    elements: impl Iterator<Item = (Pane, T)>, +    layout_element: impl Fn(T, &Renderer, &layout::Limits) -> layout::Node, +) -> layout::Node { +    let limits = limits.width(width).height(height); +    let size = limits.resolve(Size::ZERO); + +    let regions = state.pane_regions(f32::from(spacing), size); +    let children = elements +        .filter_map(|(pane, element)| { +            let region = regions.get(&pane)?; +            let size = Size::new(region.width, region.height); + +            let mut node = layout_element( +                element, +                renderer, +                &layout::Limits::new(size, size),              ); -        if let Some(((pane, content), layout)) = clicked_region.next() { -            if let Some(on_click) = &self.on_click { -                shell.publish(on_click(*pane)); +            node.move_to(Point::new(region.x, region.y)); + +            Some(node) +        }) +        .collect(); + +    layout::Node::with_children(size, children) +} + +/// Processes an [`Event`] and updates the [`state`] of a [`PaneGrid`] +/// accordingly. +pub fn update<'a, Message, T: Draggable>( +    action: &mut state::Action, +    state: &state::Internal, +    event: &Event, +    layout: Layout<'_>, +    cursor_position: Point, +    shell: &mut Shell<'_, Message>, +    spacing: u16, +    elements: impl Iterator<Item = (Pane, T)>, +    on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, +    on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, +    on_resize: &Option<(u16, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, +) -> event::Status { +    let mut event_status = event::Status::Ignored; + +    match event { +        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +        | Event::Touch(touch::Event::FingerPressed { .. }) => { +            let bounds = layout.bounds(); + +            if bounds.contains(cursor_position) { +                event_status = event::Status::Captured; + +                match on_resize { +                    Some((leeway, _)) => { +                        let relative_cursor = Point::new( +                            cursor_position.x - bounds.x, +                            cursor_position.y - bounds.y, +                        ); + +                        let splits = state.split_regions( +                            f32::from(spacing), +                            Size::new(bounds.width, bounds.height), +                        ); + +                        let clicked_split = hovered_split( +                            splits.iter(), +                            f32::from(spacing + leeway), +                            relative_cursor, +                        ); + +                        if let Some((split, axis, _)) = clicked_split { +                            if action.picked_pane().is_none() { +                                *action = +                                    state::Action::Resizing { split, axis }; +                            } +                        } else { +                            click_pane( +                                action, +                                layout, +                                cursor_position, +                                shell, +                                elements, +                                on_click, +                                on_drag, +                            ); +                        } +                    } +                    None => { +                        click_pane( +                            action, +                            layout, +                            cursor_position, +                            shell, +                            elements, +                            on_click, +                            on_drag, +                        ); +                    } +                }              } +        } +        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) +        | Event::Touch(touch::Event::FingerLifted { .. }) +        | Event::Touch(touch::Event::FingerLost { .. }) => { +            if let Some((pane, _)) = action.picked_pane() { +                if let Some(on_drag) = on_drag { +                    let mut dropped_region = elements +                        .zip(layout.children()) +                        .filter(|(_, layout)| { +                            layout.bounds().contains(cursor_position) +                        }); + +                    let event = match dropped_region.next() { +                        Some(((target, _), _)) if pane != target => { +                            DragEvent::Dropped { pane, target } +                        } +                        _ => DragEvent::Canceled { pane }, +                    }; + +                    shell.publish(on_drag(event)); +                } + +                *action = state::Action::Idle; + +                event_status = event::Status::Captured; +            } else if action.picked_split().is_some() { +                *action = state::Action::Idle; + +                event_status = event::Status::Captured; +            } +        } +        Event::Mouse(mouse::Event::CursorMoved { .. }) +        | Event::Touch(touch::Event::FingerMoved { .. }) => { +            if let Some((_, on_resize)) = on_resize { +                if let Some((split, _)) = action.picked_split() { +                    let bounds = layout.bounds(); + +                    let splits = state.split_regions( +                        f32::from(spacing), +                        Size::new(bounds.width, bounds.height), +                    ); -            if let Some(on_drag) = &self.on_drag { -                if content.can_be_picked_at(layout, cursor_position) { -                    let pane_position = layout.position(); +                    if let Some((axis, rectangle, _)) = splits.get(&split) { +                        let ratio = match axis { +                            Axis::Horizontal => { +                                let position = +                                    cursor_position.y - bounds.y - rectangle.y; -                    let origin = cursor_position -                        - Vector::new(pane_position.x, pane_position.y); +                                (position / rectangle.height).max(0.1).min(0.9) +                            } +                            Axis::Vertical => { +                                let position = +                                    cursor_position.x - bounds.x - rectangle.x; -                    self.state.pick_pane(pane, origin); +                                (position / rectangle.width).max(0.1).min(0.9) +                            } +                        }; -                    shell.publish(on_drag(DragEvent::Picked { pane: *pane })); +                        shell.publish(on_resize(ResizeEvent { split, ratio })); + +                        event_status = event::Status::Captured; +                    }                  }              }          } +        _ => {}      } -    fn trigger_resize( -        &mut self, -        layout: Layout<'_>, -        cursor_position: Point, -        shell: &mut Shell<'_, Message>, -    ) -> event::Status { -        if let Some((_, on_resize)) = &self.on_resize { -            if let Some((split, _)) = self.state.picked_split() { +    event_status +} + +fn click_pane<'a, Message, T>( +    action: &mut state::Action, +    layout: Layout<'_>, +    cursor_position: Point, +    shell: &mut Shell<'_, Message>, +    elements: impl Iterator<Item = (Pane, T)>, +    on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, +    on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, +) where +    T: Draggable, +{ +    let mut clicked_region = elements +        .zip(layout.children()) +        .filter(|(_, layout)| layout.bounds().contains(cursor_position)); + +    if let Some(((pane, content), layout)) = clicked_region.next() { +        if let Some(on_click) = &on_click { +            shell.publish(on_click(pane)); +        } + +        if let Some(on_drag) = &on_drag { +            if content.can_be_dragged_at(layout, cursor_position) { +                let pane_position = layout.position(); + +                let origin = cursor_position +                    - Vector::new(pane_position.x, pane_position.y); + +                *action = state::Action::Dragging { pane, origin }; + +                shell.publish(on_drag(DragEvent::Picked { pane })); +            } +        } +    } +} + +/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`]. +pub fn mouse_interaction( +    action: &state::Action, +    state: &state::Internal, +    layout: Layout<'_>, +    cursor_position: Point, +    spacing: u16, +    resize_leeway: Option<u16>, +) -> Option<mouse::Interaction> { +    if action.picked_pane().is_some() { +        return Some(mouse::Interaction::Grab); +    } + +    let resize_axis = +        action.picked_split().map(|(_, axis)| axis).or_else(|| { +            resize_leeway.and_then(|leeway| {                  let bounds = layout.bounds(); -                let splits = self.state.split_regions( -                    f32::from(self.spacing), -                    Size::new(bounds.width, bounds.height), +                let splits = +                    state.split_regions(f32::from(spacing), bounds.size()); + +                let relative_cursor = Point::new( +                    cursor_position.x - bounds.x, +                    cursor_position.y - bounds.y,                  ); -                if let Some((axis, rectangle, _)) = splits.get(&split) { -                    let ratio = match axis { -                        Axis::Horizontal => { -                            let position = -                                cursor_position.y - bounds.y - rectangle.y; +                hovered_split( +                    splits.iter(), +                    f32::from(spacing + leeway), +                    relative_cursor, +                ) +                .map(|(_, axis, _)| axis) +            }) +        }); -                            (position / rectangle.height).max(0.1).min(0.9) -                        } -                        Axis::Vertical => { -                            let position = -                                cursor_position.x - bounds.x - rectangle.x; +    if let Some(resize_axis) = resize_axis { +        return Some(match resize_axis { +            Axis::Horizontal => mouse::Interaction::ResizingVertically, +            Axis::Vertical => mouse::Interaction::ResizingHorizontally, +        }); +    } -                            (position / rectangle.width).max(0.1).min(0.9) -                        } -                    }; +    None +} + +/// Draws a [`PaneGrid`]. +pub fn draw<Renderer, T>( +    action: &state::Action, +    state: &state::Internal, +    layout: Layout<'_>, +    cursor_position: Point, +    renderer: &mut Renderer, +    style: &renderer::Style, +    viewport: &Rectangle, +    spacing: u16, +    resize_leeway: Option<u16>, +    style_sheet: &dyn StyleSheet, +    elements: impl Iterator<Item = (Pane, T)>, +    draw_pane: impl Fn( +        T, +        &mut Renderer, +        &renderer::Style, +        Layout<'_>, +        Point, +        &Rectangle, +    ), +) where +    Renderer: crate::Renderer, +{ +    let picked_pane = action.picked_pane(); -                    shell.publish(on_resize(ResizeEvent { split, ratio })); +    let picked_split = action +        .picked_split() +        .and_then(|(split, axis)| { +            let bounds = layout.bounds(); -                    return event::Status::Captured; -                } +            let splits = state.split_regions(f32::from(spacing), bounds.size()); + +            let (_axis, region, ratio) = splits.get(&split)?; + +            let region = +                axis.split_line_bounds(*region, *ratio, f32::from(spacing)); + +            Some((axis, region + Vector::new(bounds.x, bounds.y), true)) +        }) +        .or_else(|| match resize_leeway { +            Some(leeway) => { +                let bounds = layout.bounds(); + +                let relative_cursor = Point::new( +                    cursor_position.x - bounds.x, +                    cursor_position.y - bounds.y, +                ); + +                let splits = +                    state.split_regions(f32::from(spacing), bounds.size()); + +                let (_split, axis, region) = hovered_split( +                    splits.iter(), +                    f32::from(spacing + leeway), +                    relative_cursor, +                )?; + +                Some((axis, region + Vector::new(bounds.x, bounds.y), false)) +            } +            None => None, +        }); + +    let pane_cursor_position = if picked_pane.is_some() { +        // TODO: Remove once cursor availability is encoded in the type +        // system +        Point::new(-1.0, -1.0) +    } else { +        cursor_position +    }; + +    for ((id, pane), layout) in elements.zip(layout.children()) { +        match picked_pane { +            Some((dragging, origin)) if id == dragging => { +                let bounds = layout.bounds(); + +                renderer.with_translation( +                    cursor_position +                        - Point::new(bounds.x + origin.x, bounds.y + origin.y), +                    |renderer| { +                        renderer.with_layer(bounds, |renderer| { +                            draw_pane( +                                pane, +                                renderer, +                                style, +                                layout, +                                pane_cursor_position, +                                viewport, +                            ); +                        }); +                    }, +                ); +            } +            _ => { +                draw_pane( +                    pane, +                    renderer, +                    style, +                    layout, +                    pane_cursor_position, +                    viewport, +                );              }          } +    } -        event::Status::Ignored +    if let Some((axis, split_region, is_picked)) = picked_split { +        let highlight = if is_picked { +            style_sheet.picked_split() +        } else { +            style_sheet.hovered_split() +        }; + +        if let Some(highlight) = highlight { +            renderer.fill_quad( +                renderer::Quad { +                    bounds: match axis { +                        Axis::Horizontal => Rectangle { +                            x: split_region.x, +                            y: (split_region.y +                                + (split_region.height - highlight.width) +                                    / 2.0) +                                .round(), +                            width: split_region.width, +                            height: highlight.width, +                        }, +                        Axis::Vertical => Rectangle { +                            x: (split_region.x +                                + (split_region.width - highlight.width) / 2.0) +                                .round(), +                            y: split_region.y, +                            width: highlight.width, +                            height: split_region.height, +                        }, +                    }, +                    border_radius: 0.0, +                    border_width: 0.0, +                    border_color: Color::TRANSPARENT, +                }, +                highlight.color, +            ); +        }      }  } @@ -331,28 +663,16 @@ where          renderer: &Renderer,          limits: &layout::Limits,      ) -> layout::Node { -        let limits = limits.width(self.width).height(self.height); -        let size = limits.resolve(Size::ZERO); - -        let regions = self.state.pane_regions(f32::from(self.spacing), size); - -        let children = self -            .elements -            .iter() -            .filter_map(|(pane, element)| { -                let region = regions.get(pane)?; -                let size = Size::new(region.width, region.height); - -                let mut node = -                    element.layout(renderer, &layout::Limits::new(size, size)); - -                node.move_to(Point::new(region.x, region.y)); - -                Some(node) -            }) -            .collect(); - -        layout::Node::with_children(size, children) +        layout( +            renderer, +            limits, +            self.state, +            self.width, +            self.height, +            self.spacing, +            self.elements.iter().map(|(pane, content)| (*pane, content)), +            |element, renderer, limits| element.layout(renderer, limits), +        )      }      fn on_event( @@ -364,89 +684,21 @@ where          clipboard: &mut dyn Clipboard,          shell: &mut Shell<'_, Message>,      ) -> event::Status { -        let mut event_status = event::Status::Ignored; - -        match event { -            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) -            | Event::Touch(touch::Event::FingerPressed { .. }) => { -                let bounds = layout.bounds(); - -                if bounds.contains(cursor_position) { -                    event_status = event::Status::Captured; - -                    match self.on_resize { -                        Some((leeway, _)) => { -                            let relative_cursor = Point::new( -                                cursor_position.x - bounds.x, -                                cursor_position.y - bounds.y, -                            ); - -                            let splits = self.state.split_regions( -                                f32::from(self.spacing), -                                Size::new(bounds.width, bounds.height), -                            ); - -                            let clicked_split = hovered_split( -                                splits.iter(), -                                f32::from(self.spacing + leeway), -                                relative_cursor, -                            ); - -                            if let Some((split, axis, _)) = clicked_split { -                                self.state.pick_split(&split, axis); -                            } else { -                                self.click_pane(layout, cursor_position, shell); -                            } -                        } -                        None => { -                            self.click_pane(layout, cursor_position, shell); -                        } -                    } -                } -            } -            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) -            | Event::Touch(touch::Event::FingerLifted { .. }) -            | Event::Touch(touch::Event::FingerLost { .. }) => { -                if let Some((pane, _)) = self.state.picked_pane() { -                    if let Some(on_drag) = &self.on_drag { -                        let mut dropped_region = -                            self.elements.iter().zip(layout.children()).filter( -                                |(_, layout)| { -                                    layout.bounds().contains(cursor_position) -                                }, -                            ); - -                        let event = match dropped_region.next() { -                            Some(((target, _), _)) if pane != *target => { -                                DragEvent::Dropped { -                                    pane, -                                    target: *target, -                                } -                            } -                            _ => DragEvent::Canceled { pane }, -                        }; - -                        shell.publish(on_drag(event)); -                    } - -                    self.state.idle(); - -                    event_status = event::Status::Captured; -                } else if self.state.picked_split().is_some() { -                    self.state.idle(); - -                    event_status = event::Status::Captured; -                } -            } -            Event::Mouse(mouse::Event::CursorMoved { .. }) -            | Event::Touch(touch::Event::FingerMoved { .. }) => { -                event_status = -                    self.trigger_resize(layout, cursor_position, shell); -            } -            _ => {} -        } - -        let picked_pane = self.state.picked_pane().map(|(pane, _)| pane); +        let event_status = update( +            self.action, +            self.state, +            &event, +            layout, +            cursor_position, +            shell, +            self.spacing, +            self.elements.iter().map(|(pane, content)| (*pane, content)), +            &self.on_click, +            &self.on_drag, +            &self.on_resize, +        ); + +        let picked_pane = self.action.picked_pane().map(|(pane, _)| pane);          self.elements              .iter_mut() @@ -474,53 +726,29 @@ where          viewport: &Rectangle,          renderer: &Renderer,      ) -> mouse::Interaction { -        if self.state.picked_pane().is_some() { -            return mouse::Interaction::Grab; -        } - -        let resize_axis = -            self.state.picked_split().map(|(_, axis)| axis).or_else(|| { -                self.on_resize.as_ref().and_then(|(leeway, _)| { -                    let bounds = layout.bounds(); - -                    let splits = self -                        .state -                        .split_regions(f32::from(self.spacing), bounds.size()); - -                    let relative_cursor = Point::new( -                        cursor_position.x - bounds.x, -                        cursor_position.y - bounds.y, -                    ); - -                    hovered_split( -                        splits.iter(), -                        f32::from(self.spacing + leeway), -                        relative_cursor, +        mouse_interaction( +            self.action, +            self.state, +            layout, +            cursor_position, +            self.spacing, +            self.on_resize.as_ref().map(|(leeway, _)| *leeway), +        ) +        .unwrap_or_else(|| { +            self.elements +                .iter() +                .zip(layout.children()) +                .map(|((_pane, content), layout)| { +                    content.mouse_interaction( +                        layout, +                        cursor_position, +                        viewport, +                        renderer,                      ) -                    .map(|(_, axis, _)| axis)                  }) -            }); - -        if let Some(resize_axis) = resize_axis { -            return match resize_axis { -                Axis::Horizontal => mouse::Interaction::ResizingVertically, -                Axis::Vertical => mouse::Interaction::ResizingHorizontally, -            }; -        } - -        self.elements -            .iter() -            .zip(layout.children()) -            .map(|((_pane, content), layout)| { -                content.mouse_interaction( -                    layout, -                    cursor_position, -                    viewport, -                    renderer, -                ) -            }) -            .max() -            .unwrap_or_default() +                .max() +                .unwrap_or_default() +        })      }      fn draw( @@ -531,139 +759,22 @@ where          cursor_position: Point,          viewport: &Rectangle,      ) { -        let picked_pane = self.state.picked_pane(); - -        let picked_split = self -            .state -            .picked_split() -            .and_then(|(split, axis)| { -                let bounds = layout.bounds(); - -                let splits = self -                    .state -                    .split_regions(f32::from(self.spacing), bounds.size()); - -                let (_axis, region, ratio) = splits.get(&split)?; - -                let region = axis.split_line_bounds( -                    *region, -                    *ratio, -                    f32::from(self.spacing), -                ); - -                Some((axis, region + Vector::new(bounds.x, bounds.y), true)) -            }) -            .or_else(|| match self.on_resize { -                Some((leeway, _)) => { -                    let bounds = layout.bounds(); - -                    let relative_cursor = Point::new( -                        cursor_position.x - bounds.x, -                        cursor_position.y - bounds.y, -                    ); - -                    let splits = self -                        .state -                        .split_regions(f32::from(self.spacing), bounds.size()); - -                    let (_split, axis, region) = hovered_split( -                        splits.iter(), -                        f32::from(self.spacing + leeway), -                        relative_cursor, -                    )?; - -                    Some(( -                        axis, -                        region + Vector::new(bounds.x, bounds.y), -                        false, -                    )) -                } -                None => None, -            }); - -        let pane_cursor_position = if picked_pane.is_some() { -            // TODO: Remove once cursor availability is encoded in the type -            // system -            Point::new(-1.0, -1.0) -        } else { -            cursor_position -        }; - -        for ((id, pane), layout) in self.elements.iter().zip(layout.children()) -        { -            match picked_pane { -                Some((dragging, origin)) if *id == dragging => { -                    let bounds = layout.bounds(); - -                    renderer.with_translation( -                        cursor_position -                            - Point::new( -                                bounds.x + origin.x, -                                bounds.y + origin.y, -                            ), -                        |renderer| { -                            renderer.with_layer(bounds, |renderer| { -                                pane.draw( -                                    renderer, -                                    style, -                                    layout, -                                    pane_cursor_position, -                                    viewport, -                                ); -                            }); -                        }, -                    ); -                } -                _ => { -                    pane.draw( -                        renderer, -                        style, -                        layout, -                        pane_cursor_position, -                        viewport, -                    ); -                } -            } -        } - -        if let Some((axis, split_region, is_picked)) = picked_split { -            let highlight = if is_picked { -                self.style_sheet.picked_split() -            } else { -                self.style_sheet.hovered_split() -            }; - -            if let Some(highlight) = highlight { -                renderer.fill_quad( -                    renderer::Quad { -                        bounds: match axis { -                            Axis::Horizontal => Rectangle { -                                x: split_region.x, -                                y: (split_region.y -                                    + (split_region.height - highlight.width) -                                        / 2.0) -                                    .round(), -                                width: split_region.width, -                                height: highlight.width, -                            }, -                            Axis::Vertical => Rectangle { -                                x: (split_region.x -                                    + (split_region.width - highlight.width) -                                        / 2.0) -                                    .round(), -                                y: split_region.y, -                                width: highlight.width, -                                height: split_region.height, -                            }, -                        }, -                        border_radius: 0.0, -                        border_width: 0.0, -                        border_color: Color::TRANSPARENT, -                    }, -                    highlight.color, -                ); -            } -        } +        draw( +            self.action, +            self.state, +            layout, +            cursor_position, +            renderer, +            style, +            viewport, +            self.spacing, +            self.on_resize.as_ref().map(|(leeway, _)| *leeway), +            self.style_sheet.as_ref(), +            self.elements.iter().map(|(pane, content)| (*pane, content)), +            |pane, renderer, style, layout, cursor_position, rectangle| { +                pane.draw(renderer, style, layout, cursor_position, rectangle); +            }, +        )      }      fn overlay( diff --git a/native/src/widget/pane_grid/axis.rs b/native/src/widget/pane_grid/axis.rs index 2320cb7c..02bde064 100644 --- a/native/src/widget/pane_grid/axis.rs +++ b/native/src/widget/pane_grid/axis.rs @@ -10,7 +10,9 @@ pub enum Axis {  }  impl Axis { -    pub(super) fn split( +    /// Splits the provided [`Rectangle`] on the current [`Axis`] with the +    /// given `ratio` and `spacing`. +    pub fn split(          &self,          rectangle: &Rectangle,          ratio: f32, @@ -54,7 +56,8 @@ impl Axis {          }      } -    pub(super) fn split_line_bounds( +    /// Calculates the bounds of the split line in a [`Rectangle`] region. +    pub fn split_line_bounds(          &self,          rectangle: Rectangle,          ratio: f32, diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 8b0e8d2a..f0ed0426 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -4,7 +4,7 @@ use crate::mouse;  use crate::overlay;  use crate::renderer;  use crate::widget::container; -use crate::widget::pane_grid::TitleBar; +use crate::widget::pane_grid::{Draggable, TitleBar};  use crate::{Clipboard, Element, Layout, Point, Rectangle, Shell, Size};  /// The content of a [`Pane`]. @@ -101,23 +101,6 @@ where          }      } -    /// Returns whether the [`Content`] with the given [`Layout`] can be picked -    /// at the provided cursor position. -    pub fn can_be_picked_at( -        &self, -        layout: Layout<'_>, -        cursor_position: Point, -    ) -> bool { -        if let Some(title_bar) = &self.title_bar { -            let mut children = layout.children(); -            let title_bar_layout = children.next().unwrap(); - -            title_bar.is_over_pick_area(title_bar_layout, cursor_position) -        } else { -            false -        } -    } -      pub(crate) fn layout(          &self,          renderer: &Renderer, @@ -253,6 +236,26 @@ where      }  } +impl<'a, Message, Renderer> Draggable for &Content<'a, Message, Renderer> +where +    Renderer: crate::Renderer, +{ +    fn can_be_dragged_at( +        &self, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> bool { +        if let Some(title_bar) = &self.title_bar { +            let mut children = layout.children(); +            let title_bar_layout = children.next().unwrap(); + +            title_bar.is_over_pick_area(title_bar_layout, cursor_position) +        } else { +            false +        } +    } +} +  impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer>  where      T: Into<Element<'a, Message, Renderer>>, diff --git a/native/src/widget/pane_grid/draggable.rs b/native/src/widget/pane_grid/draggable.rs new file mode 100644 index 00000000..6044871d --- /dev/null +++ b/native/src/widget/pane_grid/draggable.rs @@ -0,0 +1,12 @@ +use crate::{Layout, Point}; + +/// A pane that can be dragged. +pub trait Draggable { +    /// Returns whether the [`Draggable`] with the given [`Layout`] can be picked +    /// at the provided cursor position. +    fn can_be_dragged_at( +        &self, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> bool; +} diff --git a/native/src/widget/pane_grid/state.rs b/native/src/widget/pane_grid/state.rs index feea0dec..f9ea21f4 100644 --- a/native/src/widget/pane_grid/state.rs +++ b/native/src/widget/pane_grid/state.rs @@ -1,3 +1,4 @@ +//! The state of a [`PaneGrid`].  use crate::widget::pane_grid::{      Axis, Configuration, Direction, Node, Pane, Split,  }; @@ -19,8 +20,13 @@ use std::collections::{BTreeMap, HashMap};  /// [`PaneGrid::new`]: crate::widget::PaneGrid::new  #[derive(Debug, Clone)]  pub struct State<T> { -    pub(super) panes: HashMap<Pane, T>, -    pub(super) internal: Internal, +    /// The panes of the [`PaneGrid`]. +    pub panes: HashMap<Pane, T>, + +    /// The internal state of the [`PaneGrid`]. +    pub internal: Internal, + +    pub(super) action: Action,  }  impl<T> State<T> { @@ -39,16 +45,13 @@ impl<T> State<T> {      pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self {          let mut panes = HashMap::new(); -        let (layout, last_id) = -            Self::distribute_content(&mut panes, config.into(), 0); +        let internal = +            Internal::from_configuration(&mut panes, config.into(), 0);          State {              panes, -            internal: Internal { -                layout, -                last_id, -                action: Action::Idle, -            }, +            internal, +            action: Action::Idle,          }      } @@ -192,16 +195,34 @@ impl<T> State<T> {              None          }      } +} + +/// The internal state of a [`PaneGrid`]. +#[derive(Debug, Clone)] +pub struct Internal { +    layout: Node, +    last_id: usize, +} -    fn distribute_content( +impl Internal { +    /// Initializes the [`Internal`] state of a [`PaneGrid`] from a +    /// [`Configuration`]. +    pub fn from_configuration<T>(          panes: &mut HashMap<Pane, T>,          content: Configuration<T>,          next_id: usize, -    ) -> (Node, usize) { -        match content { +    ) -> Self { +        let (layout, last_id) = match content {              Configuration::Split { axis, ratio, a, b } => { -                let (a, next_id) = Self::distribute_content(panes, *a, next_id); -                let (b, next_id) = Self::distribute_content(panes, *b, next_id); +                let Internal { +                    layout: a, +                    last_id: next_id, +                } = Self::from_configuration(panes, *a, next_id); + +                let Internal { +                    layout: b, +                    last_id: next_id, +                } = Self::from_configuration(panes, *b, next_id);                  (                      Node::Split { @@ -220,39 +241,53 @@ impl<T> State<T> {                  (Node::Pane(id), next_id + 1)              } -        } -    } -} +        }; -#[derive(Debug, Clone)] -pub struct Internal { -    layout: Node, -    last_id: usize, -    action: Action, +        Self { layout, last_id } +    }  } +/// The current action of a [`PaneGrid`].  #[derive(Debug, Clone, Copy, PartialEq)]  pub enum Action { +    /// The [`PaneGrid`] is idle.      Idle, -    Dragging { pane: Pane, origin: Point }, -    Resizing { split: Split, axis: Axis }, +    /// A [`Pane`] in the [`PaneGrid`] is being dragged. +    Dragging { +        /// The [`Pane`] being dragged. +        pane: Pane, +        /// The starting [`Point`] of the drag interaction. +        origin: Point, +    }, +    /// A [`Split`] in the [`PaneGrid`] is being dragged. +    Resizing { +        /// The [`Split`] being dragged. +        split: Split, +        /// The [`Axis`] of the [`Split`]. +        axis: Axis, +    },  } -impl Internal { +impl Action { +    /// Returns the current [`Pane`] that is being dragged, if any.      pub fn picked_pane(&self) -> Option<(Pane, Point)> { -        match self.action { +        match *self {              Action::Dragging { pane, origin, .. } => Some((pane, origin)),              _ => None,          }      } +    /// Returns the current [`Split`] that is being dragged, if any.      pub fn picked_split(&self) -> Option<(Split, Axis)> { -        match self.action { +        match *self {              Action::Resizing { split, axis, .. } => Some((split, axis)),              _ => None,          }      } +} +impl Internal { +    /// Calculates the current [`Pane`] regions from the [`PaneGrid`] layout.      pub fn pane_regions(          &self,          spacing: f32, @@ -261,6 +296,7 @@ impl Internal {          self.layout.pane_regions(spacing, size)      } +    /// Calculates the current [`Split`] regions from the [`PaneGrid`] layout.      pub fn split_regions(          &self,          spacing: f32, @@ -268,28 +304,4 @@ impl Internal {      ) -> BTreeMap<Split, (Axis, Rectangle, f32)> {          self.layout.split_regions(spacing, size)      } - -    pub fn pick_pane(&mut self, pane: &Pane, origin: Point) { -        self.action = Action::Dragging { -            pane: *pane, -            origin, -        }; -    } - -    pub fn pick_split(&mut self, split: &Split, axis: Axis) { -        // TODO: Obtain `axis` from layout itself. Maybe we should implement -        // `Node::find_split` -        if self.picked_pane().is_some() { -            return; -        } - -        self.action = Action::Resizing { -            split: *split, -            axis, -        }; -    } - -    pub fn idle(&mut self) { -        self.action = Action::Idle; -    }  } 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/rule.rs b/native/src/widget/rule.rs index b0cc3768..69619583 100644 --- a/native/src/widget/rule.rs +++ b/native/src/widget/rule.rs @@ -15,20 +15,20 @@ pub struct Rule<'a> {  }  impl<'a> Rule<'a> { -    /// Creates a horizontal [`Rule`] for dividing content by the given vertical spacing. -    pub fn horizontal(spacing: u16) -> Self { +    /// Creates a horizontal [`Rule`] with the given height. +    pub fn horizontal(height: u16) -> Self {          Rule {              width: Length::Fill, -            height: Length::from(Length::Units(spacing)), +            height: Length::Units(height),              is_horizontal: true,              style_sheet: Default::default(),          }      } -    /// Creates a vertical [`Rule`] for dividing content by the given horizontal spacing. -    pub fn vertical(spacing: u16) -> Self { +    /// Creates a vertical [`Rule`] with the given width. +    pub fn vertical(width: u16) -> Self {          Rule { -            width: Length::from(Length::Units(spacing)), +            width: Length::from(Length::Units(width)),              height: Length::Fill,              is_horizontal: false,              style_sheet: Default::default(), diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index ce734ad8..748fd27d 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -15,6 +15,13 @@ use std::{f32, u32};  pub use iced_style::scrollable::StyleSheet; +pub mod style { +    //! The styles of a [`Scrollable`]. +    //! +    //! [`Scrollable`]: crate::widget::Scrollable +    pub use iced_style::scrollable::{Scrollbar, Scroller}; +} +  /// A widget that can vertically display an infinite amount of content with a  /// scrollbar.  #[allow(missing_debug_implementations)] @@ -139,235 +146,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 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; +    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; + +                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 +349,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 +681,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 +707,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..057f34c6 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(); @@ -164,12 +154,7 @@ where      pub fn state(&self) -> &State {          self.state      } -} -impl<'a, Message, Renderer> TextInput<'a, Message, Renderer> -where -    Renderer: text::Renderer, -{      /// Draws the [`TextInput`] with the given [`Renderer`], overriding its      /// [`Value`] if provided.      pub fn draw( @@ -179,507 +164,328 @@ where          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 bounds = layout.bounds(); -        let text_bounds = layout.children().next().unwrap().bounds(); - -        let is_mouse_over = bounds.contains(cursor_position); - -        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); -        } +        draw( +            renderer, +            layout, +            cursor_position, +            &self.state, +            value.unwrap_or(&self.value), +            &self.placeholder, +            self.size, +            &self.font, +            self.is_secure, +            self.style_sheet.as_ref(), +        )      }  } -impl<'a, Message, Renderer> Widget<Message, Renderer> -    for 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 -    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); +    let text_size = size.unwrap_or(renderer.default_size()); -                self.state.is_focused = is_clicked; +    let limits = limits +        .pad(padding) +        .width(width) +        .height(Length::Units(text_size)); -                if is_clicked { -                    let text_layout = layout.children().next().unwrap(); -                    let target = cursor_position.x - text_layout.bounds().x; +    let mut text = layout::Node::new(limits.resolve(Size::ZERO)); +    text.move_to(Point::new(padding.left.into(), padding.top.into())); -                    let click = mouse::Click::new( -                        cursor_position, -                        self.state.last_click, -                    ); +    layout::Node::with_children(text.size().pad(padding), vec![text]) +} -                    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, -                                ) +/// 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, +{ +    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 +499,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 +532,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 +554,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 +} -        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); +    } +} + +/// 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 +803,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( diff --git a/native/src/widget/toggler.rs b/native/src/widget/toggler.rs index 48237edb..536aef78 100644 --- a/native/src/widget/toggler.rs +++ b/native/src/widget/toggler.rs @@ -1,5 +1,4 @@  //! Show toggle controls using togglers. -  use crate::alignment;  use crate::event;  use crate::layout; @@ -14,7 +13,7 @@ use crate::{  pub use iced_style::toggler::{Style, StyleSheet}; -/// A toggler widget +/// A toggler widget.  ///  /// # Example  /// @@ -32,7 +31,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 +60,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,  | 
