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;
pub mod button;
pub mod scrollable;
pub mod slider;
pub mod text;
@ -26,6 +27,9 @@ pub use slider::Slider;
#[doc(no_inline)]
pub use text::Text;
#[doc(no_inline)]
pub use scrollable::Scrollable;
pub use checkbox::Checkbox;
pub use column::Column;
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
.compute_layout(geometry::Size::undefined())
.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 radio;
pub mod row;
pub mod scrollable;
pub mod slider;
pub mod text;
@ -42,6 +43,8 @@ pub use radio::Radio;
#[doc(no_inline)]
pub use row::Row;
#[doc(no_inline)]
pub use scrollable::Scrollable;
#[doc(no_inline)]
pub use slider::Slider;
#[doc(no_inline)]
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_winit::{
button, slider, text, winit, Align, Background, Checkbox, Color, Image,
Justify, Length, Radio, Slider, Text,
button, scrollable, slider, text, winit, Align, Background, Checkbox,
Color, Image, Justify, Length, Radio, Scrollable, Slider, Text,
};
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" }
raw-window-handle = "0.1"
image = "0.22"
nalgebra = "0.18"
log = "0.4"

View file

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

View file

@ -20,6 +20,7 @@ mod column;
mod image;
mod radio;
mod row;
mod scrollable;
mod slider;
mod text;
@ -31,8 +32,6 @@ pub struct Renderer {
quad_pipeline: quad::Pipeline,
image_pipeline: crate::image::Pipeline,
quads: Vec<Quad>,
images: Vec<Image>,
glyph_brush: Rc<RefCell<GlyphBrush<'static, ()>>>,
}
@ -43,6 +42,24 @@ pub struct Target {
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 {
fn new<W: HasRawWindowHandle>(window: &W) -> Self {
let adapter = Adapter::request(&RequestAdapterOptions {
@ -79,8 +96,6 @@ impl Renderer {
quad_pipeline,
image_pipeline,
quads: Vec::new(),
images: Vec::new(),
glyph_brush: Rc::new(RefCell::new(glyph_brush)),
}
}
@ -132,27 +147,10 @@ impl Renderer {
depth_stencil_attachment: None,
});
self.draw_primitive(primitive);
let mut layer = Layer::new(0);
self.quad_pipeline.draw(
&mut self.device,
&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.draw_primitive(primitive, &mut layer);
self.flush(target.transformation, &layer, &mut encoder, &frame.view);
self.glyph_brush
.borrow_mut()
@ -170,13 +168,13 @@ impl Renderer {
*mouse_cursor
}
fn draw_primitive(&mut self, primitive: &Primitive) {
fn draw_primitive(&mut self, primitive: &Primitive, layer: &mut Layer) {
match primitive {
Primitive::None => {}
Primitive::Group { primitives } => {
// TODO: Inspect a bit and regroup (?)
for primitive in primitives {
self.draw_primitive(primitive)
self.draw_primitive(primitive, layer)
}
}
Primitive::Text {
@ -244,7 +242,7 @@ impl Renderer {
background,
border_radius,
} => {
self.quads.push(Quad {
layer.quads.push(Quad {
position: [bounds.x, bounds.y],
scale: [bounds.width, bounds.height],
color: match background {
@ -254,12 +252,55 @@ impl Renderer {
});
}
Primitive::Image { path, bounds } => {
self.images.push(Image {
layer.images.push(Image {
path: path.clone(),
position: [bounds.x, bounds.y],
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)]
pub struct Transformation([f32; 16]);
use nalgebra::Matrix3;
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 {
#[rustfmt::skip]
pub fn identity() -> Self {
Transformation([
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,
])
/// Get the identity transformation.
pub fn identity() -> Transformation {
Transformation(Matrix3::identity())
}
/// 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]
pub fn orthographic(width: u16, height: u16) -> Self {
Transformation([
2.0 / width as f32, 0.0, 0.0, 0.0,
0.0, 2.0 / height as f32, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
-1.0, -1.0, 0.0, 1.0,
])
pub fn orthographic(width: u16, height: u16) -> Transformation {
Transformation(nalgebra::Matrix3::new(
2.0 / f32::from(width), 0.0, -1.0,
0.0, 2.0 / f32::from(height), -1.0,
0.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] {
fn from(transformation: Transformation) -> [f32; 16] {
transformation.0
#[rustfmt::skip]
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),
}));
}
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 => {
*control_flow = ControlFlow::Exit;
}