Draft Scrollable widget (no clipping yet!)

This commit is contained in:
Héctor Ramón Jiménez 2019-10-25 03:47:34 +02:00
parent 4769272122
commit 719c073fc6
13 changed files with 526 additions and 50 deletions

View file

@ -14,6 +14,7 @@ mod radio;
mod row; mod row;
pub mod button; pub mod button;
pub mod scrollable;
pub mod slider; pub mod slider;
pub mod text; pub mod text;
@ -26,6 +27,9 @@ pub use slider::Slider;
#[doc(no_inline)] #[doc(no_inline)]
pub use text::Text; pub use text::Text;
#[doc(no_inline)]
pub use scrollable::Scrollable;
pub use checkbox::Checkbox; pub use checkbox::Checkbox;
pub use column::Column; pub use column::Column;
pub use image::Image; pub use image::Image;

View file

@ -0,0 +1,122 @@
use crate::{Align, Column, Justify, Length};
#[derive(Debug)]
pub struct Scrollable<'a, Element> {
pub state: &'a mut State,
pub height: Length,
pub align_self: Option<Align>,
pub align_items: Align,
pub content: Column<Element>,
}
impl<'a, Element> Scrollable<'a, Element> {
pub fn new(state: &'a mut State) -> Self {
Scrollable {
state,
height: Length::Shrink,
align_self: None,
align_items: Align::Start,
content: Column::new(),
}
}
/// Sets the vertical spacing _between_ elements.
///
/// Custom margins per element do not exist in Iced. You should use this
/// method instead! While less flexible, it helps you keep spacing between
/// elements consistent.
pub fn spacing(mut self, units: u16) -> Self {
self.content = self.content.spacing(units);
self
}
/// Sets the padding of the [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn padding(mut self, units: u16) -> Self {
self.content = self.content.padding(units);
self
}
/// Sets the width of the [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn width(mut self, width: Length) -> Self {
self.content = self.content.width(width);
self
}
/// Sets the height of the [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
/// Sets the maximum width of the [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn max_width(mut self, max_width: Length) -> Self {
self.content = self.content.max_width(max_width);
self
}
/// Sets the maximum height of the [`Column`] in pixels.
///
/// [`Column`]: struct.Column.html
pub fn max_height(mut self, max_height: Length) -> Self {
self.content = self.content.max_height(max_height);
self
}
/// Sets the alignment of the [`Column`] itself.
///
/// This is useful if you want to override the default alignment given by
/// the parent container.
///
/// [`Column`]: struct.Column.html
pub fn align_self(mut self, align: Align) -> Self {
self.align_self = Some(align);
self
}
/// Sets the horizontal alignment of the contents of the [`Column`] .
///
/// [`Column`]: struct.Column.html
pub fn align_items(mut self, align_items: Align) -> Self {
self.align_items = align_items;
self
}
/// Sets the vertical distribution strategy for the contents of the
/// [`Column`] .
///
/// [`Column`]: struct.Column.html
pub fn justify_content(mut self, justify: Justify) -> Self {
self.content = self.content.justify_content(justify);
self
}
/// Adds an element to the [`Column`].
///
/// [`Column`]: struct.Column.html
pub fn push<E>(mut self, child: E) -> Scrollable<'a, Element>
where
E: Into<Element>,
{
self.content = self.content.push(child);
self
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct State {
pub offset: u32,
}
impl State {
pub fn new() -> Self {
State::default()
}
}

69
examples/scroll.rs Normal file
View file

@ -0,0 +1,69 @@
use iced::{
button, scrollable, Align, Application, Button, Color, Element, Image,
Length, Scrollable, Text,
};
pub fn main() {
Example::default().run()
}
#[derive(Default)]
struct Example {
paragraph_count: u16,
scroll: scrollable::State,
add_button: button::State,
}
#[derive(Debug, Clone, Copy)]
pub enum Message {
AddParagraph,
}
impl Application for Example {
type Message = Message;
fn update(&mut self, message: Message) {
match message {
Message::AddParagraph => {
self.paragraph_count += 1;
}
}
}
fn view(&mut self) -> Element<Message> {
let content = Scrollable::new(&mut self.scroll)
.width(Length::Fill)
.max_width(Length::Units(600))
.spacing(20)
.padding(20)
.align_self(Align::Center);
//let content = (0..self.paragraph_count)
// .fold(content, |column, _| column.push(lorem_ipsum()))
// .push(
// Button::new(&mut self.add_button, Text::new("Add paragraph"))
// .on_press(Message::AddParagraph)
// .padding(20)
// .border_radius(5)
// .align_self(Align::Center),
// );
(0..10)
.fold(content, |content, _| {
content.push(
Image::new(format!(
"{}/examples/resources/ferris.png",
env!("CARGO_MANIFEST_DIR")
))
.width(Length::Units(400))
.align_self(Align::Center),
)
})
.into()
}
}
fn lorem_ipsum() -> Text {
Text::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi in dui vel massa blandit interdum. Quisque placerat, odio ut vulputate sagittis, augue est facilisis ex, eget euismod felis magna in sapien. Nullam luctus consequat massa, ac interdum mauris blandit pellentesque. Nullam in est urna. Aliquam tristique lectus ac luctus feugiat. Aenean libero diam, euismod facilisis consequat quis, pellentesque luctus erat. Praesent vel tincidunt elit.")
}

View file

@ -347,7 +347,7 @@ impl Cache {
.0 .0
.compute_layout(geometry::Size::undefined()) .compute_layout(geometry::Size::undefined())
.unwrap(), .unwrap(),
cursor_position: Point::new(0.0, 0.0), cursor_position: Point::new(-1.0, -1.0),
} }
} }
} }

