diff options
Diffstat (limited to '')
| -rw-r--r-- | native/src/element.rs | 25 | ||||
| -rw-r--r-- | native/src/layer.rs | 5 | ||||
| -rw-r--r-- | native/src/layer/menu.rs | 305 | ||||
| -rw-r--r-- | native/src/lib.rs | 4 | ||||
| -rw-r--r-- | native/src/overlay.rs | 100 | ||||
| -rw-r--r-- | native/src/user_interface.rs | 16 | ||||
| -rw-r--r-- | native/src/widget.rs | 3 | ||||
| -rw-r--r-- | native/src/widget/combo_box.rs | 256 | ||||
| -rw-r--r-- | native/src/widget/scrollable.rs | 6 | 
9 files changed, 698 insertions, 22 deletions
| diff --git a/native/src/element.rs b/native/src/element.rs index 01379d2d..c881871a 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -2,6 +2,7 @@ use crate::{      layout, Clipboard, Color, Event, Hasher, Layout, Length, Overlay, Point,      Widget,  }; +use std::rc::Rc;  /// A generic [`Widget`].  /// @@ -282,7 +283,7 @@ where  struct Map<'a, A, B, Renderer> {      widget: Box<dyn Widget<'a, A, Renderer> + 'a>, -    mapper: Box<dyn Fn(A) -> B>, +    mapper: Rc<dyn Fn(A) -> B>,  }  impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { @@ -295,14 +296,16 @@ impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> {      {          Map {              widget, -            mapper: Box::new(mapper), +            mapper: Rc::new(mapper),          }      }  }  impl<'a, A, B, Renderer> Widget<'a, 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() @@ -359,6 +362,15 @@ where      fn hash_layout(&self, state: &mut Hasher) {          self.widget.hash_layout(state);      } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<Overlay<'a, B, Renderer>> { +        self.widget +            .overlay(layout) +            .map(|overlay| overlay.map(self.mapper.clone())) +    }  }  struct Explain<'a, Message, Renderer: crate::Renderer> { @@ -434,4 +446,11 @@ where      fn hash_layout(&self, state: &mut Hasher) {          self.element.widget.hash_layout(state);      } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<Overlay<'a, Message, Renderer>> { +        self.element.overlay(layout) +    }  } diff --git a/native/src/layer.rs b/native/src/layer.rs index d89fb4e5..eacfe94b 100644 --- a/native/src/layer.rs +++ b/native/src/layer.rs @@ -1,3 +1,8 @@ +pub mod menu; + +#[doc(no_inline)] +pub use menu::Menu; +  use crate::{layout, Clipboard, Event, Hasher, Layout, Point, Size};  pub trait Layer<Message, Renderer> diff --git a/native/src/layer/menu.rs b/native/src/layer/menu.rs new file mode 100644 index 00000000..9e26767b --- /dev/null +++ b/native/src/layer/menu.rs @@ -0,0 +1,305 @@ +use crate::{ +    container, layout, mouse, scrollable, Clipboard, Container, Element, Event, +    Hasher, Layer, Layout, Length, Point, Rectangle, Scrollable, Size, Widget, +}; +use std::borrow::Cow; + +pub struct Menu<'a, Message, Renderer: self::Renderer> { +    container: Container<'a, Message, Renderer>, +    is_open: &'a mut bool, +    width: u16, +} + +#[derive(Default)] +pub struct State { +    scrollable: scrollable::State, +    hovered_option: Option<usize>, +    is_open: bool, +} + +impl State { +    pub fn is_open(&self) -> bool { +        self.is_open +    } + +    pub fn open(&mut self, hovered_option: Option<usize>) { +        self.is_open = true; +        self.hovered_option = hovered_option; +    } +} + +impl<'a, Message, Renderer: self::Renderer> Menu<'a, Message, Renderer> +where +    Message: 'static, +    Renderer: 'a, +{ +    pub fn new<T: 'a>( +        state: &'a mut State, +        options: impl Into<Cow<'a, [T]>>, +        on_selected: Box<dyn Fn(T) -> Message>, +        width: u16, +        text_size: u16, +        padding: u16, +    ) -> Self +    where +        T: Clone + ToString, +        [T]: ToOwned<Owned = Vec<T>>, +    { +        let container = Container::new( +            Scrollable::new(&mut state.scrollable).push(List::new( +                &mut state.hovered_option, +                options, +                on_selected, +                text_size, +                padding, +            )), +        ) +        .padding(1); + +        Self { +            container, +            is_open: &mut state.is_open, +            width, +        } +    } +} + +impl<'a, Message, Renderer> Layer<Message, Renderer> +    for Menu<'a, Message, Renderer> +where +    Renderer: self::Renderer, +{ +    fn layout( +        &self, +        renderer: &Renderer, +        bounds: Size, +        position: Point, +    ) -> layout::Node { +        let limits = layout::Limits::new( +            Size::ZERO, +            Size::new(bounds.width - position.x, bounds.height - position.y), +        ) +        .width(Length::Units(self.width)); + +        let mut node = self.container.layout(renderer, &limits); + +        node.move_to(position); + +        node +    } + +    fn hash_layout(&self, state: &mut Hasher, position: Point) { +        use std::hash::Hash; + +        (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, +        ); + +        match event { +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) +                if !bounds.contains(cursor_position) +                    || current_messages < messages.len() => +            { +                *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, primitives) +    } +} + +struct List<'a, T, Message> +where +    [T]: ToOwned, +{ +    hovered_option: &'a mut Option<usize>, +    options: Cow<'a, [T]>, +    on_selected: Box<dyn Fn(T) -> Message>, +    text_size: u16, +    padding: u16, +} + +impl<'a, T, Message> List<'a, T, Message> +where +    [T]: ToOwned, +{ +    pub fn new( +        hovered_option: &'a mut Option<usize>, +        options: impl Into<Cow<'a, [T]>>, +        on_selected: Box<dyn Fn(T) -> Message>, +        text_size: u16, +        padding: u16, +    ) -> Self { +        List { +            hovered_option, +            options: options.into(), +            on_selected, +            text_size, +            padding, +        } +    } +} + +impl<'a, T, Message, Renderer> Widget<'a, Message, Renderer> +    for List<'a, T, Message> +where +    T: ToString + Clone, +    [T]: ToOwned, +    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 size = { +            let intrinsic = Size::new( +                0.0, +                f32::from(self.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 _; + +        0.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(); + +                if bounds.contains(cursor_position) { +                    *self.hovered_option = Some( +                        ((cursor_position.y - bounds.y) +                            / f32::from(self.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.text_size, +            self.padding, +        ) +    } +} + +pub trait Renderer: scrollable::Renderer + container::Renderer { +    fn decorate( +        &mut self, +        bounds: Rectangle, +        cursor_position: Point, +        primitive: Self::Output, +    ) -> Self::Output; + +    fn draw<T: ToString>( +        &mut self, +        bounds: Rectangle, +        cursor_position: Point, +        options: &[T], +        hovered_option: Option<usize>, +        text_size: u16, +        padding: u16, +    ) -> Self::Output; +} + +impl<'a, T, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for List<'a, T, Message> +where +    T: ToString + Clone, +    [T]: ToOwned, +    Message: 'static, +    Renderer: self::Renderer, +{ +    fn into(self) -> Element<'a, Message, Renderer> { +        Element::new(self) +    } +} diff --git a/native/src/lib.rs b/native/src/lib.rs index 6974c2bd..99d80126 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -31,11 +31,12 @@  //! [`UserInterface`]: struct.UserInterface.html  //! [renderer]: renderer/index.html  //#![deny(missing_docs)] -#![deny(missing_debug_implementations)] +//#![deny(missing_debug_implementations)]  #![deny(unused_results)]  #![forbid(unsafe_code)]  #![forbid(rust_2018_idioms)]  pub mod keyboard; +pub mod layer;  pub mod layout;  pub mod mouse;  pub mod program; @@ -48,7 +49,6 @@ mod clipboard;  mod element;  mod event;  mod hasher; -mod layer;  mod overlay;  mod runtime;  mod user_interface; diff --git a/native/src/overlay.rs b/native/src/overlay.rs index a4bd5ea3..d7a1e082 100644 --- a/native/src/overlay.rs +++ b/native/src/overlay.rs @@ -1,4 +1,5 @@  use crate::{layout, Clipboard, Event, Hasher, Layer, Layout, Point, Size}; +use std::rc::Rc;  #[allow(missing_debug_implementations)]  pub struct Overlay<'a, Message, Renderer> { @@ -17,6 +18,18 @@ where          Self { position, layer }      } +    pub fn map<B>(self, f: Rc<dyn Fn(Message) -> B>) -> Overlay<'a, B, Renderer> +    where +        Message: 'static, +        Renderer: 'a, +        B: 'static, +    { +        Overlay { +            position: self.position, +            layer: Box::new(Map::new(self.layer, f)), +        } +    } +      pub fn layout(&self, renderer: &Renderer, bounds: Size) -> layout::Node {          self.layer.layout(renderer, bounds, self.position)      } @@ -37,12 +50,87 @@ where      pub fn on_event(          &mut self, -        _event: Event, -        _layout: Layout<'_>, -        _cursor_position: Point, -        _messages: &mut Vec<Message>, -        _renderer: &Renderer, -        _clipboard: Option<&dyn Clipboard>, +        event: Event, +        layout: Layout<'_>, +        cursor_position: Point, +        messages: &mut Vec<Message>, +        renderer: &Renderer, +        clipboard: Option<&dyn Clipboard>,      ) { +        self.layer.on_event( +            event, +            layout, +            cursor_position, +            messages, +            renderer, +            clipboard, +        ) +    } +} + +struct Map<'a, A, B, Renderer> { +    layer: Box<dyn Layer<A, Renderer> + 'a>, +    mapper: Rc<dyn Fn(A) -> B>, +} + +impl<'a, A, B, Renderer> Map<'a, A, B, Renderer> { +    pub fn new( +        layer: Box<dyn Layer<A, Renderer> + 'a>, +        mapper: Rc<dyn Fn(A) -> B + 'static>, +    ) -> Map<'a, A, B, Renderer> { +        Map { layer, mapper } +    } +} + +impl<'a, A, B, Renderer> Layer<B, Renderer> for Map<'a, A, B, Renderer> +where +    Renderer: crate::Renderer, +{ +    fn layout( +        &self, +        renderer: &Renderer, +        bounds: Size, +        position: Point, +    ) -> layout::Node { +        self.layer.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.layer.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.layer.draw(renderer, defaults, layout, cursor_position) +    } + +    fn hash_layout(&self, state: &mut Hasher, position: Point) { +        self.layer.hash_layout(state, position);      }  } diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 6758bce3..12cea684 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -217,6 +217,14 @@ where          for event in events {              if let Some(overlay) = &mut self.overlay { +                let base_cursor = +                    if overlay.layout.bounds().contains(cursor_position) { +                        // TODO: Encode cursor availability +                        Point::new(-1.0, -1.0) +                    } else { +                        cursor_position +                    }; +                  overlay.root.on_event(                      event.clone(),                      Layout::new(&overlay.layout), @@ -226,14 +234,6 @@ where                      clipboard,                  ); -                let base_cursor = -                    if overlay.layout.bounds().contains(cursor_position) { -                        // TODO: Encode cursor availability -                        Point::new(-1.0, -1.0) -                    } else { -                        cursor_position -                    }; -                  self.base.root.widget.on_event(                      event,                      Layout::new(&self.base.layout), diff --git a/native/src/widget.rs b/native/src/widget.rs index 0494636f..664a0cfd 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -23,6 +23,7 @@  pub mod button;  pub mod checkbox;  pub mod column; +pub mod combo_box;  pub mod container;  pub mod image;  pub mod pane_grid; @@ -43,6 +44,8 @@ pub use checkbox::Checkbox;  #[doc(no_inline)]  pub use column::Column;  #[doc(no_inline)] +pub use combo_box::ComboBox; +#[doc(no_inline)]  pub use container::Container;  #[doc(no_inline)]  pub use image::Image; diff --git a/native/src/widget/combo_box.rs b/native/src/widget/combo_box.rs new file mode 100644 index 00000000..0b25b836 --- /dev/null +++ b/native/src/widget/combo_box.rs @@ -0,0 +1,256 @@ +use crate::{ +    layer::{self, menu}, +    layout, mouse, scrollable, text, Clipboard, Element, Event, Hasher, Layout, +    Length, Overlay, Point, Rectangle, Size, Vector, Widget, +}; +use std::borrow::Cow; + +pub struct ComboBox<'a, T, Message> +where +    [T]: ToOwned<Owned = Vec<T>>, +{ +    internal: Option<Internal<'a, T, Message>>, +    options: Cow<'a, [T]>, +    selected: Option<T>, +    width: Length, +    padding: u16, +    text_size: Option<u16>, +} + +#[derive(Default)] +pub struct State { +    menu: menu::State, +} + +pub struct Internal<'a, T, Message> { +    menu: &'a mut menu::State, +    on_selected: Box<dyn Fn(T) -> Message>, +} + +impl<'a, T: 'a, Message> ComboBox<'a, T, Message> +where +    T: ToString, +    [T]: ToOwned<Owned = Vec<T>>, +{ +    pub fn new( +        state: &'a mut State, +        options: impl Into<Cow<'a, [T]>>, +        selected: Option<T>, +        on_selected: impl Fn(T) -> Message + 'static, +    ) -> Self { +        Self { +            internal: Some(Internal { +                menu: &mut state.menu, +                on_selected: Box::new(on_selected), +            }), +            options: options.into(), +            selected, +            width: Length::Shrink, +            text_size: None, +            padding: 5, +        } +    } + +    /// Sets the width of the [`ComboBox`]. +    /// +    /// [`ComboBox`]: struct.Button.html +    pub fn width(mut self, width: Length) -> Self { +        self.width = width; +        self +    } + +    /// Sets the padding of the [`ComboBox`]. +    /// +    /// [`ComboBox`]: struct.Button.html +    pub fn padding(mut self, padding: u16) -> Self { +        self.padding = padding; +        self +    } + +    pub fn text_size(mut self, size: u16) -> Self { +        self.text_size = Some(size); +        self +    } +} + +impl<'a, T: 'a, Message, Renderer> Widget<'a, Message, Renderer> +    for ComboBox<'a, T, Message> +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(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>, +    ) { +        match event { +            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { +                if let Some(internal) = &mut self.internal { +                    if layout.bounds().contains(cursor_position) { +                        let selected = self.selected.as_ref(); + +                        internal.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.text_size.unwrap_or(renderer.default_size()), +            self.padding, +        ) +    } + +    fn overlay( +        &mut self, +        layout: Layout<'_>, +    ) -> Option<Overlay<'a, Message, Renderer>> { +        let is_open = self +            .internal +            .as_ref() +            .map(|internal| internal.menu.is_open()) +            .unwrap_or(false); + +        if is_open { +            if let Some(Internal { menu, on_selected }) = self.internal.take() { +                Some(Overlay::new( +                    layout.position() +                        + Vector::new(0.0, layout.bounds().height), +                    Box::new(layer::Menu::new( +                        menu, +                        self.options.clone(), +                        on_selected, +                        layout.bounds().width.round() as u16, +                        self.text_size.unwrap_or(20), +                        self.padding, +                    )), +                )) +            } else { +                None +            } +        } else { +            None +        } +    } +} + +pub trait Renderer: text::Renderer + menu::Renderer { +    const DEFAULT_PADDING: u16; + +    fn draw( +        &mut self, +        bounds: Rectangle, +        cursor_position: Point, +        selected: Option<String>, +        text_size: u16, +        padding: u16, +    ) -> Self::Output; +} + +impl<'a, T: 'a, Message, Renderer> Into<Element<'a, Message, Renderer>> +    for ComboBox<'a, T, Message> +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/scrollable.rs b/native/src/widget/scrollable.rs index d7ad98e6..25fd8982 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -113,7 +113,7 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {  impl<'a, Message, Renderer> Widget<'a, 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) @@ -454,7 +454,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 +502,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( | 
