diff --git a/Cargo.toml b/Cargo.toml index 8473b89..3161fb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" [dependencies] crossterm = "0.22.1" +futures = "*" diff --git a/src/box_constraints.rs b/src/box_constraints.rs new file mode 100644 index 0000000..4c6fd52 --- /dev/null +++ b/src/box_constraints.rs @@ -0,0 +1,206 @@ +use crate::size::Size; + +#[derive(Clone, Copy, Debug)] +pub struct BoxConstraints { + min: Size, + max: Size, +} + +impl BoxConstraints { + /// An unbounded box constraints object. + /// + /// Can be satisfied by any nonnegative size. + pub const BIG: BoxConstraints = BoxConstraints { + min: Size::ZERO, + max: Size::MAX, + }; + + /// Create a new box constraints object. + /// + /// Create constraints based on minimum and maximum size. + /// + /// The given sizes are also [rounded away from zero], + /// so that the layout is aligned to integers. + /// + /// [rounded away from zero]: struct.Size.html#method.expand + pub fn new(min: Size, max: Size) -> BoxConstraints { + BoxConstraints { min, max } + } + + /// Create a "tight" box constraints object. + /// + /// A "tight" constraint can only be satisfied by a single size. + /// + /// The given size is also [rounded away from zero], + /// so that the layout is aligned to integers. + /// + /// [rounded away from zero]: struct.Size.html#method.expand + pub fn tight(size: Size) -> BoxConstraints { + let size = size; + BoxConstraints { + min: size, + max: size, + } + } + + /// Create a "loose" version of the constraints. + /// + /// Make a version with zero minimum size, but the same maximum size. + pub fn loosen(&self) -> BoxConstraints { + BoxConstraints { + min: Size::ZERO, + max: self.max, + } + } + + /// Clamp a given size so that it fits within the constraints. + /// + /// The given size is also [rounded away from zero], + /// so that the layout is aligned to integers. + /// + /// [rounded away from zero]: struct.Size.html#method.expand + pub fn constrain(&self, size: impl Into) -> Size { + size.into().clamp(self.min, self.max) + } + + /// Returns the max size of these constraints. + pub fn max(&self) -> Size { + self.max + } + + /// Returns the min size of these constraints. + pub fn min(&self) -> Size { + self.min + } + + /// Whether there is an upper bound on the width. + pub fn is_width_bounded(&self) -> bool { + true + } + + /// Whether there is an upper bound on the height. + pub fn is_height_bounded(&self) -> bool { + true + } + + /// Shrink min and max constraints by size + /// + /// The given size is also [rounded away from zero], + /// so that the layout is aligned to integers. + /// + /// [rounded away from zero]: struct.Size.html#method.expand + pub fn shrink(&self, diff: impl Into) -> BoxConstraints { + let diff = diff.into(); + let min = Size::new( + (self.min().width - diff.width).max(0), + (self.min().height - diff.height).max(0), + ); + let max = Size::new( + (self.max().width - diff.width).max(0), + (self.max().height - diff.height).max(0), + ); + + BoxConstraints::new(min, max) + } + + /// Test whether these constraints contain the given `Size`. + pub fn contains(&self, size: impl Into) -> bool { + let size = size.into(); + (self.min.width <= size.width && size.width <= self.max.width) + && (self.min.height <= size.height && size.height <= self.max.height) + } + + // pub fn constrain_aspect_ratio(&self, aspect_ratio: usize, width: usize) -> Size { + // // Minimizing/maximizing based on aspect ratio seems complicated, but in reality everything + // // is linear, so the amount of work to do is low. + // let ideal_size = Size { + // width, + // height: width * aspect_ratio, + // }; + + // // Firstly check if we can simply return the exact requested + // if self.contains(ideal_size) { + // return ideal_size; + // } + + // // Then we check if any `Size`s with our desired aspect ratio are inside the constraints. + // // TODO this currently outputs garbage when things are < 0. + // let min_w_min_h = self.min.height / self.min.width; + // let max_w_min_h = self.min.height / self.max.width; + // let min_w_max_h = self.max.height / self.min.width; + // let max_w_max_h = self.max.height / self.max.width; + + // // When the aspect ratio line crosses the constraints, the closest point must be one of the + // // two points where the aspect ratio enters/exits. + + // // When the aspect ratio line doesn't intersect the box of possible sizes, the closest + // // point must be either (max width, min height) or (max height, min width). So all we have + // // to do is check which one of these has the closest aspect ratio. + + // // Check each possible intersection (or not) of the aspect ratio line with the constraints + // if aspect_ratio > min_w_max_h { + // // outside max height min width + // Size { + // width: self.min.width, + // height: self.max.height, + // } + // } else if aspect_ratio < max_w_min_h { + // // outside min height max width + // Size { + // width: self.max.width, + // height: self.min.height, + // } + // } else if aspect_ratio > min_w_min_h { + // // hits the constraints on the min width line + // if width < self.min.width { + // // we take the point on the min width + // Size { + // width: self.min.width, + // height: self.min.width * aspect_ratio, + // } + // } else if aspect_ratio < max_w_max_h { + // // exits through max.width + // Size { + // width: self.max.width, + // height: self.max.width * aspect_ratio, + // } + // } else { + // // exits through max.height + // Size { + // width: self.max.height * aspect_ratio.recip(), + // height: self.max.height, + // } + // } + // } else { + // // final case is where we hit constraints on the min height line + // if width < self.min.width { + // // take the point on the min height + // Size { + // width: self.min.height * aspect_ratio.recip(), + // height: self.min.height, + // } + // } else if aspect_ratio > max_w_max_h { + // // exit thru max height + // Size { + // width: self.max.height * aspect_ratio.recip(), + // height: self.max.height, + // } + // } else { + // // exit thru max width + // Size { + // width: self.max.width, + // height: self.max.width * aspect_ratio, + // } + // } + // } + // } +} + +impl From<(u16, u16)> for BoxConstraints { + fn from(s: (u16, u16)) -> Self { + Self { + min: Size::ZERO, + max: s.into(), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 13bacf7..3774d7f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,55 +1,116 @@ use crossterm::{ - cursor, queue, - style::{self, Stylize}, - terminal::{Clear, ClearType, size}, - ExecutableCommand, QueueableCommand, Result as CTRes, + cursor, queue, + style::{self, Stylize}, + terminal::{size, Clear, ClearType}, + ExecutableCommand, QueueableCommand, Result as CTRes, }; -use std::io::{stdout, Stdout, Write}; -use std::sync::Arc; +use std::{ + io::{stdout, Stdout, Write}, + ops::DerefMut, +}; +use std::{ops::Deref, sync::Arc}; +mod box_constraints; +mod point; +mod rect; +mod size; +mod theme; +mod vec2; mod widget; -use widget::*; - -pub struct Size { - a: usize, - b: usize, -} - -impl From<(u16, u16)> for Size { - fn from(s: (u16, u16)) -> Self { - Self { - a: s.0 as usize, - b: s.1 as usize, - } - } -} - -pub struct Point { - a: usize, - b: usize, -} +pub use box_constraints::*; +pub use point::*; +pub use rect::*; +pub use size::*; +pub use vec2::*; +pub use widget::*; pub trait Data {} impl Data for Arc {} -struct Window<'a, T: Data> { - out: Stdout, - root_widget: Box<&'a dyn Widget>, +pub struct DataWrapper { + changed: bool, + data: T, } -impl<'a, T: Data> Window<'a, T> { - pub fn new(widget: &'a dyn Widget) -> Self { - Self { - out: stdout(), - root_widget: Box::new(widget), - } - } - pub fn draw(&mut self) -> CTRes<()> { - queue![self.out, Clear(ClearType::All)]?; - let terminal_size = size(); - self.root_widget.layout(terminal_size.into()); - self.out.flush(); - Ok(()) - } +impl DataWrapper { + pub fn new(data: T) -> Self { + Self { + changed: true, + data, + } + } +} + +impl DataWrapper { + fn changed(&mut self) -> bool { + if self.changed { + self.changed = false; + true + } else { + false + } + } +} + +impl Deref for DataWrapper { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DerefMut for DataWrapper { + fn deref_mut(&mut self) -> &mut Self::Target { + self.changed = true; + &mut self.data + } +} + +pub struct Window { + out: Stdout, + buf: Vec, + data: DataWrapper, + root_widget: Box>, +} + +impl<'a, T: Data> Window { + pub fn new(data: T, root: W) -> Self + where + W: Widget + 'static, + { + Self { + out: stdout(), + buf: vec![], + data: DataWrapper::new(data), + root_widget: Box::new(root), + } + } + fn draw(&mut self) -> CTRes<()> { + self.root_widget.event(&mut self.data); + self.root_widget.update(&self.data); + if self.data.changed() { + queue![self.out, Clear(ClearType::All)]?; + self.out.flush()?; + let terminal_size = size()?; + self.buf = vec![' '; terminal_size.0 as usize * terminal_size.1 as usize]; + self.root_widget.deref_mut().layout(&terminal_size.into()); + self + .root_widget + .deref_mut() + .paint(&mut self.buf, &terminal_size.into()); + for ch in &self.buf { + print!["{}", ch]; + } + self.out.flush()?; + } + Ok(()) + } + pub fn run(&mut self) -> CTRes<()> { + loop { + self.draw()?; + } + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..90ffff5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,33 @@ +use std::ops::Deref; +use std::sync::{Arc, Mutex}; + +use tuid::Data; +use tuid::*; + +#[derive(Debug, Default)] +struct ExampleAppState { + pub value: i32, +} + +impl Data for ExampleAppState {} + +fn make_ui() -> impl Widget>> { + let t1 = Text::new(Box::new( + |data: &DataWrapper>>| { + let s = format!["Hello, {}!", data.deref().lock().unwrap().value]; + s + }, + )); + + // let flex = Flex::new(); + + // flex + + t1 +} + +fn main() { + let main_state = Arc::new(Mutex::new(ExampleAppState::default())); + let mut window = Window::new(main_state, make_ui()); + window.run().unwrap(); +} diff --git a/src/point.rs b/src/point.rs new file mode 100644 index 0000000..47d57e1 --- /dev/null +++ b/src/point.rs @@ -0,0 +1,5 @@ +#[derive(Clone, Copy, Debug)] +pub struct Point { + pub x: usize, + pub y: usize, +} diff --git a/src/rect.rs b/src/rect.rs new file mode 100644 index 0000000..86bf347 --- /dev/null +++ b/src/rect.rs @@ -0,0 +1,7 @@ +#[derive(Clone, Copy, Debug)] +pub struct Rect { + pub x0: usize, + pub y0: usize, + pub x1: usize, + pub y1: usize, +} diff --git a/src/size.rs b/src/size.rs new file mode 100644 index 0000000..0d0e60c --- /dev/null +++ b/src/size.rs @@ -0,0 +1,37 @@ +#[derive(Clone, Copy, Debug)] +pub struct Size { + pub width: usize, + pub height: usize, +} + +impl Size { + pub const ZERO: Self = Self { + width: 0, + height: 0, + }; + + pub const MAX: Self = Self { + width: usize::MAX, + height: usize::MAX, + }; + + pub fn new(width: usize, height: usize) -> Self { + Self { width, height } + } + + pub fn clamp(&self, min: Size, max: Size) -> Size { + Self { + width: self.width.clamp(min.width, max.width), + height: self.height.clamp(min.height, max.height), + } + } +} + +impl From<(u16, u16)> for Size { + fn from(s: (u16, u16)) -> Self { + Self { + width: s.0 as usize, + height: s.1 as usize, + } + } +} diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..d8dac95 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,2 @@ +pub const WIDGET_PADDING_VERTICAL: usize = 1; +pub const WIDGET_PADDING_HORIZONTAL: usize = 1; \ No newline at end of file diff --git a/src/vec2.rs b/src/vec2.rs new file mode 100644 index 0000000..4d6d1fd --- /dev/null +++ b/src/vec2.rs @@ -0,0 +1,5 @@ +#[derive(Clone, Copy, Debug)] +pub struct Vec2 { + pub x: usize, + pub y: usize, +} diff --git a/src/widget/flex/axis.rs b/src/widget/flex/axis.rs new file mode 100644 index 0000000..e2ed120 --- /dev/null +++ b/src/widget/flex/axis.rs @@ -0,0 +1,101 @@ +use crate::{rect::Rect, size::Size, vec2::Vec2, Data, Point, box_constraints::BoxConstraints}; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Axis { + /// The x axis + Horizontal, + /// The y axis + Vertical, +} + +impl Axis { + /// Get the axis perpendicular to this one. + pub fn cross(self) -> Axis { + match self { + Axis::Horizontal => Axis::Vertical, + Axis::Vertical => Axis::Horizontal, + } + } + + /// Extract from the argument the magnitude along this axis + pub fn major(self, coords: Size) -> usize { + match self { + Axis::Horizontal => coords.width, + Axis::Vertical => coords.height, + } + } + + /// Extract from the argument the magnitude along the perpendicular axis + pub fn minor(self, coords: Size) -> usize { + self.cross().major(coords) + } + + /// Extract the extent of the argument in this axis as a pair. + pub fn major_span(self, rect: Rect) -> (usize, usize) { + match self { + Axis::Horizontal => (rect.x0, rect.x1), + Axis::Vertical => (rect.y0, rect.y1), + } + } + + /// Extract the extent of the argument in the minor axis as a pair. + pub fn minor_span(self, rect: Rect) -> (usize, usize) { + self.cross().major_span(rect) + } + + /// Extract the coordinate locating the argument with respect to this axis. + pub fn major_pos(self, pos: Point) -> usize { + match self { + Axis::Horizontal => pos.x, + Axis::Vertical => pos.y, + } + } + + /// Extract the coordinate locating the argument with respect to this axis. + pub fn major_vec(self, vec: Vec2) -> usize { + match self { + Axis::Horizontal => vec.x, + Axis::Vertical => vec.y, + } + } + + /// Extract the coordinate locating the argument with respect to the perpendicular axis. + pub fn minor_pos(self, pos: Point) -> usize { + self.cross().major_pos(pos) + } + + /// Extract the coordinate locating the argument with respect to the perpendicular axis. + pub fn minor_vec(self, vec: Vec2) -> usize { + self.cross().major_vec(vec) + } + + /// Arrange the major and minor measurements with respect to this axis such that it forms + /// an (x, y) pair. + pub fn pack(self, major: usize, minor: usize) -> (usize, usize) { + match self { + Axis::Horizontal => (major, minor), + Axis::Vertical => (minor, major), + } + } + + /// Generate constraints with new values on the major axis. + pub(crate) fn constraints( + self, + bc: &BoxConstraints, + min_major: usize, + major: usize, + ) -> BoxConstraints { + match self { + Axis::Horizontal => BoxConstraints::new( + Size::new(min_major, bc.min().height), + Size::new(major, bc.max().height), + ), + Axis::Vertical => BoxConstraints::new( + Size::new(bc.min().width, min_major), + Size::new(bc.max().width, major), + ), + } + } +} + +impl Data for Axis {} diff --git a/src/widget/flex/child.rs b/src/widget/flex/child.rs new file mode 100644 index 0000000..1071a63 --- /dev/null +++ b/src/widget/flex/child.rs @@ -0,0 +1,36 @@ +use crate::widget::{Widget, WidgetPod}; + +use super::cross_axis_alignment::CrossAxisAlignment; + +pub enum Child { + Fixed { + widget: WidgetPod>>, + alignment: Option, + }, + Flex { + widget: WidgetPod>>, + alignment: Option, + flex: f64, + }, + FixedSpacer( + // KeyOrValue, + usize, + usize, + ), + FlexedSpacer(f64, f64), +} + +impl Child { + fn widget_mut(&mut self) -> Option<&mut WidgetPod>>> { + match self { + Child::Fixed { widget, .. } | Child::Flex { widget, .. } => Some(widget), + _ => None, + } + } + fn widget(&self) -> Option<&WidgetPod>>> { + match self { + Child::Fixed { widget, .. } | Child::Flex { widget, .. } => Some(widget), + _ => None, + } + } +} diff --git a/src/widget/flex/cross_axis_alignment.rs b/src/widget/flex/cross_axis_alignment.rs new file mode 100644 index 0000000..8ebeb12 --- /dev/null +++ b/src/widget/flex/cross_axis_alignment.rs @@ -0,0 +1,46 @@ +use crate::Data; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CrossAxisAlignment { + /// Top or leading. + /// + /// In a vertical container, widgets are top aligned. In a horiziontal + /// container, their leading edges are aligned. + Start, + /// Widgets are centered in the container. + Center, + /// Bottom or trailing. + /// + /// In a vertical container, widgets are bottom aligned. In a horiziontal + /// container, their trailing edges are aligned. + End, + /// Align on the baseline. + /// + /// In a horizontal container, widgets are aligned along the calculated + /// baseline. In a vertical container, this is equivalent to `End`. + /// + /// The calculated baseline is the maximum baseline offset of the children. + Baseline, + /// Fill the available space. + /// + /// The size on this axis is the size of the largest widget; + /// other widgets must fill that space. + Fill, +} + +impl CrossAxisAlignment { + /// Given the difference between the size of the container and the size + /// of the child (on their minor axis) return the necessary offset for + /// this alignment. + fn align(self, val: f64) -> f64 { + match self { + CrossAxisAlignment::Start => 0.0, + // in vertical layout, baseline is equivalent to center + CrossAxisAlignment::Center | CrossAxisAlignment::Baseline => (val / 2.0).round(), + CrossAxisAlignment::End => val, + CrossAxisAlignment::Fill => 0.0, + } + } +} + +impl Data for CrossAxisAlignment {} diff --git a/src/widget/flex/flex_params.rs b/src/widget/flex/flex_params.rs new file mode 100644 index 0000000..c68ed32 --- /dev/null +++ b/src/widget/flex/flex_params.rs @@ -0,0 +1,37 @@ +use super::cross_axis_alignment::CrossAxisAlignment; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct FlexParams { + pub(super) flex: f64, + pub(super) alignment: Option, +} + +impl FlexParams { + /// Create custom `FlexParams` with a specific `flex_factor` and an optional + /// [`CrossAxisAlignment`]. + /// + /// You likely only need to create these manually if you need to specify + /// a custom alignment; if you only need to use a custom `flex_factor` you + /// can pass an `f64` to any of the functions that take `FlexParams`. + /// + /// By default, the widget uses the alignment of its parent [`Flex`] container. + pub fn new(flex: f64, alignment: impl Into>) -> Self { + #[cfg(debug)] + if flex <= 0.0 { + panic!("Flex value should be > 0.0. Flex given was: {}", flex); + } + + let flex = flex.max(0.0); + + FlexParams { + flex, + alignment: alignment.into(), + } + } +} + +impl From for FlexParams { + fn from(flex: f64) -> FlexParams { + FlexParams::new(flex, None) + } +} diff --git a/src/widget/flex/main_axis_alignment.rs b/src/widget/flex/main_axis_alignment.rs new file mode 100644 index 0000000..4c0ac5b --- /dev/null +++ b/src/widget/flex/main_axis_alignment.rs @@ -0,0 +1,26 @@ +use crate::Data; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum MainAxisAlignment { + /// Top or leading. + /// + /// Children are aligned with the top or leading edge, without padding. + Start, + /// Children are centered, without padding. + Center, + /// Bottom or trailing. + /// + /// Children are aligned with the bottom or trailing edge, without padding. + End, + /// Extra space is divided evenly between each child. + SpaceBetween, + /// Extra space is divided evenly between each child, as well as at the ends. + SpaceEvenly, + /// Space between each child, with less at the start and end. + /// + /// This divides space such that each child is separated by `n` units, + /// and the start and end have `n/2` units of padding. + SpaceAround, +} + +impl Data for MainAxisAlignment {} diff --git a/src/widget/flex/mod.rs b/src/widget/flex/mod.rs new file mode 100644 index 0000000..68e8cc9 --- /dev/null +++ b/src/widget/flex/mod.rs @@ -0,0 +1,530 @@ +// This is pretty much all stolen from linebender/druid. +// They have the license on their github repo. +// I didn't include it here because it might change- +// just go ask them what the license says and don't bother me. + +use crate::{box_constraints::BoxConstraints, Data, Size, Widget}; + +use super::WidgetPod; + +mod axis; +mod child; +mod cross_axis_alignment; +mod flex_params; +mod main_axis_alignment; +mod spacing; +use axis::*; +use child::*; +use cross_axis_alignment::*; +use flex_params::*; +use main_axis_alignment::*; +use spacing::*; + +pub struct Flex { + direction: Axis, + cross_alignment: CrossAxisAlignment, + main_alignment: MainAxisAlignment, + fill_major_axis: bool, + children: Vec>, +} + +impl Flex { + /// Create a new Flex oriented along the provided axis. + pub fn for_axis(axis: Axis) -> Self { + Flex { + direction: axis, + children: Vec::new(), + cross_alignment: CrossAxisAlignment::Center, + main_alignment: MainAxisAlignment::Start, + fill_major_axis: false, + } + } + + /// Create a new horizontal stack. + /// + /// The child widgets are laid out horizontally, from left to right. + /// + pub fn row() -> Self { + Self::for_axis(Axis::Horizontal) + } + + /// Create a new vertical stack. + /// + /// The child widgets are laid out vertically, from top to bottom. + pub fn column() -> Self { + Self::for_axis(Axis::Vertical) + } + + /// Builder-style method for specifying the childrens' [`CrossAxisAlignment`]. + /// + /// [`CrossAxisAlignment`]: enum.CrossAxisAlignment.html + pub fn cross_axis_alignment(mut self, alignment: CrossAxisAlignment) -> Self { + self.cross_alignment = alignment; + self + } + + /// Builder-style method for specifying the childrens' [`MainAxisAlignment`]. + /// + /// [`MainAxisAlignment`]: enum.MainAxisAlignment.html + pub fn main_axis_alignment(mut self, alignment: MainAxisAlignment) -> Self { + self.main_alignment = alignment; + self + } + + /// Builder-style method for setting whether the container must expand + /// to fill the available space on its main axis. + /// + /// If any children have flex then this container will expand to fill all + /// available space on its main axis; But if no children are flex, + /// this flag determines whether or not the container should shrink to fit, + /// or must expand to fill. + /// + /// If it expands, and there is extra space left over, that space is + /// distributed in accordance with the [`MainAxisAlignment`]. + /// + /// The default value is `false`. + /// + /// [`MainAxisAlignment`]: enum.MainAxisAlignment.html + pub fn must_fill_main_axis(mut self, fill: bool) -> Self { + self.fill_major_axis = fill; + self + } + + /// Builder-style variant of `add_child`. + /// + /// Convenient for assembling a group of widgets in a single expression. + pub fn with_child(mut self, child: impl Widget + 'static) -> Self { + self.add_child(child); + self + } + + /// Builder-style method to add a flexible child to the container. + /// + /// This method is used when you need more control over the behaviour + /// of the widget you are adding. In the general case, this likely + /// means giving that child a 'flex factor', but it could also mean + /// giving the child a custom [`CrossAxisAlignment`], or a combination + /// of the two. + /// + /// This function takes a child widget and [`FlexParams`]; importantly + /// you can pass in a float as your [`FlexParams`] in most cases. + /// + /// For the non-builder varient, see [`add_flex_child`]. + /// + /// # Examples + /// + /// ``` + /// use druid::widget::{Flex, FlexParams, Label, Slider, CrossAxisAlignment}; + /// + /// let my_row = Flex::row() + /// .with_flex_child(Slider::new(), 1.0) + /// .with_flex_child(Slider::new(), FlexParams::new(1.0, CrossAxisAlignment::End)); + /// ``` + /// + /// [`FlexParams`]: struct.FlexParams.html + /// [`add_flex_child`]: #method.add_flex_child + /// [`CrossAxisAlignment`]: enum.CrossAxisAlignment.html + pub fn with_flex_child( + mut self, + child: impl Widget + 'static, + params: impl Into, + ) -> Self { + self.add_flex_child(child, params); + self + } + + /// Builder-style method to add a spacer widget with a standard size. + /// + /// The actual value of this spacer depends on whether this container is + /// a row or column, as well as theme settings. + pub fn with_default_spacer(mut self) -> Self { + self.add_default_spacer(); + self + } + + /// Builder-style method for adding a fixed-size spacer to the container. + /// + /// If you are laying out standard controls in this container, you should + /// generally prefer to use [`add_default_spacer`]. + /// + /// [`add_default_spacer`]: #method.add_default_spacer + pub fn with_spacer(mut self, len: usize) -> Self { + self.add_spacer(len); + self + } + + /// Builder-style method for adding a `flex` spacer to the container. + pub fn with_flex_spacer(mut self, flex: f64) -> Self { + self.add_flex_spacer(flex); + self + } + + /// Set the childrens' [`CrossAxisAlignment`]. + /// + /// [`CrossAxisAlignment`]: enum.CrossAxisAlignment.html + pub fn set_cross_axis_alignment(&mut self, alignment: CrossAxisAlignment) { + self.cross_alignment = alignment; + } + + /// Set the childrens' [`MainAxisAlignment`]. + /// + /// [`MainAxisAlignment`]: enum.MainAxisAlignment.html + pub fn set_main_axis_alignment(&mut self, alignment: MainAxisAlignment) { + self.main_alignment = alignment; + } + + /// Set whether the container must expand to fill the available space on + /// its main axis. + pub fn set_must_fill_main_axis(&mut self, fill: bool) { + self.fill_major_axis = fill; + } + + /// Add a non-flex child widget. + /// + /// See also [`with_child`]. + /// + /// [`with_child`]: Flex::with_child + pub fn add_child(&mut self, child: impl Widget + 'static) { + let child = Child::Fixed { + widget: WidgetPod::new(Box::new(child)), + alignment: None, + }; + self.children.push(child); + } + + /// Add a flexible child widget. + /// + /// This method is used when you need more control over the behaviour + /// of the widget you are adding. In the general case, this likely + /// means giving that child a 'flex factor', but it could also mean + /// giving the child a custom [`CrossAxisAlignment`], or a combination + /// of the two. + /// + /// This function takes a child widget and [`FlexParams`]; importantly + /// you can pass in a float as your [`FlexParams`] in most cases. + /// + /// For the builder-style varient, see [`with_flex_child`]. + /// + /// # Examples + /// + /// ``` + /// use druid::widget::{Flex, FlexParams, Label, Slider, CrossAxisAlignment}; + /// + /// let mut my_row = Flex::row(); + /// my_row.add_flex_child(Slider::new(), 1.0); + /// my_row.add_flex_child(Slider::new(), FlexParams::new(1.0, CrossAxisAlignment::End)); + /// ``` + /// + /// [`with_flex_child`]: Flex::with_flex_child + pub fn add_flex_child( + &mut self, + child: impl Widget + 'static, + params: impl Into, + ) { + let params = params.into(); + let child = if params.flex > 0.0 { + Child::Flex { + widget: WidgetPod::new(Box::new(child)), + alignment: params.alignment, + flex: params.flex, + } + } else { + // tracing::warn!("Flex value should be > 0.0. To add a non-flex child use the add_child or with_child methods.\nSee the docs for more information: https://docs.rs/druid/0.7.0/druid/widget/struct.Flex.html"); + Child::Fixed { + widget: WidgetPod::new(Box::new(child)), + alignment: None, + } + }; + self.children.push(child); + } + + /// Add a spacer widget with a standard size. + /// + /// The actual value of this spacer depends on whether this container is + /// a row or column, as well as theme settings. + pub fn add_default_spacer(&mut self) { + let key = match self.direction { + Axis::Vertical => crate::theme::WIDGET_PADDING_VERTICAL, + Axis::Horizontal => crate::theme::WIDGET_PADDING_HORIZONTAL, + }; + self.add_spacer(key); + } + + /// Add an empty spacer widget with the given size. + /// + /// If you are laying out standard controls in this container, you should + /// generally prefer to use [`add_default_spacer`]. + /// + /// [`add_default_spacer`]: Flex::add_default_spacer + pub fn add_spacer(&mut self, len: usize) { + let new_child = Child::FixedSpacer(len, 0); + self.children.push(new_child); + } + + /// Add an empty spacer widget with a specific `flex` factor. + pub fn add_flex_spacer(&mut self, flex: f64) { + let flex = if flex >= 0.0 { + flex + } else { + debug_assert!( + flex >= 0.0, + "flex value for space should be greater than equal to 0, received: {}", + flex + ); + // tracing::warn!("Provided flex value was less than 0: {}", flex); + 0.0 + }; + let new_child = Child::FlexedSpacer(flex, 0.0); + self.children.push(new_child); + } +} + +impl Widget for Flex { + fn event(&mut self, data: ) { + for child in self.children.iter_mut().filter_map(|x| x.widget_mut()) { + child.event(ctx, event, data, env); + } + } + + // #[instrument(name = "Flex", level = "trace", skip(self, ctx, event, data, env))] + // fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + // for child in self.children.iter_mut().filter_map(|x| x.widget_mut()) { + // child.lifecycle(ctx, event, data, env); + // } + // } + + fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) { + for child in self.children.iter_mut() { + match child { + Child::Fixed { widget, .. } | Child::Flex { widget, .. } => { + widget.update(ctx, data, env) + } + Child::FixedSpacer(key_or_val, _) if ctx.env_key_changed(key_or_val) => { + ctx.request_layout() + } + _ => {} + } + } + } + + fn layout(&mut self, bc: &BoxConstraints) -> Size { + // bc.debug_check("Flex"); + // we loosen our constraints when passing to children. + let loosened_bc = bc.loosen(); + + // minor-axis values for all children + let mut minor = self.direction.minor(bc.min()); + // these two are calculated but only used if we're baseline aligned + let mut max_above_baseline = 0; + let mut max_below_baseline = 0; + let mut any_use_baseline = self.cross_alignment == CrossAxisAlignment::Baseline; + + // Measure non-flex children. + let mut major_non_flex = 0; + let mut flex_sum = 0.0; + for child in &mut self.children { + match child { + Child::Fixed { widget, alignment } => { + any_use_baseline &= *alignment == Some(CrossAxisAlignment::Baseline); + + let child_bc = self.direction.constraints(&loosened_bc, 0, usize::MAX); + let child_size = widget.layout(&child_bc); + let baseline_offset = 0; + + major_non_flex += self.direction.major(child_size); + minor = minor.max(self.direction.minor(child_size)); + max_above_baseline = max_above_baseline.max(child_size.height - baseline_offset); + max_below_baseline = max_below_baseline.max(baseline_offset); + } + Child::FixedSpacer(kv, calculated_siz) => { + *calculated_siz = *kv; + major_non_flex += *calculated_siz; + } + Child::Flex { flex, .. } | Child::FlexedSpacer(flex, _) => flex_sum += *flex, + } + } + + let total_major = self.direction.major(bc.max()); + let remaining = (total_major - major_non_flex).max(0.0); + let mut remainder: f64 = 0.0; + + let mut major_flex: f64 = 0.0; + let px_per_flex = remaining / flex_sum; + // Measure flex children. + for child in &mut self.children { + match child { + Child::Flex { widget, flex, .. } => { + let desired_major = (*flex) * px_per_flex + remainder; + let actual_major = desired_major.round(); + remainder = desired_major - actual_major; + + let child_bc = self.direction.constraints(&loosened_bc, 0.0, actual_major); + let child_size = widget.layout(ctx, &child_bc, data, env); + let baseline_offset = widget.baseline_offset(); + + major_flex += self.direction.major(child_size).expand(); + minor = minor.max(self.direction.minor(child_size).expand()); + max_above_baseline = max_above_baseline.max(child_size.height - baseline_offset); + max_below_baseline = max_below_baseline.max(baseline_offset); + } + Child::FlexedSpacer(flex, calculated_size) => { + let desired_major = (*flex) * px_per_flex + remainder; + *calculated_size = desired_major.round(); + remainder = desired_major - *calculated_size; + major_flex += *calculated_size; + } + _ => {} + } + } + + // figure out if we have extra space on major axis, and if so how to use it + let extra = if self.fill_major_axis { + (remaining - major_flex).max(0.0) + } else { + // if we are *not* expected to fill our available space this usually + // means we don't have any extra, unless dictated by our constraints. + (self.direction.major(bc.min()) - (major_non_flex + major_flex)).max(0.0) + }; + + let mut spacing = Spacing::new(self.main_alignment, extra, self.children.len()); + + // the actual size needed to tightly fit the children on the minor axis. + // Unlike the 'minor' var, this ignores the incoming constraints. + let minor_dim = match self.direction { + Axis::Horizontal if any_use_baseline => max_below_baseline + max_above_baseline, + _ => minor, + }; + + let extra_height = minor - minor_dim.min(minor); + + let mut major = spacing.next().unwrap_or(0.); + let mut child_paint_rect = Rect::ZERO; + + for child in &mut self.children { + match child { + Child::Fixed { widget, alignment } + | Child::Flex { + widget, alignment, .. + } => { + let child_size = widget.layout_rect().size(); + let alignment = alignment.unwrap_or(self.cross_alignment); + let child_minor_offset = match alignment { + // This will ignore baseline alignment if it is overridden on children, + // but is not the default for the container. Is this okay? + CrossAxisAlignment::Baseline if matches!(self.direction, Axis::Horizontal) => { + let child_baseline = widget.baseline_offset(); + let child_above_baseline = child_size.height - child_baseline; + extra_height + (max_above_baseline - child_above_baseline) + } + CrossAxisAlignment::Fill => { + let fill_size: Size = self + .direction + .pack(self.direction.major(child_size), minor_dim) + .into(); + let child_bc = BoxConstraints::tight(fill_size); + widget.layout(ctx, &child_bc, data, env); + 0.0 + } + _ => { + let extra_minor = minor_dim - self.direction.minor(child_size); + alignment.align(extra_minor) + } + }; + + let child_pos: Point = self.direction.pack(major, child_minor_offset).into(); + widget.set_origin(ctx, data, env, child_pos); + child_paint_rect = child_paint_rect.union(widget.paint_rect()); + major += self.direction.major(child_size).expand(); + major += spacing.next().unwrap_or(0.); + } + Child::FlexedSpacer(_, calculated_size) | Child::FixedSpacer(_, calculated_size) => { + major += *calculated_size; + } + } + } + + if flex_sum > 0.0 && total_major.is_infinite() { + tracing::warn!("A child of Flex is flex, but Flex is unbounded.") + } + + if flex_sum > 0.0 { + major = total_major; + } + + let my_size: Size = self.direction.pack(major, minor_dim).into(); + + // if we don't have to fill the main axis, we loosen that axis before constraining + let my_size = if !self.fill_major_axis { + let max_major = self.direction.major(bc.max()); + self + .direction + .constraints(bc, 0.0, max_major) + .constrain(my_size) + } else { + bc.constrain(my_size) + }; + + let my_bounds = Rect::ZERO.with_size(my_size); + let insets = child_paint_rect - my_bounds; + ctx.set_paint_insets(insets); + + let baseline_offset = match self.direction { + Axis::Horizontal => max_below_baseline, + Axis::Vertical => (&self.children) + .last() + .map(|last| { + let child = last.widget(); + if let Some(widget) = child { + let child_bl = widget.baseline_offset(); + let child_max_y = widget.layout_rect().max_y(); + let extra_bottom_padding = my_size.height - child_max_y; + child_bl + extra_bottom_padding + } else { + 0.0 + } + }) + .unwrap_or(0.0), + }; + + ctx.set_baseline_offset(baseline_offset); + trace!( + "Computed layout: size={}, baseline_offset={}", + my_size, + baseline_offset + ); + my_size + } + + // #[instrument(name = "Flex", level = "trace", skip(self, ctx, data, env))] + fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { + for child in self.children.iter_mut().filter_map(|x| x.widget_mut()) { + child.paint(ctx, data, env); + } + + // paint the baseline if we're debugging layout + if env.get(Env::DEBUG_PAINT) && ctx.widget_state.baseline_offset != 0.0 { + let color = env.get_debug_color(ctx.widget_id().to_raw()); + let my_baseline = ctx.size().height - ctx.widget_state.baseline_offset; + let line = crate::kurbo::Line::new((0.0, my_baseline), (ctx.size().width, my_baseline)); + let stroke_style = crate::piet::StrokeStyle::new().dash_pattern(&[4.0, 4.0]); + ctx.stroke_styled(line, &color, 1.0, &stroke_style); + } + } + + // fn debug_state(&self, data: &T) -> DebugState { + // let children_state = self + // .children + // .iter() + // .map(|child| { + // let child_widget_pod = child.widget()?; + // Some(child_widget_pod.widget().debug_state(data)) + // }) + // .flatten() + // .collect(); + // DebugState { + // display_name: self.short_type_name().to_string(), + // children: children_state, + // ..Default::default() + // } + // } +} diff --git a/src/widget/flex/spacing.rs b/src/widget/flex/spacing.rs new file mode 100644 index 0000000..90fcdb4 --- /dev/null +++ b/src/widget/flex/spacing.rs @@ -0,0 +1,96 @@ +use super::main_axis_alignment::MainAxisAlignment; + +pub struct Spacing { + alignment: MainAxisAlignment, + extra: f64, + n_children: usize, + index: usize, + equal_space: f64, + remainder: f64, +} + +impl Spacing { + /// Given the provided extra space and children count, + /// this returns an iterator of `f64` spacing, + /// where the first element is the spacing before any children + /// and all subsequent elements are the spacing after children. + fn new(alignment: MainAxisAlignment, extra: f64, n_children: usize) -> Spacing { + let extra = if extra.is_finite() { extra } else { 0. }; + let equal_space = if n_children > 0 { + match alignment { + MainAxisAlignment::Center => extra / 2., + MainAxisAlignment::SpaceBetween => extra / (n_children - 1).max(1) as f64, + MainAxisAlignment::SpaceEvenly => extra / (n_children + 1) as f64, + MainAxisAlignment::SpaceAround => extra / (2 * n_children) as f64, + _ => 0., + } + } else { + 0. + }; + Spacing { + alignment, + extra, + n_children, + index: 0, + equal_space, + remainder: 0., + } + } + + fn next_space(&mut self) -> f64 { + let desired_space = self.equal_space + self.remainder; + let actual_space = desired_space.round(); + self.remainder = desired_space - actual_space; + actual_space + } +} + +impl Iterator for Spacing { + type Item = f64; + + fn next(&mut self) -> Option { + if self.index > self.n_children { + return None; + } + let result = { + if self.n_children == 0 { + self.extra + } else { + #[allow(clippy::match_bool)] + match self.alignment { + MainAxisAlignment::Start => match self.index == self.n_children { + true => self.extra, + false => 0., + }, + MainAxisAlignment::End => match self.index == 0 { + true => self.extra, + false => 0., + }, + MainAxisAlignment::Center => match self.index { + 0 => self.next_space(), + i if i == self.n_children => self.next_space(), + _ => 0., + }, + MainAxisAlignment::SpaceBetween => match self.index { + 0 => 0., + i if i != self.n_children => self.next_space(), + _ => match self.n_children { + 1 => self.next_space(), + _ => 0., + }, + }, + MainAxisAlignment::SpaceEvenly => self.next_space(), + MainAxisAlignment::SpaceAround => { + if self.index == 0 || self.index == self.n_children { + self.next_space() + } else { + self.next_space() + self.next_space() + } + } + } + } + }; + self.index += 1; + Some(result) + } +} diff --git a/src/widget/text.rs b/src/widget/text.rs index 9db0a10..4a08325 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -1,39 +1,58 @@ -use crate::{Data, Size, Widget}; +use crate::{box_constraints::BoxConstraints, Data, DataWrapper, Size, Widget}; pub struct Text { - data: T, - text: Box String>, + text: Box) -> String>, + needs_repaint: bool, + buf: String, } impl Text { - pub fn new(data: T, text: Box String>) -> Self { - Self { data, text } - } - fn text(&self) -> String { - (self.text)(&self.data) - } + pub fn new(text: Box) -> String>) -> Self { + Self { + text, + needs_repaint: true, + buf: String::new(), + } + } } impl Widget for Text { - fn layout(&mut self, _bounds: &Size) -> Size { - Size { - a: self.text().chars().count(), - b: self.text().chars().filter(|ch| *ch == '\n').count(), - } - } - fn paint(&self, buf: &mut [&mut [char]]) { - let the_text = self.text(); - let mut the_chars = the_text.chars(); - for line in buf.iter_mut() { - for spot in line.iter_mut() { - if let Some(ch) = the_chars.next() { - if ch == '\n' { - break; - } else { - *spot = ch; - } - } - } - } - } + fn update(&mut self, data: &DataWrapper) { + self.buf = (*self.text)(data); + } + fn layout(&mut self, bc: &BoxConstraints) -> Size { + let mut width = 0; + let mut height = 1; + let mut x = 0; + for ch in self.buf.chars() { + if ch == '\n' { + height += 1; + x = 0; + } + x += 1; + if x > bc.max().width - 1 { + x = 0; + width = bc.max().width; + } + if x > width { + width = x; + } + } + Size::new(width, height).clamp(bc.min(), bc.max()) + } + fn paint(&self, buf: &mut [char], size: &Size) { + let mut the_chars = self.buf.chars(); + for y in 0..size.height { + for x in 0..size.width { + if let (Some(ch), Some(spot)) = (the_chars.next(), buf.get_mut(x + y * size.width)) { + if ch == '\n' { + break; + } else { + *spot = ch; + } + } + } + } + } + fn event(&mut self, data: &mut DataWrapper) {} } diff --git a/src/widget/widget.rs b/src/widget/widget.rs index dea8211..e10de6b 100644 --- a/src/widget/widget.rs +++ b/src/widget/widget.rs @@ -1,18 +1,25 @@ -use std::ops::{DerefMut, Deref}; +use std::ops::{Deref, DerefMut}; -use crate::Size; +use crate::{box_constraints::BoxConstraints, DataWrapper, Size}; pub trait Widget { - fn layout(&mut self, bounds: &Size) -> Size; - fn paint(&self, buf: &mut [&mut [char]]); + fn update(&mut self, data: &DataWrapper); + fn layout(&mut self, bc: &BoxConstraints) -> Size; + fn paint(&self, buf: &mut [char], size: &Size); + fn event(&mut self, data: &mut DataWrapper); } impl Widget for Box> { - fn layout(&mut self, bounds: &Size) -> Size { - self.deref_mut().layout(bounds) - } - - fn paint(&self, buf: &mut [&mut [char]]) { - self.deref().paint(buf) - } + fn update(&mut self, data: &DataWrapper) { + self.deref_mut().update(data) + } + fn layout(&mut self, bounds: &BoxConstraints) -> Size { + self.deref_mut().layout(bounds) + } + fn paint(&self, buf: &mut [char], size: &Size) { + self.deref().paint(buf, size) + } + fn event(&mut self, data: &mut DataWrapper) { + self.deref_mut().event(data) + } } diff --git a/src/widget/widget_pod.rs b/src/widget/widget_pod.rs index 2ab9275..ede3de6 100644 --- a/src/widget/widget_pod.rs +++ b/src/widget/widget_pod.rs @@ -1,35 +1,40 @@ use std::marker::PhantomData; -use crate::{Point, Widget, Size, Data}; - - +use crate::{box_constraints::BoxConstraints, Data, DataWrapper, Point, Size, Widget}; pub struct WidgetPod { - data: PhantomData, - inner: W, - origin: Point, + data: PhantomData, + inner: W, + origin: Point, } impl> WidgetPod { - pub fn new(inner: W) -> Self { - Self { - data: PhantomData, - inner, - origin: Point { a: 0, b: 0 }, - } - } + pub fn new(inner: W) -> Self { + Self { + data: PhantomData, + inner, + origin: Point { x: 0, y: 0 }, + } + } - pub fn set_origin(&mut self, p: Point) { - self.origin = p; - } + pub fn set_origin(&mut self, p: Point) { + self.origin = p; + } } impl> Widget for WidgetPod { - fn layout(&mut self, bounds: &Size) -> Size { - self.inner.layout(bounds) - } + fn update(&mut self, data: &DataWrapper) { + self.inner.update(data) + } - fn paint(&self, buf: &mut [&mut [char]]) { - self.inner.paint(buf) - } + fn layout(&mut self, bounds: &BoxConstraints) -> Size { + self.inner.layout(bounds) + } + + fn paint(&self, buf: &mut [char], size: &Size) { + self.inner.paint(buf, size) + } + fn event(&mut self, data: &mut DataWrapper) { + self.inner.event(data) + } }