View file

@ -26,6 +26,7 @@ pub mod column;
pub mod image; pub mod image;
pub mod radio; pub mod radio;
pub mod row; pub mod row;
pub mod scrollable;
pub mod slider; pub mod slider;
pub mod text; pub mod text;
@ -42,6 +43,8 @@ pub use radio::Radio;
#[doc(no_inline)] #[doc(no_inline)]
pub use row::Row; pub use row::Row;
#[doc(no_inline)] #[doc(no_inline)]
pub use scrollable::Scrollable;
#[doc(no_inline)]
pub use slider::Slider; pub use slider::Slider;
#[doc(no_inline)] #[doc(no_inline)]
pub use text::Text; pub use text::Text;

View file

@ -0,0 +1,121 @@
use crate::{
column, input::mouse, Element, Event, Hasher, Layout, Node, Point, Style,
Widget,
};
pub use iced_core::scrollable::State;
/// A scrollable [`Column`].
///
/// [`Column`]: ../column/struct.Column.html
pub type Scrollable<'a, Message, Renderer> =
iced_core::Scrollable<'a, Element<'a, Message, Renderer>>;
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Scrollable<'a, Message, Renderer>
where
Renderer: self::Renderer + column::Renderer,
{
fn node(&self, renderer: &Renderer) -> Node {
let mut content = self.content.node(renderer);
{
let mut style = content.0.style();
style.flex_shrink = 0.0;
content.0.set_style(style);
}
let mut style = Style::default()
.width(self.content.width)
.max_width(self.content.max_width)
.height(self.height)
.align_self(self.align_self)
.align_items(self.align_items);
style.0.flex_direction = stretch::style::FlexDirection::Column;
Node::with_children(style, vec![content])
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
) {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let content = layout.children().next().unwrap();
let content_bounds = content.bounds();
if is_mouse_over {
match event {
Event::Mouse(mouse::Event::WheelScrolled {
delta_y, ..
}) => {
// TODO: Configurable speed (?)
self.state.offset = (self.state.offset as i32
- delta_y.round() as i32 * 15)
.max(0)
.min((content_bounds.height - bounds.height) as i32)
as u32;
}
_ => {}
}
}
let cursor_position = if is_mouse_over {
Point::new(
cursor_position.x,
cursor_position.y + self.state.offset as f32,
)
} else {
Point::new(cursor_position.x, -1.0)
};
self.content.on_event(
event,
layout.children().next().unwrap(),
cursor_position,
messages,
)
}
fn draw(
&self,
renderer: &mut Renderer,
layout: Layout<'_>,
cursor_position: Point,
) -> Renderer::Output {
self::Renderer::draw(renderer, &self, layout, cursor_position)
}
fn hash_layout(&self, state: &mut Hasher) {
self.content.hash_layout(state)
}
}
pub trait Renderer: crate::Renderer + Sized {
fn draw<Message>(
&mut self,
scrollable: &Scrollable<'_, Message, Self>,
layout: Layout<'_>,
cursor_position: Point,
) -> Self::Output;
}
impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a + self::Renderer + column::Renderer,
Message: 'static,
{
fn from(
scrollable: Scrollable<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(scrollable)
}
}

