Merge pull request #575 from clarkmoody/scrollable-width

Custom Scrollbar Width
This commit is contained in:
Héctor Ramón 2020-10-27 23:35:52 +01:00 committed by GitHub
commit 8a3ce90959
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 491 additions and 19 deletions

View File

@ -67,6 +67,7 @@ members = [
"examples/pick_list", "examples/pick_list",
"examples/pokedex", "examples/pokedex",
"examples/progress_bar", "examples/progress_bar",
"examples/scrollable",
"examples/solar_system", "examples/solar_system",
"examples/stopwatch", "examples/stopwatch",
"examples/styling", "examples/styling",

View File

@ -103,6 +103,7 @@ A bunch of simpler examples exist:
- [`pick_list`](pick_list), a dropdown list of selectable options. - [`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]. - [`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. - [`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. - [`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. - [`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. - [`svg`](svg), an application that renders the [Ghostscript Tiger] by leveraging the `Svg` widget.

View File

@ -0,0 +1,9 @@
[package]
name = "scrollable"
version = "0.1.0"
authors = ["Clark Moody <clark@clarkmoody.com>"]
edition = "2018"
publish = false
[dependencies]
iced = { path = "../.." }

View File

@ -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
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@ -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),
},
]
}
}

View File

@ -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),
}
}
}
}

View File

@ -15,9 +15,6 @@ pub use iced_style::scrollable::{Scrollbar, Scroller, StyleSheet};
pub type Scrollable<'a, Message, Backend> = pub type Scrollable<'a, Message, Backend> =
iced_native::Scrollable<'a, Message, Renderer<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> impl<B> scrollable::Renderer for Renderer<B>
where where
B: Backend, B: Backend,
@ -29,29 +26,45 @@ where
bounds: Rectangle, bounds: Rectangle,
content_bounds: Rectangle, content_bounds: Rectangle,
offset: u32, offset: u32,
scrollbar_width: u16,
scrollbar_margin: u16,
scroller_width: u16,
) -> Option<scrollable::Scrollbar> { ) -> Option<scrollable::Scrollbar> {
if content_bounds.height > bounds.height { 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 { let scrollbar_bounds = Rectangle {
x: bounds.x + bounds.width x: bounds.x + bounds.width
- f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), - f32::from(outer_width / 2 + scrollbar_width / 2),
y: bounds.y, y: bounds.y,
width: f32::from(SCROLLBAR_WIDTH + 2 * SCROLLBAR_MARGIN), width: scrollbar_width as f32,
height: bounds.height, height: bounds.height,
}; };
let ratio = bounds.height / content_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 y_offset = offset as f32 * ratio;
let scroller_bounds = Rectangle { 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, y: scrollbar_bounds.y + y_offset,
width: scrollbar_bounds.width - f32::from(2 * SCROLLBAR_MARGIN), width: scroller_width as f32,
height: scrollbar_height, height: scroller_height,
}; };
Some(scrollable::Scrollbar { Some(scrollable::Scrollbar {
outer_bounds,
bounds: scrollbar_bounds, bounds: scrollbar_bounds,
margin: scrollbar_margin,
scroller: scrollable::Scroller { scroller: scrollable::Scroller {
bounds: scroller_bounds, bounds: scroller_bounds,
}, },
@ -109,12 +122,7 @@ where
let scrollbar = if is_scrollbar_visible { let scrollbar = if is_scrollbar_visible {
Primitive::Quad { Primitive::Quad {
bounds: Rectangle { bounds: scrollbar.bounds,
x: scrollbar.bounds.x + f32::from(SCROLLBAR_MARGIN),
width: scrollbar.bounds.width
- f32::from(2 * SCROLLBAR_MARGIN),
..scrollbar.bounds
},
background: style background: style
.background .background
.unwrap_or(Background::Color(Color::TRANSPARENT)), .unwrap_or(Background::Color(Color::TRANSPARENT)),

View File

@ -89,6 +89,9 @@ impl scrollable::Renderer for Null {
_bounds: Rectangle, _bounds: Rectangle,
_content_bounds: Rectangle, _content_bounds: Rectangle,
_offset: u32, _offset: u32,
_scrollbar_width: u16,
_scrollbar_margin: u16,
_scroller_width: u16,
) -> Option<scrollable::Scrollbar> { ) -> Option<scrollable::Scrollbar> {
None None
} }

View File

@ -13,6 +13,9 @@ pub struct Scrollable<'a, Message, Renderer: self::Renderer> {
state: &'a mut State, state: &'a mut State,
height: Length, height: Length,
max_height: u32, max_height: u32,
scrollbar_width: u16,
scrollbar_margin: u16,
scroller_width: u16,
content: Column<'a, Message, Renderer>, content: Column<'a, Message, Renderer>,
style: Renderer::Style, style: Renderer::Style,
} }
@ -27,6 +30,9 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
state, state,
height: Length::Shrink, height: Length::Shrink,
max_height: u32::MAX, max_height: u32::MAX,
scrollbar_width: 10,
scrollbar_margin: 0,
scroller_width: 10,
content: Column::new(), content: Column::new(),
style: Renderer::Style::default(), style: Renderer::Style::default(),
} }
@ -90,6 +96,32 @@ impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
self 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`] . /// Sets the style of the [`Scrollable`] .
/// ///
/// [`Scrollable`]: struct.Scrollable.html /// [`Scrollable`]: struct.Scrollable.html
@ -178,7 +210,14 @@ where
} }
let offset = self.state.offset(bounds, content_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_scrollbar = scrollbar let is_mouse_over_scrollbar = scrollbar
.as_ref() .as_ref()
.map(|scrollbar| scrollbar.is_mouse_over(cursor_position)) .map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
@ -269,7 +308,14 @@ where
let content_layout = layout.children().next().unwrap(); let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds(); let content_bounds = content_layout.bounds();
let offset = self.state.offset(bounds, content_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 = bounds.contains(cursor_position);
let is_mouse_over_scrollbar = scrollbar let is_mouse_over_scrollbar = scrollbar
@ -413,11 +459,23 @@ impl State {
/// [`Scrollable`]: struct.Scrollable.html /// [`Scrollable`]: struct.Scrollable.html
#[derive(Debug)] #[derive(Debug)]
pub struct Scrollbar { 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`]. /// The bounds of the [`Scrollbar`].
/// ///
/// [`Scrollbar`]: struct.Scrollbar.html /// [`Scrollbar`]: struct.Scrollbar.html
pub bounds: Rectangle, pub bounds: Rectangle,
/// The margin within the [`Scrollbar`].
///
/// [`Scrollbar`]: struct.Scrollbar.html
pub margin: u16,
/// The bounds of the [`Scroller`]. /// The bounds of the [`Scroller`].
/// ///
/// [`Scroller`]: struct.Scroller.html /// [`Scroller`]: struct.Scroller.html
@ -426,11 +484,11 @@ pub struct Scrollbar {
impl Scrollbar { impl Scrollbar {
fn is_mouse_over(&self, cursor_position: Point) -> bool { 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> { 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) { Some(if self.scroller.bounds.contains(cursor_position) {
(cursor_position.y - self.scroller.bounds.y) (cursor_position.y - self.scroller.bounds.y)
/ self.scroller.bounds.height / self.scroller.bounds.height
@ -486,6 +544,9 @@ pub trait Renderer: column::Renderer + Sized {
bounds: Rectangle, bounds: Rectangle,
content_bounds: Rectangle, content_bounds: Rectangle,
offset: u32, offset: u32,
scrollbar_width: u16,
scrollbar_margin: u16,
scroller_width: u16,
) -> Option<Scrollbar>; ) -> Option<Scrollbar>;
/// Draws the [`Scrollable`]. /// Draws the [`Scrollable`].