diff options
Diffstat (limited to 'native')
| -rw-r--r-- | native/src/element.rs | 37 | ||||
| -rw-r--r-- | native/src/lib.rs | 2 | ||||
| -rw-r--r-- | native/src/overlay.rs | 84 | ||||
| -rw-r--r-- | native/src/overlay/element.rs | 170 | ||||
| -rw-r--r-- | native/src/overlay/menu.rs | 464 | ||||
| -rw-r--r-- | native/src/program/state.rs | 8 | ||||
| -rw-r--r-- | native/src/renderer.rs | 11 | ||||
| -rw-r--r-- | native/src/renderer/null.rs | 3 | ||||
| -rw-r--r-- | native/src/user_interface.rs | 203 | ||||
| -rw-r--r-- | native/src/widget.rs | 15 | ||||
| -rw-r--r-- | native/src/widget/column.rs | 15 | ||||
| -rw-r--r-- | native/src/widget/container.rs | 11 | ||||
| -rw-r--r-- | native/src/widget/pane_grid.rs | 15 | ||||
| -rw-r--r-- | native/src/widget/pane_grid/content.rs | 19 | ||||
| -rw-r--r-- | native/src/widget/pick_list.rs | 320 | ||||
| -rw-r--r-- | native/src/widget/row.rs | 15 | ||||
| -rw-r--r-- | native/src/widget/scrollable.rs | 28 | ||||
| -rw-r--r-- | native/src/widget/space.rs | 2 | 
18 files changed, 1358 insertions, 64 deletions
| diff --git a/native/src/element.rs b/native/src/element.rs index 73e39012..514a135b 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -1,5 +1,6 @@  use crate::{ -    layout, Clipboard, Color, Event, Hasher, Layout, Length, Point, Widget, +    layout, overlay, Clipboard, Color, Event, Hasher, Layout, Length, Point, +    Widget,  };  /// A generic [`Widget`]. @@ -22,7 +23,7 @@ impl<'a, Message, Renderer> Element<'a, Message, Renderer>  where      Renderer: crate::Renderer,  { -    /// Create a new [`Element`] containing the given [`Widget`]. +    /// Creates a new [`Element`] containing the given [`Widget`].      ///      /// [`Element`]: struct.Element.html      /// [`Widget`]: widget/trait.Widget.html @@ -270,6 +271,16 @@ where      pub fn hash_layout(&self, state: &mut Hasher) {          self.widget.hash_layout(state);      } + +    /// Returns the overlay of the [`Element`], if there is any. +    /// +    /// [`Element`]: struct.Element.html +    pub fn overlay<'b>( +        &'b mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'b, Message, Renderer>> { +        self.widget.overlay(layout) +    }  }  struct Map<'a, A, B, Renderer> { @@ -294,7 +305,9 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> {  impl<'a, A, B, Renderer> Widget<B, Renderer> for Map<'a, A, B, Renderer>  where -    Renderer: crate::Renderer, +    Renderer: crate::Renderer + 'a, +    A: 'static, +    B: 'static,  {      fn width(&self) -> Length {          self.widget.width() @@ -351,6 +364,17 @@ where      fn hash_layout(&self, state: &mut Hasher) {          self.widget.hash_layout(state);      } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, B, Renderer>> { +        let mapper = &self.mapper; + +        self.widget +            .overlay(layout) +            .map(move |overlay| overlay.map(mapper)) +    }  }  struct Explain<'a, Message, Renderer: crate::Renderer> { @@ -426,4 +450,11 @@ where      fn hash_layout(&self, state: &mut Hasher) {          self.element.widget.hash_layout(state);      } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        self.element.overlay(layout) +    }  } diff --git a/native/src/lib.rs b/native/src/lib.rs index b67ff2a1..067e3c0a 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -38,6 +38,7 @@  pub mod keyboard;  pub mod layout;  pub mod mouse; +pub mod overlay;  pub mod program;  pub mod renderer;  pub mod subscription; @@ -75,6 +76,7 @@ pub use element::Element;  pub use event::Event;  pub use hasher::Hasher;  pub use layout::Layout; +pub use overlay::Overlay;  pub use program::Program;  pub use renderer::Renderer;  pub use runtime::Runtime; diff --git a/native/src/overlay.rs b/native/src/overlay.rs new file mode 100644 index 00000000..7c3bec32 --- /dev/null +++ b/native/src/overlay.rs @@ -0,0 +1,84 @@ +//! Display interactive elements on top of other widgets. +mod element; + +pub mod menu; + +pub use element::Element; +pub use menu::Menu; + +use crate::{layout, Clipboard, Event, Hasher, Layout, Point, Size}; + +/// An interactive component that can be displayed on top of other widgets. +pub trait Overlay<Message, Renderer> +where +    Renderer: crate::Renderer, +{ +    /// Returns the layout [`Node`] of the [`Overlay`]. +    /// +    /// This [`Node`] is used by the runtime to compute the [`Layout`] of the +    /// user interface. +    /// +    /// [`Node`]: ../layout/struct.Node.html +    /// [`Widget`]: trait.Overlay.html +    /// [`Layout`]: ../layout/struct.Layout.html +    fn layout( +        &self, +        renderer: &Renderer, +        bounds: Size, +        position: Point, +    ) -> layout::Node; + +    /// Draws the [`Overlay`] using the associated `Renderer`. +    /// +    /// [`Overlay`]: trait.Overlay.html +    fn draw( +        &self, +        renderer: &mut Renderer, +        defaults: &Renderer::Defaults, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> Renderer::Output; + +    /// Computes the _layout_ hash of the [`Overlay`]. +    /// +    /// The produced hash is used by the runtime to decide if the [`Layout`] +    /// needs to be recomputed between frames. Therefore, to ensure maximum +    /// efficiency, the hash should only be affected by the properties of the +    /// [`Overlay`] that can affect layouting. +    /// +    /// For example, the [`Text`] widget does not hash its color property, as +    /// its value cannot affect the overall [`Layout`] of the user interface. +    /// +    /// [`Overlay`]: trait.Overlay.html +    /// [`Layout`]: ../layout/struct.Layout.html +    /// [`Text`]: text/struct.Text.html +    fn hash_layout(&self, state: &mut Hasher, position: Point); + +    /// Processes a runtime [`Event`]. +    /// +    /// It receives: +    ///   * an [`Event`] describing user interaction +    ///   * the computed [`Layout`] of the [`Overlay`] +    ///   * the current cursor position +    ///   * a mutable `Message` list, allowing the [`Overlay`] to produce +    ///   new messages based on user interaction. +    ///   * the `Renderer` +    ///   * a [`Clipboard`], if available +    /// +    /// By default, it does nothing. +    /// +    /// [`Event`]: ../enum.Event.html +    /// [`Overlay`]: trait.Widget.html +    /// [`Layout`]: ../layout/struct.Layout.html +    /// [`Clipboard`]: ../trait.Clipboard.html +    fn on_event( +        &mut self, +        _event: Event, +        _layout: Layout<'_>, +        _cursor_position: Point, +        _messages: &mut Vec<Message>, +        _renderer: &Renderer, +        _clipboard: Option<&dyn Clipboard>, +    ) { +    } +} diff --git a/native/src/overlay/element.rs b/native/src/overlay/element.rs new file mode 100644 index 00000000..e1fd9b88 --- /dev/null +++ b/native/src/overlay/element.rs @@ -0,0 +1,170 @@ +pub use crate::Overlay; + +use crate::{layout, Clipboard, Event, Hasher, Layout, Point, Size, Vector}; + +/// A generic [`Overlay`]. +/// +/// [`Overlay`]: trait.Overlay.html +#[allow(missing_debug_implementations)] +pub struct Element<'a, Message, Renderer> { +    position: Point, +    overlay: Box<dyn Overlay<Message, Renderer> + 'a>, +} + +impl<'a, Message, Renderer> Element<'a, Message, Renderer> +where +    Renderer: crate::Renderer, +{ +    /// Creates a new [`Element`] containing the given [`Overlay`]. +    /// +    /// [`Element`]: struct.Element.html +    /// [`Overlay`]: trait.Overlay.html +    pub fn new( +        position: Point, +        overlay: Box<dyn Overlay<Message, Renderer> + 'a>, +    ) -> Self { +        Self { position, overlay } +    } + +    /// Translates the [`Element`]. +    /// +    /// [`Element`]: struct.Element.html +    pub fn translate(mut self, translation: Vector) -> Self { +        self.position = self.position + translation; +        self +    } + +    /// Applies a transformation to the produced message of the [`Element`]. +    /// +    /// [`Element`]: struct.Element.html +    pub fn map<B>(self, f: &'a dyn Fn(Message) -> B) -> Element<'a, B, Renderer> +    where +        Message: 'a, +        Renderer: 'a, +        B: 'static, +    { +        Element { +            position: self.position, +            overlay: Box::new(Map::new(self.overlay, f)), +        } +    } + +    /// Computes the layout of the [`Element`] in the given bounds. +    /// +    /// [`Element`]: struct.Element.html +    pub fn layout(&self, renderer: &Renderer, bounds: Size) -> layout::Node { +        self.overlay.layout(renderer, bounds, self.position) +    } + +    /// Processes a runtime [`Event`]. +    /// +    /// [`Event`]: enum.Event.html +    pub fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        messages: &mut Vec<Message>, +        renderer: &Renderer, +        clipboard: Option<&dyn Clipboard>, +    ) { +        self.overlay.on_event( +            event, +            layout, +            cursor_position, +            messages, +            renderer, +            clipboard, +        ) +    } + +    /// Draws the [`Element`] and its children using the given [`Layout`]. +    /// +    /// [`Element`]: struct.Element.html +    /// [`Layout`]: layout/struct.Layout.html +    pub fn draw( +        &self, +        renderer: &mut Renderer, +        defaults: &Renderer::Defaults, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> Renderer::Output { +        self.overlay +            .draw(renderer, defaults, layout, cursor_position) +    } + +    /// Computes the _layout_ hash of the [`Element`]. +    /// +    /// [`Element`]: struct.Element.html +    pub fn hash_layout(&self, state: &mut Hasher) { +        self.overlay.hash_layout(state, self.position); +    } +} + +struct Map<'a, A, B, Renderer> { +    content: Box<dyn Overlay<A, Renderer> + 'a>, +    mapper: &'a dyn Fn(A) -> B, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { +    pub fn new( +        content: Box<dyn Overlay<A, Renderer> + 'a>, +        mapper: &'a dyn Fn(A) -> B, +    ) -> Map<'a, A, B, Renderer> { +        Map { content, mapper } +    } +} + +impl<'a, A, B, Renderer> Overlay<B, Renderer> for Map<'a, A, B, Renderer> +where +    Renderer: crate::Renderer, +{ +    fn layout( +        &self, +        renderer: &Renderer, +        bounds: Size, +        position: Point, +    ) -> layout::Node { +        self.content.layout(renderer, bounds, position) +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        messages: &mut Vec<B>, +        renderer: &Renderer, +        clipboard: Option<&dyn Clipboard>, +    ) { +        let mut original_messages = Vec::new(); + +        self.content.on_event( +            event, +            layout, +            cursor_position, +            &mut original_messages, +            renderer, +            clipboard, +        ); + +        original_messages +            .drain(..) +            .for_each(|message| messages.push((self.mapper)(message))); +    } + +    fn draw( +        &self, +        renderer: &mut Renderer, +        defaults: &Renderer::Defaults, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> Renderer::Output { +        self.content +            .draw(renderer, defaults, layout, cursor_position) +    } + +    fn hash_layout(&self, state: &mut Hasher, position: Point) { +        self.content.hash_layout(state, position); +    } +} diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs new file mode 100644 index 00000000..0d4bc63c --- /dev/null +++ b/native/src/overlay/menu.rs @@ -0,0 +1,464 @@ +//! Build and show dropdown menus. +use crate::{ +    container, layout, mouse, overlay, scrollable, text, Clipboard, Container, +    Element, Event, Hasher, Layout, Length, Point, Rectangle, Scrollable, Size, +    Vector, Widget, +}; + +/// A list of selectable options. +#[allow(missing_debug_implementations)] +pub struct Menu<'a, T, Message, Renderer: self::Renderer> { +    state: &'a mut State, +    options: &'a [T], +    on_selected: &'a dyn Fn(T) -> Message, +    width: u16, +    padding: u16, +    text_size: Option<u16>, +    font: Renderer::Font, +    style: <Renderer as self::Renderer>::Style, +} + +impl<'a, T, Message, Renderer> Menu<'a, T, Message, Renderer> +where +    T: ToString + Clone, +    Message: 'a, +    Renderer: self::Renderer + 'a, +{ +    /// Creates a new [`Menu`] with the given [`State`], a list of options, and +    /// the message to produced when an option is selected. +    /// +    /// [`Menu`]: struct.Menu.html +    /// [`State`]: struct.State.html +    pub fn new( +        state: &'a mut State, +        options: &'a [T], +        on_selected: &'a dyn Fn(T) -> Message, +    ) -> Self { +        Menu { +            state, +            options, +            on_selected, +            width: 0, +            padding: 0, +            text_size: None, +            font: Default::default(), +            style: Default::default(), +        } +    } + +    /// Sets the width of the [`Menu`]. +    /// +    /// [`Menu`]: struct.Menu.html +    pub fn width(mut self, width: u16) -> Self { +        self.width = width; +        self +    } + +    /// Sets the padding of the [`Menu`]. +    /// +    /// [`Menu`]: struct.Menu.html +    pub fn padding(mut self, padding: u16) -> Self { +        self.padding = padding; +        self +    } + +    /// Sets the text size of the [`Menu`]. +    /// +    /// [`Menu`]: struct.Menu.html +    pub fn text_size(mut self, text_size: u16) -> Self { +        self.text_size = Some(text_size); +        self +    } + +    /// Sets the font of the [`Menu`]. +    /// +    /// [`Menu`]: struct.Menu.html +    pub fn font(mut self, font: Renderer::Font) -> Self { +        self.font = font; +        self +    } + +    /// Sets the style of the [`Menu`]. +    /// +    /// [`Menu`]: struct.Menu.html +    pub fn style( +        mut self, +        style: impl Into<<Renderer as self::Renderer>::Style>, +    ) -> Self { +        self.style = style.into(); +        self +    } + +    /// Turns the [`Menu`] into an overlay [`Element`] at the given target +    /// position. +    /// +    /// The `target_height` will be used to display the menu either on top +    /// of the target or under it, depending on the screen position and the +    /// dimensions of the [`Menu`]. +    /// +    /// [`Menu`]: struct.Menu.html +    pub fn overlay( +        self, +        position: Point, +        target_height: f32, +    ) -> overlay::Element<'a, Message, Renderer> { +        overlay::Element::new( +            position, +            Box::new(Overlay::new(self, target_height)), +        ) +    } +} + +/// The local state of a [`Menu`]. +/// +/// [`Menu`]: struct.Menu.html +#[derive(Debug, Clone, Default)] +pub struct State { +    scrollable: scrollable::State, +    hovered_option: Option<usize>, +    is_open: bool, +} + +impl State { +    /// Returns whether the [`Menu`] is currently open or not. +    /// +    /// [`Menu`]: struct.Menu.html +    pub fn is_open(&self) -> bool { +        self.is_open +    } + +    /// Opens the [`Menu`] with the given option hovered by default. +    /// +    /// [`Menu`]: struct.Menu.html +    pub fn open(&mut self, hovered_option: Option<usize>) { +        self.is_open = true; +        self.hovered_option = hovered_option; +    } +} + +struct Overlay<'a, Message, Renderer: self::Renderer> { +    container: Container<'a, Message, Renderer>, +    is_open: &'a mut bool, +    width: u16, +    target_height: f32, +    style: <Renderer as self::Renderer>::Style, +} + +impl<'a, Message, Renderer: self::Renderer> Overlay<'a, Message, Renderer> +where +    Message: 'a, +    Renderer: 'a, +{ +    pub fn new<T>( +        menu: Menu<'a, T, Message, Renderer>, +        target_height: f32, +    ) -> Self +    where +        T: Clone + ToString, +    { +        let Menu { +            state, +            options, +            on_selected, +            width, +            padding, +            font, +            text_size, +            style, +        } = menu; + +        let container = +            Container::new(Scrollable::new(&mut state.scrollable).push(List { +                options, +                hovered_option: &mut state.hovered_option, +                on_selected, +                font, +                text_size, +                padding, +                style: style.clone(), +            })) +            .padding(1); + +        Self { +            container, +            is_open: &mut state.is_open, +            width: width, +            target_height, +            style: style, +        } +    } +} + +impl<'a, Message, Renderer> crate::Overlay<Message, Renderer> +    for Overlay<'a, Message, Renderer> +where +    Renderer: self::Renderer, +{ +    fn layout( +        &self, +        renderer: &Renderer, +        bounds: Size, +        position: Point, +    ) -> layout::Node { +        let space_below = bounds.height - (position.y + self.target_height); +        let space_above = position.y; + +        let limits = layout::Limits::new( +            Size::ZERO, +            Size::new( +                bounds.width - position.x, +                if space_below > space_above { +                    space_below +                } else { +                    space_above +                }, +            ), +        ) +        .width(Length::Units(self.width)); + +        let mut node = self.container.layout(renderer, &limits); + +        node.move_to(if space_below > space_above { +            position + Vector::new(0.0, self.target_height) +        } else { +            position - Vector::new(0.0, node.size().height) +        }); + +        node +    } + +    fn hash_layout(&self, state: &mut Hasher, position: Point) { +        use std::hash::Hash; + +        struct Marker; +        std::any::TypeId::of::<Marker>().hash(state); + +        (position.x as u32).hash(state); +        (position.y as u32).hash(state); +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        messages: &mut Vec<Message>, +        renderer: &Renderer, +        clipboard: Option<&dyn Clipboard>, +    ) { +        let bounds = layout.bounds(); +        let current_messages = messages.len(); + +        self.container.on_event( +            event.clone(), +            layout, +            cursor_position, +            messages, +            renderer, +            clipboard, +        ); + +        let option_was_selected = current_messages < messages.len(); + +        match event { +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +                if !bounds.contains(cursor_position) || option_was_selected => +            { +                *self.is_open = false; +            } +            _ => {} +        } +    } + +    fn draw( +        &self, +        renderer: &mut Renderer, +        defaults: &Renderer::Defaults, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> Renderer::Output { +        let primitives = +            self.container +                .draw(renderer, defaults, layout, cursor_position); + +        renderer.decorate( +            layout.bounds(), +            cursor_position, +            &self.style, +            primitives, +        ) +    } +} + +struct List<'a, T, Message, Renderer: self::Renderer> { +    options: &'a [T], +    hovered_option: &'a mut Option<usize>, +    on_selected: &'a dyn Fn(T) -> Message, +    padding: u16, +    text_size: Option<u16>, +    font: Renderer::Font, +    style: <Renderer as self::Renderer>::Style, +} + +impl<'a, T, Message, Renderer: self::Renderer> Widget<Message, Renderer> +    for List<'a, T, Message, Renderer> +where +    T: Clone + ToString, +    Renderer: self::Renderer, +{ +    fn width(&self) -> Length { +        Length::Fill +    } + +    fn height(&self) -> Length { +        Length::Shrink +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        use std::f32; + +        let limits = limits.width(Length::Fill).height(Length::Shrink); +        let text_size = self.text_size.unwrap_or(renderer.default_size()); + +        let size = { +            let intrinsic = Size::new( +                0.0, +                f32::from(text_size + self.padding * 2) +                    * self.options.len() as f32, +            ); + +            limits.resolve(intrinsic) +        }; + +        layout::Node::new(size) +    } + +    fn hash_layout(&self, state: &mut Hasher) { +        use std::hash::Hash as _; + +        struct Marker; +        std::any::TypeId::of::<Marker>().hash(state); + +        self.options.len().hash(state); +        self.text_size.hash(state); +        self.padding.hash(state); +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        messages: &mut Vec<Message>, +        renderer: &Renderer, +        _clipboard: Option<&dyn Clipboard>, +    ) { +        match event { +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { +                let bounds = layout.bounds(); + +                if bounds.contains(cursor_position) { +                    if let Some(index) = *self.hovered_option { +                        if let Some(option) = self.options.get(index) { +                            messages.push((self.on_selected)(option.clone())); +                        } +                    } +                } +            } +            Event::Mouse(mouse::Event::CursorMoved { .. }) => { +                let bounds = layout.bounds(); +                let text_size = +                    self.text_size.unwrap_or(renderer.default_size()); + +                if bounds.contains(cursor_position) { +                    *self.hovered_option = Some( +                        ((cursor_position.y - bounds.y) +                            / f32::from(text_size + self.padding * 2)) +                            as usize, +                    ); +                } +            } +            _ => {} +        } +    } + +    fn draw( +        &self, +        renderer: &mut Renderer, +        _defaults: &Renderer::Defaults, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> Renderer::Output { +        self::Renderer::draw( +            renderer, +            layout.bounds(), +            cursor_position, +            self.options, +            *self.hovered_option, +            self.padding, +            self.text_size.unwrap_or(renderer.default_size()), +            self.font, +            &self.style, +        ) +    } +} + +/// The renderer of a [`Menu`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`Menu`] in your user interface. +/// +/// [`Menu`]: struct.Menu.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: +    scrollable::Renderer + container::Renderer + text::Renderer +{ +    /// The [`Menu`] style supported by this renderer. +    /// +    /// [`Menu`]: struct.Menu.html +    type Style: Default + Clone; + +    /// Decorates a the list of options of a [`Menu`]. +    /// +    /// This method can be used to draw a background for the [`Menu`]. +    /// +    /// [`Menu`]: struct.Menu.html +    fn decorate( +        &mut self, +        bounds: Rectangle, +        cursor_position: Point, +        style: &<Self as Renderer>::Style, +        primitive: Self::Output, +    ) -> Self::Output; + +    /// Draws the list of options of a [`Menu`]. +    /// +    /// [`Menu`]: struct.Menu.html +    fn draw<T: ToString>( +        &mut self, +        bounds: Rectangle, +        cursor_position: Point, +        options: &[T], +        hovered_option: Option<usize>, +        padding: u16, +        text_size: u16, +        font: Self::Font, +        style: &<Self as Renderer>::Style, +    ) -> Self::Output; +} + +impl<'a, T, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for List<'a, T, Message, Renderer> +where +    T: ToString + Clone, +    Message: 'a, +    Renderer: 'a + self::Renderer, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/native/src/program/state.rs b/native/src/program/state.rs index fdc42e8b..95e6b60c 100644 --- a/native/src/program/state.rs +++ b/native/src/program/state.rs @@ -35,7 +35,7 @@ where          renderer: &mut P::Renderer,          debug: &mut Debug,      ) -> Self { -        let user_interface = build_user_interface( +        let mut user_interface = build_user_interface(              &mut program,              Cache::default(),              renderer, @@ -121,12 +121,14 @@ where          debug.event_processing_started();          let mut messages = user_interface.update( -            self.queued_events.drain(..), +            &self.queued_events,              cursor_position,              clipboard,              renderer,          );          messages.extend(self.queued_messages.drain(..)); + +        self.queued_events.clear();          debug.event_processing_finished();          if messages.is_empty() { @@ -153,7 +155,7 @@ where                      command                  })); -            let user_interface = build_user_interface( +            let mut user_interface = build_user_interface(                  &mut self.program,                  temp_cache,                  renderer, diff --git a/native/src/renderer.rs b/native/src/renderer.rs index a16df72b..d986141f 100644 --- a/native/src/renderer.rs +++ b/native/src/renderer.rs @@ -25,7 +25,7 @@ mod null;  #[cfg(debug_assertions)]  pub use null::Null; -use crate::{layout, Element}; +use crate::{layout, Element, Rectangle};  /// A component that can take the state of a user interface and produce an  /// output for its users. @@ -56,4 +56,13 @@ pub trait Renderer: Sized {      ) -> layout::Node {          element.layout(self, limits)      } + +    /// Overlays the `overlay` output with the given bounds on top of the `base` +    /// output. +    fn overlay( +        &mut self, +        base: Self::Output, +        overlay: Self::Output, +        overlay_bounds: Rectangle, +    ) -> Self::Output;  } diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 580f58f8..07f79319 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -23,6 +23,9 @@ impl Null {  impl Renderer for Null {      type Output = ();      type Defaults = (); + +    fn overlay(&mut self, _base: (), _overlay: (), _overlay_bounds: Rectangle) { +    }  }  impl column::Renderer for Null { diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index b9646043..00a290f1 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,4 +1,4 @@ -use crate::{layout, Clipboard, Element, Event, Layout, Point, Size}; +use crate::{layout, overlay, Clipboard, Element, Event, Layout, Point, Size};  use std::hash::Hasher; @@ -19,9 +19,9 @@ use std::hash::Hasher;  /// [`UserInterface`]: struct.UserInterface.html  #[allow(missing_debug_implementations)]  pub struct UserInterface<'a, Message, Renderer> { -    hash: u64,      root: Element<'a, Message, Renderer>, -    layout: layout::Node, +    base: Layer, +    overlay: Option<Layer>,      bounds: Size,  } @@ -94,25 +94,36 @@ where      ) -> Self {          let root = root.into(); -        let hash = { -            let hasher = &mut crate::Hasher::default(); -            root.hash_layout(hasher); +        let (base, overlay) = { +            let hash = { +                let hasher = &mut crate::Hasher::default(); +                root.hash_layout(hasher); -            hasher.finish() -        }; +                hasher.finish() +            }; -        let layout_is_cached = hash == cache.hash && bounds == cache.bounds; +            let layout_is_cached = +                hash == cache.base.hash && bounds == cache.bounds; -        let layout = if layout_is_cached { -            cache.layout -        } else { -            renderer.layout(&root, &layout::Limits::new(Size::ZERO, bounds)) +            let (layout, overlay) = if layout_is_cached { +                (cache.base.layout, cache.overlay) +            } else { +                ( +                    renderer.layout( +                        &root, +                        &layout::Limits::new(Size::ZERO, bounds), +                    ), +                    None, +                ) +            }; + +            (Layer { layout, hash }, overlay)          };          UserInterface { -            hash,              root, -            layout, +            base, +            overlay,              bounds,          }      } @@ -169,7 +180,7 @@ where      ///      ///     // Update the user interface      ///     let messages = user_interface.update( -    ///         events.drain(..), +    ///         &events,      ///         cursor_position,      ///         None,      ///         &renderer, @@ -185,18 +196,54 @@ where      /// ```      pub fn update(          &mut self, -        events: impl IntoIterator<Item = Event>, +        events: &[Event],          cursor_position: Point,          clipboard: Option<&dyn Clipboard>,          renderer: &Renderer,      ) -> Vec<Message> {          let mut messages = Vec::new(); +        let base_cursor = if let Some(mut overlay) = +            self.root.overlay(Layout::new(&self.base.layout)) +        { +            let layer = Self::overlay_layer( +                self.overlay.take(), +                self.bounds, +                &mut overlay, +                renderer, +            ); + +            for event in events { +                overlay.on_event( +                    event.clone(), +                    Layout::new(&layer.layout), +                    cursor_position, +                    &mut messages, +                    renderer, +                    clipboard, +                ); +            } + +            let base_cursor = if layer.layout.bounds().contains(cursor_position) +            { +                // TODO: Type-safe cursor availability +                Point::new(-1.0, -1.0) +            } else { +                cursor_position +            }; + +            self.overlay = Some(layer); + +            base_cursor +        } else { +            cursor_position +        }; +          for event in events {              self.root.widget.on_event( -                event, -                Layout::new(&self.layout), -                cursor_position, +                event.clone(), +                Layout::new(&self.base.layout), +                base_cursor,                  &mut messages,                  renderer,                  clipboard, @@ -256,7 +303,7 @@ where      ///     );      ///      ///     let messages = user_interface.update( -    ///         events.drain(..), +    ///         &events,      ///         cursor_position,      ///         None,      ///         &renderer, @@ -276,16 +323,63 @@ where      /// }      /// ```      pub fn draw( -        &self, +        &mut self,          renderer: &mut Renderer,          cursor_position: Point,      ) -> Renderer::Output { -        self.root.widget.draw( -            renderer, -            &Renderer::Defaults::default(), -            Layout::new(&self.layout), -            cursor_position, -        ) +        let overlay = if let Some(mut overlay) = +            self.root.overlay(Layout::new(&self.base.layout)) +        { +            let layer = Self::overlay_layer( +                self.overlay.take(), +                self.bounds, +                &mut overlay, +                renderer, +            ); + +            let overlay_bounds = layer.layout.bounds(); + +            let overlay_primitives = overlay.draw( +                renderer, +                &Renderer::Defaults::default(), +                Layout::new(&layer.layout), +                cursor_position, +            ); + +            self.overlay = Some(layer); + +            Some((overlay_primitives, overlay_bounds)) +        } else { +            None +        }; + +        if let Some((overlay_primitives, overlay_bounds)) = overlay { +            let base_cursor = if overlay_bounds.contains(cursor_position) { +                Point::new(-1.0, -1.0) +            } else { +                cursor_position +            }; + +            let base_primitives = self.root.widget.draw( +                renderer, +                &Renderer::Defaults::default(), +                Layout::new(&self.base.layout), +                base_cursor, +            ); + +            renderer.overlay( +                base_primitives, +                overlay_primitives, +                overlay_bounds, +            ) +        } else { +            self.root.widget.draw( +                renderer, +                &Renderer::Defaults::default(), +                Layout::new(&self.base.layout), +                cursor_position, +            ) +        }      }      /// Extract the [`Cache`] of the [`UserInterface`], consuming it in the @@ -295,11 +389,41 @@ where      /// [`UserInterface`]: struct.UserInterface.html      pub fn into_cache(self) -> Cache {          Cache { -            hash: self.hash, -            layout: self.layout, +            base: self.base, +            overlay: self.overlay,              bounds: self.bounds,          }      } + +    fn overlay_layer( +        cache: Option<Layer>, +        bounds: Size, +        overlay: &mut overlay::Element<'_, Message, Renderer>, +        renderer: &Renderer, +    ) -> Layer { +        let new_hash = { +            let hasher = &mut crate::Hasher::default(); +            overlay.hash_layout(hasher); + +            hasher.finish() +        }; + +        let layout = match cache { +            Some(Layer { hash, layout }) if new_hash == hash => layout, +            _ => overlay.layout(renderer, bounds), +        }; + +        Layer { +            layout, +            hash: new_hash, +        } +    } +} + +#[derive(Debug, Clone)] +struct Layer { +    layout: layout::Node, +    hash: u64,  }  /// Reusable data of a specific [`UserInterface`]. @@ -307,8 +431,8 @@ where  /// [`UserInterface`]: struct.UserInterface.html  #[derive(Debug, Clone)]  pub struct Cache { -    hash: u64, -    layout: layout::Node, +    base: Layer, +    overlay: Option<Layer>,      bounds: Size,  } @@ -322,8 +446,11 @@ impl Cache {      /// [`UserInterface`]: struct.UserInterface.html      pub fn new() -> Cache {          Cache { -            hash: 0, -            layout: layout::Node::new(Size::new(0.0, 0.0)), +            base: Layer { +                layout: layout::Node::new(Size::new(0.0, 0.0)), +                hash: 0, +            }, +            overlay: None,              bounds: Size::ZERO,          }      } @@ -334,11 +461,3 @@ impl Default for Cache {          Cache::new()      }  } - -impl PartialEq for Cache { -    fn eq(&self, other: &Cache) -> bool { -        self.hash == other.hash -    } -} - -impl Eq for Cache {} diff --git a/native/src/widget.rs b/native/src/widget.rs index 4453145b..8539e519 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -26,6 +26,7 @@ pub mod column;  pub mod container;  pub mod image;  pub mod pane_grid; +pub mod pick_list;  pub mod progress_bar;  pub mod radio;  pub mod row; @@ -49,6 +50,8 @@ pub use image::Image;  #[doc(no_inline)]  pub use pane_grid::PaneGrid;  #[doc(no_inline)] +pub use pick_list::PickList; +#[doc(no_inline)]  pub use progress_bar::ProgressBar;  #[doc(no_inline)]  pub use radio::Radio; @@ -67,7 +70,7 @@ pub use text::Text;  #[doc(no_inline)]  pub use text_input::TextInput; -use crate::{layout, Clipboard, Event, Hasher, Layout, Length, Point}; +use crate::{layout, overlay, Clipboard, Event, Hasher, Layout, Length, Point};  /// A component that displays information and allows interaction.  /// @@ -175,4 +178,14 @@ where          _clipboard: Option<&dyn Clipboard>,      ) {      } + +    /// Returns the overlay of the [`Element`], if there is any. +    /// +    /// [`Element`]: struct.Element.html +    fn overlay( +        &mut self, +        _layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        None +    }  } diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index 259a7e6e..42cfe9b9 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -2,8 +2,8 @@  use std::hash::Hash;  use crate::{ -    layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, -    Widget, +    layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, +    Point, Widget,  };  use std::u32; @@ -204,6 +204,17 @@ where              child.widget.hash_layout(state);          }      } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        self.children +            .iter_mut() +            .zip(layout.children()) +            .filter_map(|(child, layout)| child.widget.overlay(layout)) +            .next() +    }  }  /// The renderer of a [`Column`]. diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index 2590fe3b..b8316e62 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -2,8 +2,8 @@  use std::hash::Hash;  use crate::{ -    layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, -    Rectangle, Widget, +    layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, +    Point, Rectangle, Widget,  };  use std::u32; @@ -214,6 +214,13 @@ where          self.content.hash_layout(state);      } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        self.content.overlay(layout.children().next().unwrap()) +    }  }  /// The renderer of a [`Container`]. diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index c472d043..0b237b9e 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -29,8 +29,8 @@ pub use state::{Focus, State};  pub use title_bar::TitleBar;  use crate::{ -    container, keyboard, layout, mouse, row, text, Clipboard, Element, Event, -    Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget, +    container, keyboard, layout, mouse, overlay, row, text, Clipboard, Element, +    Event, Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget,  };  /// A collection of panes distributed using either vertical or horizontal splits @@ -636,6 +636,17 @@ where              element.hash_layout(state);          }      } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        self.elements +            .iter_mut() +            .zip(layout.children()) +            .filter_map(|((_, pane), layout)| pane.overlay(layout)) +            .next() +    }  }  /// The renderer of a [`PaneGrid`]. diff --git a/native/src/widget/pane_grid/content.rs b/native/src/widget/pane_grid/content.rs index 1f5ce640..39a92186 100644 --- a/native/src/widget/pane_grid/content.rs +++ b/native/src/widget/pane_grid/content.rs @@ -1,5 +1,6 @@  use crate::container;  use crate::layout; +use crate::overlay;  use crate::pane_grid::{self, TitleBar};  use crate::{Clipboard, Element, Event, Hasher, Layout, Point, Size}; @@ -184,6 +185,24 @@ where      pub(crate) fn hash_layout(&self, state: &mut Hasher) {          self.body.hash_layout(state);      } + +    pub(crate) fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        let body_layout = if self.title_bar.is_some() { +            let mut children = layout.children(); + +            // Overlays only allowed in the pane body, for now at least. +            let _title_bar_layout = children.next(); + +            children.next()? +        } else { +            layout +        }; + +        self.body.overlay(body_layout) +    }  }  impl<'a, T, Message, Renderer> From<T> for Content<'a, Message, Renderer> diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs new file mode 100644 index 00000000..9f62e550 --- /dev/null +++ b/native/src/widget/pick_list.rs @@ -0,0 +1,320 @@ +//! Display a dropdown list of selectable values. +use crate::{ +    layout, mouse, overlay, +    overlay::menu::{self, Menu}, +    scrollable, text, Clipboard, Element, Event, Hasher, Layout, Length, Point, +    Rectangle, Size, Widget, +}; +use std::borrow::Cow; + +/// A widget for selecting a single value from a list of options. +#[allow(missing_debug_implementations)] +pub struct PickList<'a, T, Message, Renderer: self::Renderer> +where +    [T]: ToOwned<Owned = Vec<T>>, +{ +    menu: &'a mut menu::State, +    on_selected: Box<dyn Fn(T) -> Message>, +    options: Cow<'a, [T]>, +    selected: Option<T>, +    width: Length, +    padding: u16, +    text_size: Option<u16>, +    font: Renderer::Font, +    style: <Renderer as self::Renderer>::Style, +    is_open: bool, +} + +/// The local state of a [`PickList`]. +/// +/// [`PickList`]: struct.PickList.html +#[derive(Debug, Clone, Default)] +pub struct State { +    menu: menu::State, +} + +impl<'a, T: 'a, Message, Renderer: self::Renderer> +    PickList<'a, T, Message, Renderer> +where +    T: ToString, +    [T]: ToOwned<Owned = Vec<T>>, +{ +    /// Creates a new [`PickList`] with the given [`State`], a list of options, +    /// the current selected value, and the message to produce when an option is +    /// selected. +    /// +    /// [`PickList`]: struct.PickList.html +    /// [`State`]: struct.State.html +    pub fn new( +        state: &'a mut State, +        options: impl Into<Cow<'a, [T]>>, +        selected: Option<T>, +        on_selected: impl Fn(T) -> Message + 'static, +    ) -> Self { +        let is_open = state.menu.is_open(); + +        Self { +            menu: &mut state.menu, +            on_selected: Box::new(on_selected), +            options: options.into(), +            selected, +            width: Length::Shrink, +            text_size: None, +            padding: Renderer::DEFAULT_PADDING, +            font: Default::default(), +            style: Default::default(), +            is_open, +        } +    } + +    /// Sets the width of the [`PickList`]. +    /// +    /// [`PickList`]: struct.PickList.html +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    /// Sets the padding of the [`PickList`]. +    /// +    /// [`PickList`]: struct.PickList.html +    pub fn padding(mut self, padding: u16) -> Self { +        self.padding = padding; +        self +    } + +    /// Sets the text size of the [`PickList`]. +    /// +    /// [`PickList`]: struct.PickList.html +    pub fn text_size(mut self, size: u16) -> Self { +        self.text_size = Some(size); +        self +    } + +    /// Sets the font of the [`PickList`]. +    /// +    /// [`PickList`]: struct.PickList.html +    pub fn font(mut self, font: Renderer::Font) -> Self { +        self.font = font; +        self +    } + +    /// Sets the style of the [`PickList`]. +    /// +    /// [`PickList`]: struct.PickList.html +    pub fn style( +        mut self, +        style: impl Into<<Renderer as self::Renderer>::Style>, +    ) -> Self { +        self.style = style.into(); +        self +    } +} + +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: self::Renderer + scrollable::Renderer + 'a, +{ +    fn width(&self) -> Length { +        Length::Shrink +    } + +    fn height(&self) -> Length { +        Length::Shrink +    } + +    fn layout( +        &self, +        renderer: &Renderer, +        limits: &layout::Limits, +    ) -> layout::Node { +        use std::f32; + +        let limits = limits +            .width(self.width) +            .height(Length::Shrink) +            .pad(f32::from(self.padding)); + +        let text_size = self.text_size.unwrap_or(renderer.default_size()); + +        let max_width = match self.width { +            Length::Shrink => { +                let labels = self.options.iter().map(ToString::to_string); + +                labels +                    .map(|label| { +                        let (width, _) = renderer.measure( +                            &label, +                            text_size, +                            Renderer::Font::default(), +                            Size::new(f32::INFINITY, f32::INFINITY), +                        ); + +                        width.round() as u32 +                    }) +                    .max() +                    .unwrap_or(100) +            } +            _ => 0, +        }; + +        let size = { +            let intrinsic = Size::new( +                max_width as f32 +                    + f32::from(text_size) +                    + f32::from(self.padding), +                f32::from(text_size), +            ); + +            limits.resolve(intrinsic).pad(f32::from(self.padding)) +        }; + +        layout::Node::new(size) +    } + +    fn hash_layout(&self, state: &mut Hasher) { +        use std::hash::Hash as _; + +        match self.width { +            Length::Shrink => { +                self.options +                    .iter() +                    .map(ToString::to_string) +                    .for_each(|label| label.hash(state)); +            } +            _ => { +                self.width.hash(state); +            } +        } +    } + +    fn on_event( +        &mut self, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        _messages: &mut Vec<Message>, +        _renderer: &Renderer, +        _clipboard: Option<&dyn Clipboard>, +    ) { +        if !self.is_open { +            match event { +                Event::Mouse(mouse::Event::ButtonPressed( +                    mouse::Button::Left, +                )) => { +                    if layout.bounds().contains(cursor_position) { +                        let selected = self.selected.as_ref(); + +                        self.menu.open( +                            self.options +                                .iter() +                                .position(|option| Some(option) == selected), +                        ); +                    } +                } +                _ => {} +            } +        } +    } + +    fn draw( +        &self, +        renderer: &mut Renderer, +        _defaults: &Renderer::Defaults, +        layout: Layout<'_>, +        cursor_position: Point, +    ) -> Renderer::Output { +        self::Renderer::draw( +            renderer, +            layout.bounds(), +            cursor_position, +            self.selected.as_ref().map(ToString::to_string), +            self.padding, +            self.text_size.unwrap_or(renderer.default_size()), +            self.font, +            &self.style, +        ) +    } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        if self.menu.is_open() { +            let bounds = layout.bounds(); + +            let mut menu = +                Menu::new(&mut self.menu, &self.options, &self.on_selected) +                    .width(bounds.width.round() as u16) +                    .padding(self.padding) +                    .font(self.font) +                    .style(Renderer::menu_style(&self.style)); + +            if let Some(text_size) = self.text_size { +                menu = menu.text_size(text_size); +            } + +            Some(menu.overlay(layout.position(), bounds.height)) +        } else { +            None +        } +    } +} + +/// The renderer of a [`PickList`]. +/// +/// Your [renderer] will need to implement this trait before being +/// able to use a [`PickList`] in your user interface. +/// +/// [`PickList`]: struct.PickList.html +/// [renderer]: ../../renderer/index.html +pub trait Renderer: text::Renderer + menu::Renderer { +    /// The default padding of a [`PickList`]. +    /// +    /// [`PickList`]: struct.PickList.html +    const DEFAULT_PADDING: u16; + +    /// The [`PickList`] style supported by this renderer. +    /// +    /// [`PickList`]: struct.PickList.html +    type Style: Default; + +    /// Returns the style of the [`Menu`] of the [`PickList`]. +    /// +    /// [`Menu`]: ../../overlay/menu/struct.Menu.html +    /// [`PickList`]: struct.PickList.html +    fn menu_style( +        style: &<Self as Renderer>::Style, +    ) -> <Self as menu::Renderer>::Style; + +    /// Draws a [`PickList`]. +    /// +    /// [`PickList`]: struct.PickList.html +    fn draw( +        &mut self, +        bounds: Rectangle, +        cursor_position: Point, +        selected: Option<String>, +        padding: u16, +        text_size: u16, +        font: Self::Font, +        style: &<Self as Renderer>::Style, +    ) -> Self::Output; +} + +impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for PickList<'a, T, Message, Renderer> +where +    T: Clone + ToString + Eq, +    [T]: ToOwned<Owned = Vec<T>>, +    Renderer: self::Renderer + 'a, +    Message: 'static, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index 31f7472f..2b6db224 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -2,8 +2,8 @@  use std::hash::Hash;  use crate::{ -    layout, Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, -    Widget, +    layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, +    Point, Widget,  };  use std::u32; @@ -206,6 +206,17 @@ where              child.widget.hash_layout(state);          }      } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        self.children +            .iter_mut() +            .zip(layout.children()) +            .filter_map(|(child, layout)| child.widget.overlay(layout)) +            .next() +    }  }  /// The renderer of a [`Row`]. diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 3c8e5e5b..75e97027 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -1,7 +1,7 @@  //! Navigate an endless amount of content with a scrollbar.  use crate::{ -    column, layout, mouse, Align, Clipboard, Column, Element, Event, Hasher, -    Layout, Length, Point, Rectangle, Size, Widget, +    column, layout, mouse, overlay, Align, Clipboard, Column, Element, Event, +    Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget,  };  use std::{f32, hash::Hash, u32}; @@ -113,7 +113,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {  impl<'a, Message, Renderer> Widget<Message, Renderer>      for Scrollable<'a, Message, Renderer>  where -    Renderer: self::Renderer + column::Renderer, +    Renderer: self::Renderer,  {      fn width(&self) -> Length {          Widget::<Message, Renderer>::width(&self.content) @@ -315,6 +315,24 @@ where          self.content.hash_layout(state)      } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<overlay::Element<'_, Message, Renderer>> { +        let Self { content, state, .. } = self; + +        content +            .overlay(layout.children().next().unwrap()) +            .map(|overlay| { +                let bounds = layout.bounds(); +                let content_layout = layout.children().next().unwrap(); +                let content_bounds = content_layout.bounds(); +                let offset = state.offset(bounds, content_bounds); + +                overlay.translate(Vector::new(0.0, -(offset as f32))) +            }) +    }  }  /// The local state of a [`Scrollable`]. @@ -454,7 +472,7 @@ pub struct Scroller {  ///  /// [`Scrollable`]: struct.Scrollable.html  /// [renderer]: ../../renderer/index.html -pub trait Renderer: crate::Renderer + Sized { +pub trait Renderer: column::Renderer + Sized {      /// The style supported by this renderer.      type Style: Default; @@ -502,7 +520,7 @@ pub trait Renderer: crate::Renderer + Sized {  impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>      for Element<'a, Message, Renderer>  where -    Renderer: 'a + self::Renderer + column::Renderer, +    Renderer: 'a + self::Renderer,      Message: 'a,  {      fn from( diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index e56a8fe1..f1576ffb 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -43,7 +43,7 @@ impl Space {      }  } -impl<'a, Message, Renderer> Widget<Message, Renderer> for Space +impl<Message, Renderer> Widget<Message, Renderer> for Space  where      Renderer: self::Renderer,  { | 