View file

@ -1,8 +1,8 @@
pub use iced_wgpu::{Primitive, Renderer}; pub use iced_wgpu::{Primitive, Renderer};
pub use iced_winit::{ pub use iced_winit::{
button, slider, text, winit, Align, Background, Checkbox, Color, Image, button, scrollable, slider, text, winit, Align, Background, Checkbox,
Justify, Length, Radio, Slider, Text, Color, Image, Justify, Length, Radio, Scrollable, Slider, Text,
}; };
pub type Element<'a, Message> = iced_winit::Element<'a, Message, Renderer>; pub type Element<'a, Message> = iced_winit::Element<'a, Message, Renderer>;

View file

@ -13,4 +13,5 @@ wgpu = { version = "0.3", git = "https://github.com/gfx-rs/wgpu-rs", rev = "cb25
wgpu_glyph = { version = "0.4", git = "https://github.com/hecrj/wgpu_glyph", rev = "48daa98f5f785963838b4345e86ac40eac095ba9" } wgpu_glyph = { version = "0.4", git = "https://github.com/hecrj/wgpu_glyph", rev = "48daa98f5f785963838b4345e86ac40eac095ba9" }
raw-window-handle = "0.1" raw-window-handle = "0.1"
image = "0.22" image = "0.22"
nalgebra = "0.18"
log = "0.4" log = "0.4"

View file

@ -23,4 +23,9 @@ pub enum Primitive {
path: String, path: String,
bounds: Rectangle, bounds: Rectangle,
}, },
Scrollable {
bounds: Rectangle,
offset: u32,
content: Box<Primitive>,
},
} }

View file

