diff options
44 files changed, 636 insertions, 73 deletions
@@ -1,4 +1,4 @@ -/target +target/  pkg/  **/*.rs.bk  Cargo.lock @@ -67,6 +67,7 @@ members = [      "examples/pick_list",      "examples/pokedex",      "examples/progress_bar", +    "examples/scrollable",      "examples/solar_system",      "examples/stopwatch",      "examples/styling", diff --git a/examples/README.md b/examples/README.md index 34a916a1..32ccf724 100644 --- a/examples/README.md +++ b/examples/README.md @@ -103,6 +103,7 @@ A bunch of simpler examples exist:  - [`pick_list`](pick_list), a dropdown list of selectable options.  - [`pokedex`](pokedex), an application that displays a random Pokédex entry (sprite included!) by using the [PokéAPI].  - [`progress_bar`](progress_bar), a simple progress bar that can be filled by using a slider. +- [`scrollable`](scrollable), a showcase of the various scrollbar width options.  - [`solar_system`](solar_system), an animated solar system drawn using the `Canvas` widget and showcasing how to compose different transforms.  - [`stopwatch`](stopwatch), a watch with start/stop and reset buttons showcasing how to listen to time.  - [`svg`](svg), an application that renders the [Ghostscript Tiger] by leveraging the `Svg` widget. diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 0eba1cd0..a0003d65 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -12,7 +12,7 @@ mod circle {      use iced_graphics::{Backend, Defaults, Primitive, Renderer};      use iced_native::{          layout, mouse, Background, Color, Element, Hasher, Layout, Length, -        Point, Size, Widget, +        Point, Rectangle, Size, Widget,      };      pub struct Circle { @@ -60,6 +60,7 @@ mod circle {              _defaults: &Defaults,              layout: Layout<'_>,              _cursor_position: Point, +            _viewport: &Rectangle,          ) -> (Primitive, mouse::Interaction) {              (                  Primitive::Quad { diff --git a/examples/geometry/src/main.rs b/examples/geometry/src/main.rs index 1c13c8d5..f650b2c1 100644 --- a/examples/geometry/src/main.rs +++ b/examples/geometry/src/main.rs @@ -15,8 +15,8 @@ mod rainbow {          Backend, Defaults, Primitive, Renderer,      };      use iced_native::{ -        layout, mouse, Element, Hasher, Layout, Length, Point, Size, Vector, -        Widget, +        layout, mouse, Element, Hasher, Layout, Length, Point, Rectangle, Size, +        Vector, Widget,      };      pub struct Rainbow; @@ -57,6 +57,7 @@ mod rainbow {              _defaults: &Defaults,              layout: Layout<'_>,              cursor_position: Point, +            _viewport: &Rectangle,          ) -> (Primitive, mouse::Interaction) {              let b = layout.bounds(); diff --git a/examples/scrollable/Cargo.toml b/examples/scrollable/Cargo.toml new file mode 100644 index 00000000..12753fb6 --- /dev/null +++ b/examples/scrollable/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "scrollable" +version = "0.1.0" +authors = ["Clark Moody <clark@clarkmoody.com>"] +edition = "2018" +publish = false + +[dependencies] +iced = { path = "../.." } diff --git a/examples/scrollable/README.md b/examples/scrollable/README.md new file mode 100644 index 00000000..ed0e31b5 --- /dev/null +++ b/examples/scrollable/README.md @@ -0,0 +1,15 @@ +# Scrollable +An example showcasing the various size and style options for the Scrollable. + +All the example code is located in the __[`main`](src/main.rs)__ file. + +<div align="center"> +  <a href="./screenshot.png"> +    <img src="./screenshot.png" height="640px"> +  </a> +</div> + +You can run it with `cargo run`: +``` +cargo run --package scrollable +``` diff --git a/examples/scrollable/screenshot.png b/examples/scrollable/screenshot.png Binary files differnew file mode 100644 index 00000000..2d800251 --- /dev/null +++ b/examples/scrollable/screenshot.png diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs new file mode 100644 index 00000000..8dd2e20c --- /dev/null +++ b/examples/scrollable/src/main.rs @@ -0,0 +1,184 @@ +mod style; + +use iced::{ +    scrollable, Column, Container, Element, Length, Radio, Row, Rule, Sandbox, +    Scrollable, Settings, Space, Text, +}; + +pub fn main() -> iced::Result { +    ScrollableDemo::run(Settings::default()) +} + +struct ScrollableDemo { +    theme: style::Theme, +    variants: Vec<Variant>, +} + +#[derive(Debug, Clone)] +enum Message { +    ThemeChanged(style::Theme), +} + +impl Sandbox for ScrollableDemo { +    type Message = Message; + +    fn new() -> Self { +        ScrollableDemo { +            theme: Default::default(), +            variants: Variant::all(), +        } +    } + +    fn title(&self) -> String { +        String::from("Scrollable - Iced") +    } + +    fn update(&mut self, message: Message) { +        match message { +            Message::ThemeChanged(theme) => self.theme = theme, +        } +    } + +    fn view(&mut self) -> Element<Message> { +        let ScrollableDemo { +            theme, variants, .. +        } = self; + +        let choose_theme = style::Theme::ALL.iter().fold( +            Column::new().spacing(10).push(Text::new("Choose a theme:")), +            |column, option| { +                column.push( +                    Radio::new( +                        *option, +                        &format!("{:?}", option), +                        Some(*theme), +                        Message::ThemeChanged, +                    ) +                    .style(*theme), +                ) +            }, +        ); + +        let scrollable_row = Row::with_children( +            variants +                .iter_mut() +                .map(|variant| { +                    let mut scrollable = Scrollable::new(&mut variant.state) +                        .padding(10) +                        .width(Length::Fill) +                        .height(Length::Fill) +                        .style(*theme) +                        .push(Text::new(variant.title)); + +                    if let Some(scrollbar_width) = variant.scrollbar_width { +                        scrollable = scrollable +                            .scrollbar_width(scrollbar_width) +                            .push(Text::new(format!( +                                "scrollbar_width: {:?}", +                                scrollbar_width +                            ))); +                    } + +                    if let Some(scrollbar_margin) = variant.scrollbar_margin { +                        scrollable = scrollable +                            .scrollbar_margin(scrollbar_margin) +                            .push(Text::new(format!( +                                "scrollbar_margin: {:?}", +                                scrollbar_margin +                            ))); +                    } + +                    if let Some(scroller_width) = variant.scroller_width { +                        scrollable = scrollable +                            .scroller_width(scroller_width) +                            .push(Text::new(format!( +                                "scroller_width: {:?}", +                                scroller_width +                            ))); +                    } + +                    scrollable = scrollable +                        .push(Space::with_height(Length::Units(100))) +                        .push(Text::new( +                            "Some content that should wrap within the \ +                            scrollable. Let's output a lot of short words, so \ +                            that we'll make sure to see how wrapping works \ +                            with these scrollbars.", +                        )) +                        .push(Space::with_height(Length::Units(1200))) +                        .push(Text::new("Middle")) +                        .push(Space::with_height(Length::Units(1200))) +                        .push(Text::new("The End.")); + +                    Container::new(scrollable) +                        .width(Length::Fill) +                        .height(Length::Fill) +                        .style(*theme) +                        .into() +                }) +                .collect(), +        ) +        .spacing(20) +        .width(Length::Fill) +        .height(Length::Fill); + +        let content = Column::new() +            .spacing(20) +            .padding(20) +            .push(choose_theme) +            .push(Rule::horizontal(20).style(self.theme)) +            .push(scrollable_row); + +        Container::new(content) +            .width(Length::Fill) +            .height(Length::Fill) +            .center_x() +            .center_y() +            .style(self.theme) +            .into() +    } +} + +/// A version of a scrollable +struct Variant { +    title: &'static str, +    state: scrollable::State, +    scrollbar_width: Option<u16>, +    scrollbar_margin: Option<u16>, +    scroller_width: Option<u16>, +} + +impl Variant { +    pub fn all() -> Vec<Self> { +        vec![ +            Self { +                title: "Default Scrollbar", +                state: scrollable::State::new(), +                scrollbar_width: None, +                scrollbar_margin: None, +                scroller_width: None, +            }, +            Self { +                title: "Slimmed & Margin", +                state: scrollable::State::new(), +                scrollbar_width: Some(4), +                scrollbar_margin: Some(3), +                scroller_width: Some(4), +            }, +            Self { +                title: "Wide Scroller", +                state: scrollable::State::new(), +                scrollbar_width: Some(4), +                scrollbar_margin: None, +                scroller_width: Some(10), +            }, +            Self { +                title: "Narrow Scroller", +                state: scrollable::State::new(), +                scrollbar_width: Some(10), +                scrollbar_margin: None, +                scroller_width: Some(4), +            }, +        ] +    } +} diff --git a/examples/scrollable/src/style.rs b/examples/scrollable/src/style.rs new file mode 100644 index 00000000..24d711ac --- /dev/null +++ b/examples/scrollable/src/style.rs @@ -0,0 +1,190 @@ +use iced::{container, radio, rule, scrollable}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Theme { +    Light, +    Dark, +} + +impl Theme { +    pub const ALL: [Theme; 2] = [Theme::Light, Theme::Dark]; +} + +impl Default for Theme { +    fn default() -> Theme { +        Theme::Light +    } +} + +impl From<Theme> for Box<dyn container::StyleSheet> { +    fn from(theme: Theme) -> Self { +        match theme { +            Theme::Light => Default::default(), +            Theme::Dark => dark::Container.into(), +        } +    } +} + +impl From<Theme> for Box<dyn radio::StyleSheet> { +    fn from(theme: Theme) -> Self { +        match theme { +            Theme::Light => Default::default(), +            Theme::Dark => dark::Radio.into(), +        } +    } +} + +impl From<Theme> for Box<dyn scrollable::StyleSheet> { +    fn from(theme: Theme) -> Self { +        match theme { +            Theme::Light => Default::default(), +            Theme::Dark => dark::Scrollable.into(), +        } +    } +} + +impl From<Theme> for Box<dyn rule::StyleSheet> { +    fn from(theme: Theme) -> Self { +        match theme { +            Theme::Light => Default::default(), +            Theme::Dark => dark::Rule.into(), +        } +    } +} + +mod dark { +    use iced::{container, radio, rule, scrollable, Color}; + +    const BACKGROUND: Color = Color::from_rgb( +        0x36 as f32 / 255.0, +        0x39 as f32 / 255.0, +        0x3F as f32 / 255.0, +    ); + +    const SURFACE: Color = Color::from_rgb( +        0x40 as f32 / 255.0, +        0x44 as f32 / 255.0, +        0x4B as f32 / 255.0, +    ); + +    const ACCENT: Color = Color::from_rgb( +        0x6F as f32 / 255.0, +        0xFF as f32 / 255.0, +        0xE9 as f32 / 255.0, +    ); + +    const ACTIVE: Color = Color::from_rgb( +        0x72 as f32 / 255.0, +        0x89 as f32 / 255.0, +        0xDA as f32 / 255.0, +    ); + +    const SCROLLBAR: Color = Color::from_rgb( +        0x2E as f32 / 255.0, +        0x33 as f32 / 255.0, +        0x38 as f32 / 255.0, +    ); + +    const SCROLLER: Color = Color::from_rgb( +        0x20 as f32 / 255.0, +        0x22 as f32 / 255.0, +        0x25 as f32 / 255.0, +    ); + +    pub struct Container; + +    impl container::StyleSheet for Container { +        fn style(&self) -> container::Style { +            container::Style { +                background: Color { +                    a: 0.99, +                    ..BACKGROUND +                } +                .into(), +                text_color: Color::WHITE.into(), +                ..container::Style::default() +            } +        } +    } + +    pub struct Radio; + +    impl radio::StyleSheet for Radio { +        fn active(&self) -> radio::Style { +            radio::Style { +                background: SURFACE.into(), +                dot_color: ACTIVE, +                border_width: 1, +                border_color: ACTIVE, +            } +        } + +        fn hovered(&self) -> radio::Style { +            radio::Style { +                background: Color { a: 0.5, ..SURFACE }.into(), +                ..self.active() +            } +        } +    } + +    pub struct Scrollable; + +    impl scrollable::StyleSheet for Scrollable { +        fn active(&self) -> scrollable::Scrollbar { +            scrollable::Scrollbar { +                background: Color { +                    a: 0.8, +                    ..SCROLLBAR +                } +                .into(), +                border_radius: 2, +                border_width: 0, +                border_color: Color::TRANSPARENT, +                scroller: scrollable::Scroller { +                    color: Color { a: 0.7, ..SCROLLER }, +                    border_radius: 2, +                    border_width: 0, +                    border_color: Color::TRANSPARENT, +                }, +            } +        } + +        fn hovered(&self) -> scrollable::Scrollbar { +            let active = self.active(); + +            scrollable::Scrollbar { +                background: SCROLLBAR.into(), +                scroller: scrollable::Scroller { +                    color: SCROLLER, +                    ..active.scroller +                }, +                ..active +            } +        } + +        fn dragging(&self) -> scrollable::Scrollbar { +            let hovered = self.hovered(); + +            scrollable::Scrollbar { +                scroller: scrollable::Scroller { +                    color: ACCENT, +                    ..hovered.scroller +                }, +                ..hovered +            } +        } +    } + +    pub struct Rule; + +    impl rule::StyleSheet for Rule { +        fn style(&self) -> rule::Style { +            rule::Style { +                color: SURFACE, +                width: 2, +                radius: 1, +                fill_mode: rule::FillMode::Percent(30.0), +            } +        } +    } +} diff --git a/futures/src/subscription.rs b/futures/src/subscription.rs index 7a75fc31..e97ff3ab 100644 --- a/futures/src/subscription.rs +++ b/futures/src/subscription.rs @@ -76,10 +76,6 @@ where      ///      /// The value will be part of the identity of a [`Subscription`].      /// -    /// This is necessary if you want to use multiple instances of the same -    /// [`Subscription`] to produce different kinds of messages based on some -    /// external data. -    ///      /// [`Subscription`]: struct.Subscription.html      pub fn with<T>(mut self, value: T) -> Subscription<H, E, (T, O)>      where @@ -103,24 +99,19 @@ where      /// Transforms the [`Subscription`] output with the given function.      ///      /// [`Subscription`]: struct.Subscription.html -    pub fn map<A>( -        mut self, -        f: impl Fn(O) -> A + Send + Sync + 'static, -    ) -> Subscription<H, E, A> +    pub fn map<A>(mut self, f: fn(O) -> A) -> Subscription<H, E, A>      where          H: 'static,          E: 'static,          O: 'static,          A: 'static,      { -        let function = std::sync::Arc::new(f); -          Subscription {              recipes: self                  .recipes                  .drain(..)                  .map(|recipe| { -                    Box::new(Map::new(recipe, function.clone())) +                    Box::new(Map::new(recipe, f))                          as Box<dyn Recipe<H, E, Output = A>>                  })                  .collect(), @@ -186,13 +177,13 @@ pub trait Recipe<Hasher: std::hash::Hasher, Event> {  struct Map<Hasher, Event, A, B> {      recipe: Box<dyn Recipe<Hasher, Event, Output = A>>, -    mapper: std::sync::Arc<dyn Fn(A) -> B + Send + Sync>, +    mapper: fn(A) -> B,  }  impl<H, E, A, B> Map<H, E, A, B> {      fn new(          recipe: Box<dyn Recipe<H, E, Output = A>>, -        mapper: std::sync::Arc<dyn Fn(A) -> B + Send + Sync + 'static>, +        mapper: fn(A) -> B,      ) -> Self {          Map { recipe, mapper }      } @@ -209,8 +200,8 @@ where      fn hash(&self, state: &mut H) {          use std::hash::Hash; -        std::any::TypeId::of::<B>().hash(state);          self.recipe.hash(state); +        self.mapper.hash(state);      }      fn stream(self: Box<Self>, input: BoxStream<E>) -> BoxStream<Self::Output> { diff --git a/graphics/src/overlay/menu.rs b/graphics/src/overlay/menu.rs index a952f065..f42c5e3c 100644 --- a/graphics/src/overlay/menu.rs +++ b/graphics/src/overlay/menu.rs @@ -42,6 +42,7 @@ where          &mut self,          bounds: Rectangle,          cursor_position: Point, +        viewport: &Rectangle,          options: &[T],          hovered_option: Option<usize>,          padding: u16, @@ -52,16 +53,24 @@ where          use std::f32;          let is_mouse_over = bounds.contains(cursor_position); +        let option_height = text_size as usize + padding as usize * 2;          let mut primitives = Vec::new(); -        for (i, option) in options.iter().enumerate() { +        let offset = viewport.y - bounds.y; +        let start = (offset / option_height as f32) as usize; +        let end = +            ((offset + viewport.height) / option_height as f32).ceil() as usize; + +        let visible_options = &options[start..end.min(options.len())]; + +        for (i, option) in visible_options.iter().enumerate() { +            let i = start + i;              let is_selected = hovered_option == Some(i);              let bounds = Rectangle {                  x: bounds.x, -                y: bounds.y -                    + ((text_size as usize + padding as usize * 2) * i) as f32, +                y: bounds.y + (option_height * i) as f32,                  width: bounds.width,                  height: f32::from(text_size + padding * 2),              }; diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index 5d51e6d4..a880e22a 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -96,10 +96,11 @@ where          widget: &dyn Widget<Message, Self>,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,          color: Color,      ) -> Self::Output {          let (primitive, cursor) = -            widget.draw(self, defaults, layout, cursor_position); +            widget.draw(self, defaults, layout, cursor_position, viewport);          let mut primitives = Vec::new(); diff --git a/graphics/src/widget/button.rs b/graphics/src/widget/button.rs index ecabc868..a1afc940 100644 --- a/graphics/src/widget/button.rs +++ b/graphics/src/widget/button.rs @@ -62,6 +62,7 @@ where              },              content_layout,              cursor_position, +            &bounds,          );          ( diff --git a/graphics/src/widget/canvas.rs b/graphics/src/widget/canvas.rs index bc0802e5..73778d16 100644 --- a/graphics/src/widget/canvas.rs +++ b/graphics/src/widget/canvas.rs @@ -8,8 +8,8 @@  //! [`Frame`]: struct.Frame.html  use crate::{Backend, Defaults, Primitive, Renderer};  use iced_native::{ -    layout, mouse, Clipboard, Element, Hasher, Layout, Length, Point, Size, -    Vector, Widget, +    layout, mouse, Clipboard, Element, Hasher, Layout, Length, Point, +    Rectangle, Size, Vector, Widget,  };  use std::hash::Hash;  use std::marker::PhantomData; @@ -196,6 +196,7 @@ where          _defaults: &Defaults,          layout: Layout<'_>,          cursor_position: Point, +        _viewport: &Rectangle,      ) -> (Primitive, mouse::Interaction) {          let bounds = layout.bounds();          let translation = Vector::new(bounds.x, bounds.y); diff --git a/graphics/src/widget/column.rs b/graphics/src/widget/column.rs index 6c7235c7..0cf56842 100644 --- a/graphics/src/widget/column.rs +++ b/graphics/src/widget/column.rs @@ -1,7 +1,7 @@  use crate::{Backend, Primitive, Renderer};  use iced_native::column;  use iced_native::mouse; -use iced_native::{Element, Layout, Point}; +use iced_native::{Element, Layout, Point, Rectangle};  /// A container that distributes its contents vertically.  pub type Column<'a, Message, Backend> = @@ -17,6 +17,7 @@ where          content: &[Element<'_, Message, Self>],          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Self::Output {          let mut mouse_interaction = mouse::Interaction::default(); @@ -26,8 +27,13 @@ where                      .iter()                      .zip(layout.children())                      .map(|(child, layout)| { -                        let (primitive, new_mouse_interaction) = -                            child.draw(self, defaults, layout, cursor_position); +                        let (primitive, new_mouse_interaction) = child.draw( +                            self, +                            defaults, +                            layout, +                            cursor_position, +                            viewport, +                        );                          if new_mouse_interaction > mouse_interaction {                              mouse_interaction = new_mouse_interaction; diff --git a/graphics/src/widget/container.rs b/graphics/src/widget/container.rs index 5b3a01d2..4854f589 100644 --- a/graphics/src/widget/container.rs +++ b/graphics/src/widget/container.rs @@ -24,6 +24,7 @@ where          defaults: &Defaults,          bounds: Rectangle,          cursor_position: Point, +        viewport: &Rectangle,          style_sheet: &Self::Style,          content: &Element<'_, Message, Self>,          content_layout: Layout<'_>, @@ -36,8 +37,13 @@ where              },          }; -        let (content, mouse_interaction) = -            content.draw(self, &defaults, content_layout, cursor_position); +        let (content, mouse_interaction) = content.draw( +            self, +            &defaults, +            content_layout, +            cursor_position, +            viewport, +        );          if let Some(background) = background(bounds, &style) {              ( diff --git a/graphics/src/widget/pane_grid.rs b/graphics/src/widget/pane_grid.rs index aa8a3f7c..5b0eb391 100644 --- a/graphics/src/widget/pane_grid.rs +++ b/graphics/src/widget/pane_grid.rs @@ -137,7 +137,7 @@ where          let (body, body_layout) = body;          let (body_primitive, body_interaction) = -            body.draw(self, defaults, body_layout, cursor_position); +            body.draw(self, defaults, body_layout, cursor_position, &bounds);          let background = crate::widget::container::background(bounds, &style); @@ -224,6 +224,7 @@ where                  &defaults,                  controls_layout,                  cursor_position, +                &bounds,              );              ( diff --git a/graphics/src/widget/row.rs b/graphics/src/widget/row.rs index 4c1dbadc..397d80bf 100644 --- a/graphics/src/widget/row.rs +++ b/graphics/src/widget/row.rs @@ -1,7 +1,7 @@  use crate::{Backend, Primitive, Renderer};  use iced_native::mouse;  use iced_native::row; -use iced_native::{Element, Layout, Point}; +use iced_native::{Element, Layout, Point, Rectangle};  /// A container that distributes its contents horizontally.  pub type Row<'a, Message, Backend> = @@ -17,6 +17,7 @@ where          content: &[Element<'_, Message, Self>],          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Self::Output {          let mut mouse_interaction = mouse::Interaction::default(); @@ -26,8 +27,13 @@ where                      .iter()                      .zip(layout.children())                      .map(|(child, layout)| { -                        let (primitive, new_mouse_interaction) = -                            child.draw(self, defaults, layout, cursor_position); +                        let (primitive, new_mouse_interaction) = child.draw( +                            self, +                            defaults, +                            layout, +                            cursor_position, +                            viewport, +                        );                          if new_mouse_interaction > mouse_interaction {                              mouse_interaction = new_mouse_interaction; diff --git a/graphics/src/widget/scrollable.rs b/graphics/src/widget/scrollable.rs index b149db0a..fed79c18 100644 --- a/graphics/src/widget/scrollable.rs +++ b/graphics/src/widget/scrollable.rs @@ -15,9 +15,6 @@ pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet};  pub type Scrollable<'a, Message, Backend> =      iced_native::Scrollable<'a, Message, Renderer<Backend>>; -const SCROLLBAR_WIDTH: u16 = 10; -const SCROLLBAR_MARGIN: u16 = 2; -  impl<B> scrollable::Renderer for Renderer<B>  where      B: Backend, @@ -29,29 +26,45 @@ where          bounds: Rectangle,          content_bounds: Rectangle,          offset: u32, +        scrollbar_width: u16, +        scrollbar_margin: u16, +        scroller_width: u16,      ) -> Option<scrollable::Scrollbar> {          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(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), +                    - f32::from(outer_width / 2 + scrollbar_width / 2),                  y: bounds.y, -                width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), +                width: scrollbar_width as f32,                  height: bounds.height,              };              let ratio = bounds.height / content_bounds.height; -            let scrollbar_height = bounds.height * ratio; +            let scroller_height = bounds.height * ratio;              let y_offset = offset as f32 * ratio;              let scroller_bounds = Rectangle { -                x: scrollbar_bounds.x + f32::from(SCROLLBAR_MARGIN), +                x: bounds.x + bounds.width +                    - f32::from(outer_width / 2 + scroller_width / 2),                  y: scrollbar_bounds.y + y_offset, -                width: scrollbar_bounds.width - f32::from(2 * SCROLLBAR_MARGIN), -                height: scrollbar_height, +                width: scroller_width as f32, +                height: scroller_height,              };              Some(scrollable::Scrollbar { +                outer_bounds,                  bounds: scrollbar_bounds, +                margin: scrollbar_margin,                  scroller: scrollable::Scroller {                      bounds: scroller_bounds,                  }, @@ -109,12 +122,7 @@ where                  let scrollbar = if is_scrollbar_visible {                      Primitive::Quad { -                        bounds: Rectangle { -                            x: scrollbar.bounds.x + f32::from(SCROLLBAR_MARGIN), -                            width: scrollbar.bounds.width -                                - f32::from(2 * SCROLLBAR_MARGIN), -                            ..scrollbar.bounds -                        }, +                        bounds: scrollbar.bounds,                          background: style                              .background                              .unwrap_or(Background::Color(Color::TRANSPARENT)), diff --git a/native/src/element.rs b/native/src/element.rs index 514a135b..10e1b5fb 100644 --- a/native/src/element.rs +++ b/native/src/element.rs @@ -1,6 +1,7 @@ +use crate::layout; +use crate::overlay;  use crate::{ -    layout, overlay, Clipboard, Color, Event, Hasher, Layout, Length, Point, -    Widget, +    Clipboard, Color, Event, Hasher, Layout, Length, Point, Rectangle, Widget,  };  /// A generic [`Widget`]. @@ -260,9 +261,10 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Renderer::Output {          self.widget -            .draw(renderer, defaults, layout, cursor_position) +            .draw(renderer, defaults, layout, cursor_position, viewport)      }      /// Computes the _layout_ hash of the [`Element`]. @@ -356,9 +358,10 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Renderer::Output {          self.widget -            .draw(renderer, defaults, layout, cursor_position) +            .draw(renderer, defaults, layout, cursor_position, viewport)      }      fn hash_layout(&self, state: &mut Hasher) { @@ -437,12 +440,14 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Renderer::Output {          renderer.explain(              defaults,              self.element.widget.as_ref(),              layout,              cursor_position, +            viewport,              self.color,          )      } diff --git a/native/src/layout/debugger.rs b/native/src/layout/debugger.rs index e4b21609..4c6dd793 100644 --- a/native/src/layout/debugger.rs +++ b/native/src/layout/debugger.rs @@ -1,4 +1,4 @@ -use crate::{Color, Layout, Point, Renderer, Widget}; +use crate::{Color, Layout, Point, Rectangle, Renderer, Widget};  /// A renderer able to graphically explain a [`Layout`].  /// @@ -21,6 +21,7 @@ pub trait Debugger: Renderer {          widget: &dyn Widget<Message, Self>,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,          color: Color,      ) -> Self::Output;  } diff --git a/native/src/overlay/menu.rs b/native/src/overlay/menu.rs index c2df468e..4b392a8e 100644 --- a/native/src/overlay/menu.rs +++ b/native/src/overlay/menu.rs @@ -253,9 +253,13 @@ where          layout: Layout<'_>,          cursor_position: Point,      ) -> Renderer::Output { -        let primitives = -            self.container -                .draw(renderer, defaults, layout, cursor_position); +        let primitives = self.container.draw( +            renderer, +            defaults, +            layout, +            cursor_position, +            &layout.bounds(), +        );          renderer.decorate(              layout.bounds(), @@ -368,11 +372,13 @@ where          _defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Renderer::Output {          self::Renderer::draw(              renderer,              layout.bounds(),              cursor_position, +            viewport,              self.options,              *self.hovered_option,              self.padding, @@ -418,6 +424,7 @@ pub trait Renderer:          &mut self,          bounds: Rectangle,          cursor_position: Point, +        viewport: &Rectangle,          options: &[T],          hovered_option: Option<usize>,          padding: u16, diff --git a/native/src/renderer/null.rs b/native/src/renderer/null.rs index 2aee0da1..a3c3cf33 100644 --- a/native/src/renderer/null.rs +++ b/native/src/renderer/null.rs @@ -35,6 +35,7 @@ impl column::Renderer for Null {          _content: &[Element<'_, Message, Self>],          _layout: Layout<'_>,          _cursor_position: Point, +        _viewport: &Rectangle,      ) {      }  } @@ -46,6 +47,7 @@ impl row::Renderer for Null {          _content: &[Element<'_, Message, Self>],          _layout: Layout<'_>,          _cursor_position: Point, +        _viewport: &Rectangle,      ) {      }  } @@ -89,6 +91,9 @@ impl scrollable::Renderer for Null {          _bounds: Rectangle,          _content_bounds: Rectangle,          _offset: u32, +        _scrollbar_width: u16, +        _scrollbar_margin: u16, +        _scroller_width: u16,      ) -> Option<scrollable::Scrollbar> {          None      } @@ -234,6 +239,7 @@ impl container::Renderer for Null {          _defaults: &Self::Defaults,          _bounds: Rectangle,          _cursor_position: Point, +        _viewport: &Rectangle,          _style: &Self::Style,          _content: &Element<'_, Message, Self>,          _content_layout: Layout<'_>, diff --git a/native/src/user_interface.rs b/native/src/user_interface.rs index 00a290f1..59d91f42 100644 --- a/native/src/user_interface.rs +++ b/native/src/user_interface.rs @@ -1,4 +1,6 @@ -use crate::{layout, overlay, Clipboard, Element, Event, Layout, Point, Size}; +use crate::layout; +use crate::overlay; +use crate::{Clipboard, Element, Event, Layout, Point, Rectangle, Size};  use std::hash::Hasher; @@ -327,6 +329,8 @@ where          renderer: &mut Renderer,          cursor_position: Point,      ) -> Renderer::Output { +        let viewport = Rectangle::with_size(self.bounds); +          let overlay = if let Some(mut overlay) =              self.root.overlay(Layout::new(&self.base.layout))          { @@ -365,6 +369,7 @@ where                  &Renderer::Defaults::default(),                  Layout::new(&self.base.layout),                  base_cursor, +                &viewport,              );              renderer.overlay( @@ -378,6 +383,7 @@ where                  &Renderer::Defaults::default(),                  Layout::new(&self.base.layout),                  cursor_position, +                &viewport,              )          }      } diff --git a/native/src/widget.rs b/native/src/widget.rs index a10281df..8687ce6f 100644 --- a/native/src/widget.rs +++ b/native/src/widget.rs @@ -73,7 +73,9 @@ pub use text::Text;  #[doc(no_inline)]  pub use text_input::TextInput; -use crate::{layout, overlay, Clipboard, Event, Hasher, Layout, Length, Point}; +use crate::{ +    layout, overlay, Clipboard, Event, Hasher, Layout, Length, Point, Rectangle, +};  /// A component that displays information and allows interaction.  /// @@ -137,6 +139,7 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Renderer::Output;      /// Computes the _layout_ hash of the [`Widget`]. diff --git a/native/src/widget/button.rs b/native/src/widget/button.rs index fd74563a..995ba7bc 100644 --- a/native/src/widget/button.rs +++ b/native/src/widget/button.rs @@ -217,6 +217,7 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          renderer.draw(              defaults, diff --git a/native/src/widget/checkbox.rs b/native/src/widget/checkbox.rs index 99178aae..e389427e 100644 --- a/native/src/widget/checkbox.rs +++ b/native/src/widget/checkbox.rs @@ -180,6 +180,7 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          let bounds = layout.bounds();          let mut children = layout.children(); diff --git a/native/src/widget/column.rs b/native/src/widget/column.rs index a5ac2101..e874ad42 100644 --- a/native/src/widget/column.rs +++ b/native/src/widget/column.rs @@ -1,9 +1,11 @@  //! Distribute content vertically.  use std::hash::Hash; +use crate::layout; +use crate::overlay;  use crate::{ -    layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, -    Point, Widget, +    Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, +    Widget,  };  use std::u32; @@ -181,8 +183,15 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Renderer::Output { -        renderer.draw(defaults, &self.children, layout, cursor_position) +        renderer.draw( +            defaults, +            &self.children, +            layout, +            cursor_position, +            viewport, +        )      }      fn hash_layout(&self, state: &mut Hasher) { @@ -237,6 +246,7 @@ pub trait Renderer: crate::Renderer + Sized {          content: &[Element<'_, Message, Self>],          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Self::Output;  } diff --git a/native/src/widget/container.rs b/native/src/widget/container.rs index b8316e62..5b04d699 100644 --- a/native/src/widget/container.rs +++ b/native/src/widget/container.rs @@ -191,11 +191,13 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Renderer::Output {          renderer.draw(              defaults,              layout.bounds(),              cursor_position, +            viewport,              &self.style,              &self.content,              layout.children().next().unwrap(), @@ -242,6 +244,7 @@ pub trait Renderer: crate::Renderer {          defaults: &Self::Defaults,          bounds: Rectangle,          cursor_position: Point, +        viewport: &Rectangle,          style: &Self::Style,          content: &Element<'_, Message, Self>,          content_layout: Layout<'_>, diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs index 132f249d..9586da0f 100644 --- a/native/src/widget/image.rs +++ b/native/src/widget/image.rs @@ -1,5 +1,6 @@  //! Display images in your user interface. -use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; +use crate::layout; +use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget};  use std::{      hash::{Hash, Hasher as _}, @@ -97,6 +98,7 @@ where          _defaults: &Renderer::Defaults,          layout: Layout<'_>,          _cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          renderer.draw(self.handle.clone(), layout)      } diff --git a/native/src/widget/pane_grid.rs b/native/src/widget/pane_grid.rs index 5180fd3b..276bfae3 100644 --- a/native/src/widget/pane_grid.rs +++ b/native/src/widget/pane_grid.rs @@ -588,6 +588,7 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          let picked_split = self              .state diff --git a/native/src/widget/pick_list.rs b/native/src/widget/pick_list.rs index 0e42a3e4..e086e367 100644 --- a/native/src/widget/pick_list.rs +++ b/native/src/widget/pick_list.rs @@ -256,6 +256,7 @@ where          _defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          self::Renderer::draw(              renderer, diff --git a/native/src/widget/progress_bar.rs b/native/src/widget/progress_bar.rs index 5ab76d47..4f8a7e1b 100644 --- a/native/src/widget/progress_bar.rs +++ b/native/src/widget/progress_bar.rs @@ -104,6 +104,7 @@ where          _defaults: &Renderer::Defaults,          layout: Layout<'_>,          _cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          renderer.draw(              layout.bounds(), diff --git a/native/src/widget/radio.rs b/native/src/widget/radio.rs index a7cabd49..06d3f846 100644 --- a/native/src/widget/radio.rs +++ b/native/src/widget/radio.rs @@ -183,6 +183,7 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          let bounds = layout.bounds();          let mut children = layout.children(); diff --git a/native/src/widget/row.rs b/native/src/widget/row.rs index b37a5cfc..bc8a3df1 100644 --- a/native/src/widget/row.rs +++ b/native/src/widget/row.rs @@ -1,11 +1,12 @@  //! Distribute content horizontally. -use std::hash::Hash; - +use crate::layout; +use crate::overlay;  use crate::{ -    layout, overlay, Align, Clipboard, Element, Event, Hasher, Layout, Length, -    Point, Widget, +    Align, Clipboard, Element, Event, Hasher, Layout, Length, Point, Rectangle, +    Widget,  }; +use std::hash::Hash;  use std::u32;  /// A container that distributes its contents horizontally. @@ -182,8 +183,15 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Renderer::Output { -        renderer.draw(defaults, &self.children, layout, cursor_position) +        renderer.draw( +            defaults, +            &self.children, +            layout, +            cursor_position, +            viewport, +        )      }      fn hash_layout(&self, state: &mut Hasher) { @@ -238,6 +246,7 @@ pub trait Renderer: crate::Renderer + Sized {          children: &[Element<'_, Message, Self>],          layout: Layout<'_>,          cursor_position: Point, +        viewport: &Rectangle,      ) -> Self::Output;  } diff --git a/native/src/widget/rule.rs b/native/src/widget/rule.rs index 25cec53b..66328c08 100644 --- a/native/src/widget/rule.rs +++ b/native/src/widget/rule.rs @@ -77,6 +77,7 @@ where          _defaults: &Renderer::Defaults,          layout: Layout<'_>,          _cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          renderer.draw(layout.bounds(), &self.style, self.is_horizontal)      } diff --git a/native/src/widget/scrollable.rs b/native/src/widget/scrollable.rs index 75e97027..60ec2d7d 100644 --- a/native/src/widget/scrollable.rs +++ b/native/src/widget/scrollable.rs @@ -13,6 +13,9 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> {      state: &'a mut State,      height: Length,      max_height: u32, +    scrollbar_width: u16, +    scrollbar_margin: u16, +    scroller_width: u16,      content: Column<'a, Message, Renderer>,      style: Renderer::Style,  } @@ -27,6 +30,9 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {              state,              height: Length::Shrink,              max_height: u32::MAX, +            scrollbar_width: 10, +            scrollbar_margin: 0, +            scroller_width: 10,              content: Column::new(),              style: Renderer::Style::default(),          } @@ -90,6 +96,32 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {          self      } +    /// Sets the scrollbar width of the [`Scrollable`] . +    /// Silently enforces a minimum value of 1. +    /// +    /// [`Scrollable`]: struct.Scrollable.html +    pub fn scrollbar_width(mut self, scrollbar_width: u16) -> Self { +        self.scrollbar_width = scrollbar_width.max(1); +        self +    } + +    /// Sets the scrollbar margin of the [`Scrollable`] . +    /// +    /// [`Scrollable`]: struct.Scrollable.html +    pub fn scrollbar_margin(mut self, scrollbar_margin: u16) -> Self { +        self.scrollbar_margin = scrollbar_margin; +        self +    } + +    /// Sets the scroller width of the [`Scrollable`] . +    /// Silently enforces a minimum value of 1. +    /// +    /// [`Scrollable`]: struct.Scrollable.html +    pub fn scroller_width(mut self, scroller_width: u16) -> Self { +        self.scroller_width = scroller_width.max(1); +        self +    } +      /// Sets the style of the [`Scrollable`] .      ///      /// [`Scrollable`]: struct.Scrollable.html @@ -178,7 +210,14 @@ where          }          let offset = self.state.offset(bounds, content_bounds); -        let scrollbar = renderer.scrollbar(bounds, content_bounds, offset); +        let scrollbar = renderer.scrollbar( +            bounds, +            content_bounds, +            offset, +            self.scrollbar_width, +            self.scrollbar_margin, +            self.scroller_width, +        );          let is_mouse_over_scrollbar = scrollbar              .as_ref()              .map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) @@ -264,12 +303,20 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          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 = renderer.scrollbar(bounds, content_bounds, offset); +        let scrollbar = renderer.scrollbar( +            bounds, +            content_bounds, +            offset, +            self.scrollbar_width, +            self.scrollbar_margin, +            self.scroller_width, +        );          let is_mouse_over = bounds.contains(cursor_position);          let is_mouse_over_scrollbar = scrollbar @@ -289,6 +336,10 @@ where                  defaults,                  content_layout,                  cursor_position, +                &Rectangle { +                    y: bounds.y + offset as f32, +                    ..bounds +                },              )          }; @@ -413,11 +464,23 @@ impl State {  /// [`Scrollable`]: struct.Scrollable.html  #[derive(Debug)]  pub struct Scrollbar { +    /// The outer bounds of the scrollable, including the [`Scrollbar`] and +    /// [`Scroller`]. +    /// +    /// [`Scrollbar`]: struct.Scrollbar.html +    /// [`Scroller`]: struct.Scroller.html +    pub outer_bounds: Rectangle, +      /// The bounds of the [`Scrollbar`].      ///      /// [`Scrollbar`]: struct.Scrollbar.html      pub bounds: Rectangle, +    /// The margin within the [`Scrollbar`]. +    /// +    /// [`Scrollbar`]: struct.Scrollbar.html +    pub margin: u16, +      /// The bounds of the [`Scroller`].      ///      /// [`Scroller`]: struct.Scroller.html @@ -426,11 +489,11 @@ pub struct Scrollbar {  impl Scrollbar {      fn is_mouse_over(&self, cursor_position: Point) -> bool { -        self.bounds.contains(cursor_position) +        self.outer_bounds.contains(cursor_position)      }      fn grab_scroller(&self, cursor_position: Point) -> Option<f32> { -        if self.bounds.contains(cursor_position) { +        if self.outer_bounds.contains(cursor_position) {              Some(if self.scroller.bounds.contains(cursor_position) {                  (cursor_position.y - self.scroller.bounds.y)                      / self.scroller.bounds.height @@ -486,6 +549,9 @@ pub trait Renderer: column::Renderer + Sized {          bounds: Rectangle,          content_bounds: Rectangle,          offset: u32, +        scrollbar_width: u16, +        scrollbar_margin: u16, +        scroller_width: u16,      ) -> Option<Scrollbar>;      /// Draws the [`Scrollable`]. diff --git a/native/src/widget/slider.rs b/native/src/widget/slider.rs index 3255dd04..d6e366aa 100644 --- a/native/src/widget/slider.rs +++ b/native/src/widget/slider.rs @@ -259,6 +259,7 @@ where          _defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          let start = *self.range.start();          let end = *self.range.end(); diff --git a/native/src/widget/space.rs b/native/src/widget/space.rs index f1576ffb..032f341d 100644 --- a/native/src/widget/space.rs +++ b/native/src/widget/space.rs @@ -71,6 +71,7 @@ where          _defaults: &Renderer::Defaults,          layout: Layout<'_>,          _cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          renderer.draw(layout.bounds())      } diff --git a/native/src/widget/svg.rs b/native/src/widget/svg.rs index 114d5e41..ede1aff8 100644 --- a/native/src/widget/svg.rs +++ b/native/src/widget/svg.rs @@ -1,5 +1,6 @@  //! Display vector graphics in your application. -use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget}; +use crate::layout; +use crate::{Element, Hasher, Layout, Length, Point, Rectangle, Size, Widget};  use std::{      hash::{Hash, Hasher as _}, @@ -103,6 +104,7 @@ where          _defaults: &Renderer::Defaults,          layout: Layout<'_>,          _cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          renderer.draw(self.handle.clone(), layout)      } diff --git a/native/src/widget/text.rs b/native/src/widget/text.rs index 48a69e34..c2544b8e 100644 --- a/native/src/widget/text.rs +++ b/native/src/widget/text.rs @@ -149,6 +149,7 @@ where          defaults: &Renderer::Defaults,          layout: Layout<'_>,          _cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          renderer.draw(              defaults, diff --git a/native/src/widget/text_input.rs b/native/src/widget/text_input.rs index 32a57ce6..64182f2d 100644 --- a/native/src/widget/text_input.rs +++ b/native/src/widget/text_input.rs @@ -497,6 +497,7 @@ where          _defaults: &Renderer::Defaults,          layout: Layout<'_>,          cursor_position: Point, +        _viewport: &Rectangle,      ) -> Renderer::Output {          let bounds = layout.bounds();          let text_bounds = layout.children().next().unwrap().bounds(); diff --git a/src/window/icon.rs b/src/window/icon.rs index 15e0312d..0d27b00e 100644 --- a/src/window/icon.rs +++ b/src/window/icon.rs @@ -112,7 +112,7 @@ impl fmt::Display for Error {                  write!(f,                  "The number of RGBA pixels ({:?}) does not match the provided \                  dimensions ({:?}x{:?}).", -                width, height, pixel_count, +                pixel_count, width, height,              )              }              Error::OsError(e) => write!(  | 