@ -20,6 +20,7 @@ mod column;
mod image; mod image;
mod radio; mod radio;
mod row; mod row;
mod scrollable;
mod slider; mod slider;
mod text; mod text;
@ -31,8 +32,6 @@ pub struct Renderer {
quad_pipeline: quad::Pipeline, quad_pipeline: quad::Pipeline,
image_pipeline: crate::image::Pipeline, image_pipeline: crate::image::Pipeline,
quads: Vec<Quad>,
images: Vec<Image>,
glyph_brush: Rc<RefCell<GlyphBrush<'static, ()>>>, glyph_brush: Rc<RefCell<GlyphBrush<'static, ()>>>,
} }
@ -43,6 +42,24 @@ pub struct Target {
swap_chain: SwapChain, swap_chain: SwapChain,
} }
pub struct Layer {
quads: Vec<Quad>,
images: Vec<Image>,
layers: Vec<Layer>,
y_offset: u32,
}
impl Layer {
pub fn new(y_offset: u32) -> Self {
Self {
quads: Vec::new(),
images: Vec::new(),
layers: Vec::new(),
y_offset,
}
}
}
impl Renderer { impl Renderer {
fn new<W: HasRawWindowHandle>(window: &W) -> Self { fn new<W: HasRawWindowHandle>(window: &W) -> Self {
let adapter = Adapter::request(&RequestAdapterOptions { let adapter = Adapter::request(&RequestAdapterOptions {
@ -79,8 +96,6 @@ impl Renderer {
quad_pipeline, quad_pipeline,
image_pipeline, image_pipeline,
quads: Vec::new(),
images: Vec::new(),
glyph_brush: Rc::new(RefCell::new(glyph_brush)), glyph_brush: Rc::new(RefCell::new(glyph_brush)),
} }
} }
@ -132,27 +147,10 @@ impl Renderer {
depth_stencil_attachment: None, depth_stencil_attachment: None,
}); });
self.draw_primitive(primitive); let mut layer = Layer::new(0);
self.quad_pipeline.draw( self.draw_primitive(primitive, &mut layer);
&mut self.device, self.flush(target.transformation, &layer, &mut encoder, &frame.view);
&mut encoder,
&self.quads,
target.transformation,
&frame.view,
);
self.quads.clear();
self.image_pipeline.draw(
&mut self.device,
&mut encoder,
&self.images,
target.transformation,
&frame.view,
);
self.images.clear();
self.glyph_brush self.glyph_brush
.borrow_mut() .borrow_mut()
@ -170,13 +168,13 @@ impl Renderer {
*mouse_cursor *mouse_cursor
} }
fn draw_primitive(&mut self, primitive: &Primitive) { fn draw_primitive(&mut self, primitive: &Primitive, layer: &mut Layer) {
match primitive { match primitive {
Primitive::None => {} Primitive::None => {}
Primitive::Group { primitives } => { Primitive::Group { primitives } => {
// TODO: Inspect a bit and regroup (?) // TODO: Inspect a bit and regroup (?)
for primitive in primitives { for primitive in primitives {
self.draw_primitive(primitive) self.draw_primitive(primitive, layer)
} }
} }
Primitive::Text { Primitive::Text {
@ -244,7 +242,7 @@ impl Renderer {
background, background,
border_radius, border_radius,
} => { } => {
self.quads.push(Quad { layer.quads.push(Quad {
position: [bounds.x, bounds.y], position: [bounds.x, bounds.y],
scale: [bounds.width, bounds.height], scale: [bounds.width, bounds.height],
color: match background { color: match background {
@ -254,12 +252,55 @@ impl Renderer {
}); });
} }
Primitive::Image { path, bounds } => { Primitive::Image { path, bounds } => {
self.images.push(Image { layer.images.push(Image {
path: path.clone(), path: path.clone(),
position: [bounds.x, bounds.y], position: [bounds.x, bounds.y],
scale: [bounds.width, bounds.height], scale: [bounds.width, bounds.height],
}); });
} }
Primitive::Scrollable {
bounds,
offset,
content,
} => {
let mut new_layer = Layer::new(layer.y_offset + offset);
// TODO: Primitive culling
self.draw_primitive(content, &mut new_layer);
layer.layers.push(new_layer);
}
}
}
fn flush(
&mut self,
transformation: Transformation,
layer: &Layer,
encoder: &mut wgpu::CommandEncoder,
target: &wgpu::TextureView,
) {
let translated = transformation
* Transformation::translate(0.0, -(layer.y_offset as f32));
self.quad_pipeline.draw(
&mut self.device,
encoder,
&layer.quads,
transformation,
target,
);
self.image_pipeline.draw(
&mut self.device,
encoder,
&layer.images,
translated,
target,
);
for layer in layer.layers.iter() {
self.flush(transformation, layer, encoder, target);
} }
} }
} }

View file

@ -0,0 +1,70 @@
use crate::{Primitive, Renderer};
use iced_native::{
scrollable, Background, Color, Layout, Point, Rectangle, Scrollable, Widget,
};
impl scrollable::Renderer for Renderer {
fn draw<Message>(
&mut self,
scrollable: &Scrollable<'_, Message, Self>,
layout: Layout<'_>,
cursor_position: Point,
) -> Self::Output {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let content = layout.children().next().unwrap();
let content_bounds = content.bounds();
let cursor_position = if bounds.contains(cursor_position) {
Point::new(
cursor_position.x,
cursor_position.y + scrollable.state.offset as f32,
)
} else {
Point::new(cursor_position.x, -1.0)
};
let (content, mouse_cursor) =
scrollable.content.draw(self, content, cursor_position);
let primitive = Primitive::Scrollable {
bounds,
offset: scrollable.state.offset,
content: Box::new(content),
};
(
Primitive::Group {
primitives: if is_mouse_over
&& content_bounds.height > bounds.height
{
let ratio = bounds.height / content_bounds.height;
let scrollbar_height = bounds.height * ratio;
let y_offset = scrollable.state.offset as f32 * ratio;
let scrollbar = Primitive::Quad {
bounds: Rectangle {
x: bounds.x + bounds.width - 12.0,
y: bounds.y + y_offset,
width: 10.0,
height: scrollbar_height,
},
background: Background::Color(Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.5,
}),
border_radius: 5,
};
vec![primitive, scrollbar]
} else {
vec![primitive]
},
},
mouse_cursor,
)
}
}

View file

@ -1,30 +1,59 @@
#[derive(Debug, Clone, Copy)] use nalgebra::Matrix3;
pub struct Transformation([f32; 16]); use std::ops::Mul;
/// A 2D transformation matrix.
///
/// It can be used to apply a transformation to a [`Target`].
///
/// [`Target`]: struct.Target.html
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transformation(Matrix3<f32>);
impl Transformation { impl Transformation {
#[rustfmt::skip] /// Get the identity transformation.
pub fn identity() -> Self { pub fn identity() -> Transformation {
Transformation([ Transformation(Matrix3::identity())
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
])
} }
/// Creates an orthographic projection.
///
/// You should rarely need this. On creation, a [`Target`] is automatically
/// set up with the correct orthographic projection.
///
/// [`Target`]: struct.Target.html
#[rustfmt::skip] #[rustfmt::skip]
pub fn orthographic(width: u16, height: u16) -> Self { pub fn orthographic(width: u16, height: u16) -> Transformation {
Transformation([ Transformation(nalgebra::Matrix3::new(
2.0 / width as f32, 0.0, 0.0, 0.0, 2.0 / f32::from(width), 0.0, -1.0,
0.0, 2.0 / height as f32, 0.0, 0.0, 0.0, 2.0 / f32::from(height), -1.0,
0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0
-1.0, -1.0, 0.0, 1.0, ))
]) }
/// Creates a translate transformation.
///
/// You can use this to pan your camera, for example.
pub fn translate(x: f32, y: f32) -> Transformation {
Transformation(Matrix3::new_translation(&nalgebra::Vector2::new(x, y)))
}
}
impl Mul for Transformation {
type Output = Self;
fn mul(self, rhs: Self) -> Self {
Transformation(self.0 * rhs.0)
} }
} }
impl From<Transformation> for [f32; 16] { impl From<Transformation> for [f32; 16] {
fn from(transformation: Transformation) -> [f32; 16] { #[rustfmt::skip]
transformation.0 fn from(t: Transformation) -> [f32; 16] {
[
t.0[0], t.0[1], 0.0, t.0[2],
t.0[3], t.0[4], 0.0, t.0[5],
0.0, 0.0, -1.0, 0.0,
t.0[6], t.0[7], 0.0, t.0[8]
]
} }
} }

View file

@ -136,6 +136,17 @@ pub trait Application {
state: conversion::button_state(state), state: conversion::button_state(state),
})); }));
} }
WindowEvent::MouseWheel { delta, .. } => match delta {
winit::event::MouseScrollDelta::LineDelta(
delta_x,
delta_y,
) => {
events.push(Event::Mouse(
mouse::Event::WheelScrolled { delta_x, delta_y },
));
}
_ => {}
},
WindowEvent::CloseRequested => { WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit; *control_flow = ControlFlow::Exit;
} }