From 250fb20fcaea43e86091e18c56067a2d781111eb Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Thu, 26 Jul 2018 15:33:57 -0700 Subject: [PATCH] add widget layout solver --- Cargo.toml | 1 + examples/widgets_basic.rs | 10 +- src/lib.rs | 1 + src/surface.rs | 2 +- src/widgets/layout.rs | 784 ++++++++++++++++++++++++++++++++++++++ src/widgets/mod.rs | 96 +++-- 6 files changed, 851 insertions(+), 43 deletions(-) create mode 100644 src/widgets/layout.rs diff --git a/Cargo.toml b/Cargo.toml index 52c0248f7..cb67d0008 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ derive_builder = "~0.5" semver = "0.9" libc = "~0.2" bitflags = "~1.0" +cassowary = "~0.3" [dependencies.num-derive] version = "~0.2" diff --git a/examples/widgets_basic.rs b/examples/widgets_basic.rs index 983ba97ad..d0a7b2559 100644 --- a/examples/widgets_basic.rs +++ b/examples/widgets_basic.rs @@ -21,7 +21,6 @@ struct MainScreen { } impl WidgetImpl for MainScreen { - fn set_widget_id(&mut self, _id: WidgetId) {} fn process_event(&mut self, event: &WidgetEvent) -> EventDisposition { match event { WidgetEvent::Input(InputEvent::Key(KeyEvent { @@ -42,10 +41,6 @@ impl WidgetImpl for MainScreen { } } - fn get_size_constraints(&self) -> SizeConstraints { - SizeConstraints::default() - } - fn render_to_surface(&self, surface: &mut Surface) { surface.add_change(Change::ClearScreen(AnsiColor::Blue.into())); surface.add_change(self.buf.clone()); @@ -59,6 +54,7 @@ impl WidgetImpl for MainScreen { fn get_cursor_shape_and_position(&self) -> CursorShapeAndPosition { CursorShapeAndPosition { coords: self.cursor.get(), + shape: termwiz::surface::CursorShape::SteadyBar, ..Default::default() } } @@ -72,7 +68,7 @@ fn main() -> Result<(), Error> { let mut screen = Screen::new(Widget::new(Box::new(MainScreen::default()))); - screen.render_to_screen(&mut buf); + screen.render_to_screen(&mut buf)?; buf.flush()?; loop { @@ -95,7 +91,7 @@ fn main() -> Result<(), Error> { } } - screen.render_to_screen(&mut buf); + screen.render_to_screen(&mut buf)?; buf.flush()?; } diff --git a/src/lib.rs b/src/lib.rs index f053af65b..49ac7c467 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ extern crate num_derive; extern crate derive_builder; #[macro_use] extern crate bitflags; +extern crate cassowary; pub mod caps; pub mod cell; diff --git a/src/surface.rs b/src/surface.rs index 60930b281..3e0715feb 100644 --- a/src/surface.rs +++ b/src/surface.rs @@ -815,7 +815,7 @@ impl Surface { fn compute_position_change(current: usize, pos: &Position, limit: usize) -> usize { use self::Position::*; match pos { - NoChange => min(current, limit - 1), + NoChange => min(current, limit.saturating_sub(1)), Relative(delta) => { if *delta > 0 { min(current.saturating_add(*delta as usize), limit - 1) diff --git a/src/widgets/layout.rs b/src/widgets/layout.rs new file mode 100644 index 000000000..1d1ea0cca --- /dev/null +++ b/src/widgets/layout.rs @@ -0,0 +1,784 @@ +//! This module provides some automatic layout functionality for widgets. +//! The parameters are similar to those that you may have encountered +//! in HTML, but do not fully recreate the layout model. +use cassowary::strength::{REQUIRED, STRONG, WEAK}; +use cassowary::WeightedRelation::*; +use cassowary::{AddConstraintError, Expression, Solver, SuggestValueError, Variable}; +use failure::{err_msg, Error}; +use std::collections::HashMap; + +use widgets::{ParentRelativeCoords, WidgetHandle}; + +/// Expands to an Expression holding the value of the variable, +/// or if there is no variable, a constant with the specified +/// value. +/// Equivalent to Option::unwrap_or(). +fn unwrap_variable_or(var: Option, value: f64) -> Expression { + // The `v + 0.0` portion "converts" the variable to an Expression. + var.map(|v| v + 0.0) + .unwrap_or_else(|| Expression::from_constant(value)) +} + +/// Specify whether a width or a height has a preferred fixed size +/// or whether it should occupy a percentage of its parent container. +/// The default is 100% of the parent container. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DimensionSpec { + /// Occupy a fixed number of cells + Fixed(u16), + /// Occupy a percentage of the space in the parent container + Percentage(u8), +} + +impl Default for DimensionSpec { + fn default() -> Self { + DimensionSpec::Percentage(100) + } +} + +/// Specifies the extent of a width or height. The `spec` field +/// holds the preferred size, while the `minimum` and `maximum` +/// fields set optional lower and upper bounds. +#[derive(Clone, Default, Copy, Debug, PartialEq, Eq)] +pub struct Dimension { + pub spec: DimensionSpec, + pub maximum: Option, + pub minimum: Option, +} + +/// Specifies whether the children of a widget are laid out +/// vertically (top to bottom) or horizontally (left to right). +/// The default is horizontal. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ChildOrientation { + Vertical, + Horizontal, +} + +impl Default for ChildOrientation { + fn default() -> Self { + ChildOrientation::Horizontal + } +} + +/// Specifies whether the widget should be aligned to the top, +/// middle or bottom of the vertical space in its parent. +/// The default is Top. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum VerticalAlignment { + Top, + Middle, + Bottom, +} + +impl Default for VerticalAlignment { + fn default() -> Self { + VerticalAlignment::Top + } +} + +/// Specifies whether the widget should be aligned to the left, +/// center or right of the horizontal space in its parent. +/// The default is Left. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum HorizontalAlignment { + Left, + Center, + Right, +} + +impl Default for HorizontalAlignment { + fn default() -> Self { + HorizontalAlignment::Left + } +} + +/// Specifies the size constraints for a widget +#[derive(Clone, Default, Copy, Debug, PartialEq, Eq)] +pub struct Constraints { + pub width: Dimension, + pub height: Dimension, + pub valign: VerticalAlignment, + pub halign: HorizontalAlignment, + pub child_orientation: ChildOrientation, +} + +impl Constraints { + pub fn with_fixed_width_height(width: u16, height: u16) -> Self { + Self::default() + .set_fixed_width(width) + .set_fixed_height(height) + .clone() + } + + pub fn set_fixed_width(&mut self, width: u16) -> &mut Self { + self.width = Dimension { + spec: DimensionSpec::Fixed(width), + minimum: Some(width), + maximum: Some(width), + ..Default::default() + }; + self + } + + pub fn set_pct_width(&mut self, width: u8) -> &mut Self { + self.width = Dimension { + spec: DimensionSpec::Percentage(width), + ..Default::default() + }; + self + } + + pub fn set_fixed_height(&mut self, height: u16) -> &mut Self { + self.height = Dimension { + spec: DimensionSpec::Fixed(height), + minimum: Some(height), + maximum: Some(height), + ..Default::default() + }; + self + } + + pub fn set_pct_height(&mut self, height: u8) -> &mut Self { + self.height = Dimension { + spec: DimensionSpec::Percentage(height), + ..Default::default() + }; + self + } + + pub fn set_valign(&mut self, valign: VerticalAlignment) -> &mut Self { + self.valign = valign; + self + } + + pub fn set_halign(&mut self, halign: HorizontalAlignment) -> &mut Self { + self.halign = halign; + self + } +} + +/// Holds state used to compute the layout of a tree of widgets +pub struct LayoutState { + solver: Solver, + screen_width: Variable, + screen_height: Variable, + widget_states: HashMap, +} + +/// Each `WidgetHandle` has a `WidgetState` associated with it. +/// This allows us to look up the `Variable` for each of the +/// layout features of a widget by `WidgetHandle` after the solver +/// has computed the layout solution. +#[derive(Copy, Clone)] +struct WidgetState { + left: Variable, + top: Variable, + width: Variable, + height: Variable, +} + +fn suggesterr(e: SuggestValueError) -> Error { + match e { + SuggestValueError::UnknownEditVariable => err_msg("Unknown edit variable"), + SuggestValueError::InternalSolverError(e) => format_err!("Internal solver error: {}", e), + } +} + +fn adderr(e: AddConstraintError) -> Error { + format_err!("{:?}", e) +} + +impl LayoutState { + /// Convenience function that performs the entire layout operation + /// and updates the widget tree. + pub fn compute_layout( + screen_width: usize, + screen_height: usize, + root_widget: &WidgetHandle, + ) -> Result<(), Error> { + let mut layout = Self::new(); + layout.add_widget_recursive(root_widget); + layout.update_constraints(screen_width, screen_height, root_widget) + } + + /// Create a new `LayoutState` + pub fn new() -> Self { + let mut solver = Solver::new(); + let screen_width = Variable::new(); + let screen_height = Variable::new(); + solver + .add_edit_variable(screen_width, STRONG) + .expect("failed to add screen_width to solver"); + solver + .add_edit_variable(screen_height, STRONG) + .expect("failed to add screen_height to solver"); + Self { + solver, + screen_width, + screen_height, + widget_states: HashMap::new(), + } + } + + /// Creates a WidgetState entry for a widget. + fn add_widget(&mut self, widget: &WidgetHandle) { + let state = WidgetState { + left: Variable::new(), + top: Variable::new(), + width: Variable::new(), + height: Variable::new(), + }; + self.widget_states.insert(widget.clone(), state); + } + + pub fn add_widget_recursive(&mut self, widget: &WidgetHandle) { + self.add_widget(widget); + for child in &widget.borrow().children { + self.add_widget_recursive(child); + } + } + + /// Assign the screen dimensions, compute constraints, solve + /// the layout and then apply the size and positioning information + /// to the widgets in the widget tree. + pub fn update_constraints( + &mut self, + screen_width: usize, + screen_height: usize, + root_widget: &WidgetHandle, + ) -> Result<(), Error> { + self.solver + .suggest_value(self.screen_width, screen_width as f64) + .map_err(suggesterr)?; + self.solver + .suggest_value(self.screen_height, screen_height as f64) + .map_err(suggesterr)?; + + let width = self.screen_width; + let height = self.screen_height; + self.update_widget_constraint(root_widget, width, height, None, None)?; + + // The updates are in an unspecified order, and the coordinates are in + // the screen absolute coordinate space, rather than the parent-relative + // coordinates that we desire. So we need to either to accumulate the + // deltas and then sort them such that we walk the widget tree to apply + // them, or just walk the tree and apply them all anyway. The latter + // feels easier and probably has a low enough cardinality that it won't + // feel too expensive. + self.solver.fetch_changes(); + self.apply_widget_state(root_widget, 0, 0)?; + + Ok(()) + } + + fn apply_widget_state( + &self, + widget: &WidgetHandle, + parent_left: usize, + parent_top: usize, + ) -> Result<(), Error> { + let state = *self.widget_states + .get(widget) + .ok_or_else(|| err_msg("widget has no solver state"))?; + let width = self.solver.get_value(state.width) as usize; + let height = self.solver.get_value(state.height) as usize; + let left = self.solver.get_value(state.left) as usize; + let top = self.solver.get_value(state.top) as usize; + + widget.borrow_mut().set_size_and_position( + width, + height, + ParentRelativeCoords::new(left - parent_left, top - parent_top), + ); + + for child in &widget.borrow().children { + self.apply_widget_state(child, left, top)?; + } + + Ok(()) + } + + fn update_widget_constraint( + &mut self, + widget: &WidgetHandle, + parent_width: Variable, + parent_height: Variable, + parent_left: Option, + parent_top: Option, + ) -> Result<(WidgetState, Constraints), Error> { + let state = *self.widget_states + .get(widget) + .ok_or_else(|| err_msg("widget has no solver state"))?; + + let is_root_widget = parent_left.is_none(); + + let parent_left = unwrap_variable_or(parent_left, 0.0); + let parent_top = unwrap_variable_or(parent_top, 0.0); + + // First, we should fit inside the parent container + self.solver + .add_constraint( + state.left + state.width | LE(REQUIRED) | parent_left.clone() + parent_width, + ) + .map_err(adderr)?; + self.solver + .add_constraint(state.left | GE(REQUIRED) | parent_left.clone()) + .map_err(adderr)?; + + self.solver + .add_constraint( + state.top + state.height | LE(REQUIRED) | parent_top.clone() + parent_height, + ) + .map_err(adderr)?; + self.solver + .add_constraint(state.top | GE(REQUIRED) | parent_top.clone()) + .map_err(adderr)?; + + let constraints = widget.borrow().get_size_constraints(); + + if is_root_widget { + // We handle alignment on the root widget specially here; + // for non-root widgets, we handle it when assessing children + match constraints.halign { + HorizontalAlignment::Left => self.solver + .add_constraint(state.left | EQ(STRONG) | 0.0) + .map_err(adderr)?, + HorizontalAlignment::Right => self.solver + .add_constraint(state.left | EQ(STRONG) | parent_width - state.width) + .map_err(adderr)?, + HorizontalAlignment::Center => self.solver + .add_constraint(state.left | EQ(STRONG) | (parent_width - state.width) / 2.0) + .map_err(adderr)?, + } + + match constraints.valign { + VerticalAlignment::Top => self.solver + .add_constraint(state.top | EQ(STRONG) | 0.0) + .map_err(adderr)?, + VerticalAlignment::Bottom => self.solver + .add_constraint(state.top | EQ(STRONG) | parent_height - state.height) + .map_err(adderr)?, + VerticalAlignment::Middle => self.solver + .add_constraint(state.top | EQ(STRONG) | (parent_height - state.height) / 2.0) + .map_err(adderr)?, + } + } + + match constraints.width.spec { + DimensionSpec::Fixed(width) => { + self.solver + .add_constraint(state.width | EQ(STRONG) | f64::from(width)) + .map_err(adderr)?; + } + DimensionSpec::Percentage(pct) => { + self.solver + .add_constraint( + state.width | EQ(STRONG) | f64::from(pct) * parent_width / 100.0, + ) + .map_err(adderr)?; + } + } + self.solver + .add_constraint( + state.width | GE(STRONG) | f64::from(constraints.width.minimum.unwrap_or(1).max(1)), + ) + .map_err(adderr)?; + if let Some(max_width) = constraints.width.maximum { + self.solver + .add_constraint(state.width | LE(STRONG) | f64::from(max_width)) + .map_err(adderr)?; + } + + match constraints.height.spec { + DimensionSpec::Fixed(height) => { + self.solver + .add_constraint(state.height | EQ(STRONG) | f64::from(height)) + .map_err(adderr)?; + } + DimensionSpec::Percentage(pct) => { + self.solver + .add_constraint( + state.height | EQ(STRONG) | f64::from(pct) * parent_height / 100.0, + ) + .map_err(adderr)?; + } + } + self.solver + .add_constraint( + state.height + | GE(STRONG) + | f64::from(constraints.height.minimum.unwrap_or(1).max(1)), + ) + .map_err(adderr)?; + if let Some(max_height) = constraints.height.maximum { + self.solver + .add_constraint(state.height | LE(STRONG) | f64::from(max_height)) + .map_err(adderr)?; + } + + let has_children = !widget.borrow().children.is_empty(); + if has_children { + let mut left_edge: Expression = state.left + 0.0; + let mut top_edge: Expression = state.top + 0.0; + let mut width_constraint = Expression::from_constant(0.0); + let mut height_constraint = Expression::from_constant(0.0); + + for child in &widget.borrow().children { + let (child_state, child_constraints) = self.update_widget_constraint( + child, + state.width, + state.height, + Some(state.left), + Some(state.top), + )?; + + match child_constraints.halign { + HorizontalAlignment::Left => self.solver + .add_constraint(child_state.left | EQ(STRONG) | left_edge.clone()) + .map_err(adderr)?, + HorizontalAlignment::Right => self.solver + .add_constraint( + child_state.left + child_state.width + | EQ(STRONG) + | state.left + state.width, + ) + .map_err(adderr)?, + HorizontalAlignment::Center => self.solver + .add_constraint( + child_state.left + | EQ(STRONG) + | state.left + (state.width - child_state.width) / 2.0, + ) + .map_err(adderr)?, + } + + match child_constraints.valign { + VerticalAlignment::Top => self.solver + .add_constraint(child_state.top | EQ(STRONG) | top_edge.clone()) + .map_err(adderr)?, + VerticalAlignment::Bottom => self.solver + .add_constraint( + child_state.top + child_state.height + | EQ(STRONG) + | state.top + state.height, + ) + .map_err(adderr)?, + VerticalAlignment::Middle => self.solver + .add_constraint( + child_state.top + | EQ(STRONG) + | state.top + (state.height - child_state.height) / 2.0, + ) + .map_err(adderr)?, + } + + match constraints.child_orientation { + ChildOrientation::Horizontal => { + left_edge = child_state.left + child_state.width; + width_constraint = width_constraint + child_state.width; + } + ChildOrientation::Vertical => { + top_edge = child_state.top + child_state.height; + height_constraint = height_constraint + child_state.height; + } + } + } + + // This constraint encourages the contents to fill out to the width + // of the container, rather than clumping left + self.solver + .add_constraint(left_edge | EQ(STRONG) | state.left + state.width) + .map_err(adderr)?; + + self.solver + .add_constraint(state.width | GE(WEAK) | width_constraint) + .map_err(adderr)?; + + // This constraint encourages the contents to fill out to the height + // of the container, rather than clumping top + self.solver + .add_constraint(top_edge | EQ(STRONG) | state.top + state.height) + .map_err(adderr)?; + + self.solver + .add_constraint(state.height | GE(WEAK) | height_constraint) + .map_err(adderr)?; + } + + Ok((state, constraints)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use surface::Surface; + use widgets::{Widget, WidgetImpl}; + + struct UnspecWidget {} + impl WidgetImpl for UnspecWidget { + fn render_to_surface(&self, _surface: &mut Surface) {} + } + + struct ConstrainedWidget { + constraints: Constraints, + } + impl WidgetImpl for ConstrainedWidget { + fn render_to_surface(&self, _surface: &mut Surface) {} + fn get_size_constraints(&self) -> Constraints { + self.constraints + } + } + impl ConstrainedWidget { + fn new(constraints: Constraints) -> Self { + Self { constraints } + } + } + + #[test] + fn single_widget_unspec() { + let widget = Widget::new(Box::new(UnspecWidget {})); + let mut layout = LayoutState::new(); + layout.add_widget_recursive(&widget); + layout.update_constraints(40, 12, &widget).unwrap(); + + assert_eq!( + widget.borrow().get_size_and_position(), + (40, 12, ParentRelativeCoords::new(0, 0)) + ); + } + + #[test] + fn two_children_pct() { + let root = Widget::new(Box::new(UnspecWidget {})); + + let a = Widget::new(Box::new(ConstrainedWidget::new( + Constraints::default().set_pct_width(50).clone(), + ))); + let b = Widget::new(Box::new(ConstrainedWidget::new( + Constraints::default().set_pct_width(50).clone(), + ))); + + root.borrow_mut().add_child(&a); + root.borrow_mut().add_child(&b); + + let mut layout = LayoutState::new(); + layout.add_widget_recursive(&root); + layout.update_constraints(100, 100, &root).unwrap(); + + assert_eq!( + ( + root.borrow().get_size_and_position(), + a.borrow().get_size_and_position(), + b.borrow().get_size_and_position() + ), + ( + (100, 100, ParentRelativeCoords::new(0, 0)), + (50, 100, ParentRelativeCoords::new(0, 0)), + (50, 100, ParentRelativeCoords::new(50, 0)) + ) + ); + } + + #[test] + fn three_children_pct() { + let root = Widget::new(Box::new(UnspecWidget {})); + + let a = Widget::new(Box::new(ConstrainedWidget::new( + Constraints::default().set_pct_width(20).clone(), + ))); + let b = Widget::new(Box::new(ConstrainedWidget::new( + Constraints::default().set_pct_width(20).clone(), + ))); + let c = Widget::new(Box::new(ConstrainedWidget::new( + Constraints::default().set_pct_width(20).clone(), + ))); + + root.borrow_mut().add_child(&a); + root.borrow_mut().add_child(&b); + root.borrow_mut().add_child(&c); + + let mut layout = LayoutState::new(); + layout.add_widget_recursive(&root); + layout.update_constraints(100, 100, &root).unwrap(); + + assert_eq!( + ( + root.borrow().get_size_and_position(), + a.borrow().get_size_and_position(), + b.borrow().get_size_and_position(), + c.borrow().get_size_and_position(), + ), + ( + (100, 100, ParentRelativeCoords::new(0, 0)), + (20, 100, ParentRelativeCoords::new(0, 0)), + (20, 100, ParentRelativeCoords::new(20, 0)), + (20, 100, ParentRelativeCoords::new(40, 0)) + ) + ); + } + + #[test] + fn two_children_a_b() { + let root = Widget::new(Box::new(UnspecWidget {})); + + let a = Widget::new(Box::new(ConstrainedWidget::new( + Constraints::with_fixed_width_height(5, 2), + ))); + let b = Widget::new(Box::new(ConstrainedWidget::new( + Constraints::with_fixed_width_height(3, 2), + ))); + + root.borrow_mut().add_child(&a); + root.borrow_mut().add_child(&b); + + let mut layout = LayoutState::new(); + layout.add_widget_recursive(&root); + layout.update_constraints(100, 100, &root).unwrap(); + + assert_eq!( + ( + root.borrow().get_size_and_position(), + a.borrow().get_size_and_position(), + b.borrow().get_size_and_position() + ), + ( + (100, 100, ParentRelativeCoords::new(0, 0)), + (5, 2, ParentRelativeCoords::new(0, 0)), + (3, 2, ParentRelativeCoords::new(5, 0)) + ) + ); + } + + #[test] + fn two_children_b_a() { + let root = Widget::new(Box::new(UnspecWidget {})); + + let a = Widget::new(Box::new(ConstrainedWidget::new( + Constraints::with_fixed_width_height(5, 2), + ))); + let b = Widget::new(Box::new(ConstrainedWidget::new( + Constraints::with_fixed_width_height(3, 2), + ))); + + root.borrow_mut().add_child(&b); + root.borrow_mut().add_child(&a); + + let mut layout = LayoutState::new(); + layout.add_widget_recursive(&root); + layout.update_constraints(100, 100, &root).unwrap(); + + assert_eq!( + ( + root.borrow().get_size_and_position(), + a.borrow().get_size_and_position(), + b.borrow().get_size_and_position() + ), + ( + (100, 100, ParentRelativeCoords::new(0, 0)), + (5, 2, ParentRelativeCoords::new(3, 0)), + (3, 2, ParentRelativeCoords::new(0, 0)), + ) + ); + } + + #[test] + fn two_children_overflow() { + let root = Widget::new(Box::new(UnspecWidget {})); + + let a = Widget::new(Box::new(ConstrainedWidget::new( + Constraints::with_fixed_width_height(5, 2), + ))); + let b = Widget::new(Box::new(ConstrainedWidget::new( + Constraints::with_fixed_width_height(3, 2), + ))); + + root.borrow_mut().add_child(&a); + root.borrow_mut().add_child(&b); + + let mut layout = LayoutState::new(); + layout.add_widget_recursive(&root); + layout.update_constraints(6, 2, &root).unwrap(); + + assert_eq!( + ( + root.borrow().get_size_and_position(), + a.borrow().get_size_and_position(), + b.borrow().get_size_and_position() + ), + ( + (8, 2, ParentRelativeCoords::new(0, 0)), + (5, 2, ParentRelativeCoords::new(0, 0)), + (3, 2, ParentRelativeCoords::new(5, 0)), + ) + ); + } + + macro_rules! single_constrain { + ($name:ident, $constraint:expr, $width:expr, $height:expr, $coords:expr) => { + #[test] + fn $name() { + let widget = Widget::new(Box::new(ConstrainedWidget::new($constraint))); + let mut layout = LayoutState::new(); + layout.add_widget_recursive(&widget); + layout.update_constraints(100, 100, &widget).unwrap(); + + assert_eq!( + widget.borrow().get_size_and_position(), + ($width, $height, $coords) + ); + } + }; + } + + single_constrain!( + single_constrained_widget_top, + Constraints::with_fixed_width_height(10, 2), + 10, + 2, + ParentRelativeCoords::new(0, 0) + ); + + single_constrain!( + single_constrained_widget_bottom, + Constraints::with_fixed_width_height(10, 2) + .set_valign(VerticalAlignment::Bottom) + .clone(), + 10, + 2, + ParentRelativeCoords::new(0, 98) + ); + + single_constrain!( + single_constrained_widget_middle, + Constraints::with_fixed_width_height(10, 2) + .set_valign(VerticalAlignment::Middle) + .clone(), + 10, + 2, + ParentRelativeCoords::new(0, 49) + ); + + single_constrain!( + single_constrained_widget_right, + Constraints::with_fixed_width_height(10, 2) + .set_halign(HorizontalAlignment::Right) + .clone(), + 10, + 2, + ParentRelativeCoords::new(90, 0) + ); + + single_constrain!( + single_constrained_widget_bottom_center, + Constraints::with_fixed_width_height(10, 2) + .set_valign(VerticalAlignment::Bottom) + .set_halign(HorizontalAlignment::Center) + .clone(), + 10, + 2, + ParentRelativeCoords::new(45, 98) + ); + +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 8ad2addf6..377f70505 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,9 +1,14 @@ use color::ColorAttribute; +use failure::Error; use input::InputEvent; use std::cell::{Ref, RefCell, RefMut}; +use std::fmt::{Debug, Error as FmtError, Formatter}; +use std::hash::{Hash, Hasher}; use std::rc::{Rc, Weak}; use surface::{Change, CursorShape, Position, SequenceNo, Surface}; +pub mod layout; + /// Describes an event that may need to be processed by the widget pub enum WidgetEvent { Input(InputEvent), @@ -18,35 +23,6 @@ pub enum EventDisposition { Stop, } -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct SizeConstraints { - pub min_width: Option, - pub min_height: Option, - pub max_width: Option, - pub max_height: Option, - pub width: Option, - pub height: Option, -} - -impl SizeConstraints { - pub fn deduce_initial_size(&self) -> (usize, usize) { - match (self.width, self.height) { - (Some(w), Some(h)) => return (w, h), - _ => {} - } - match (self.max_width, self.max_height) { - (Some(w), Some(h)) => return (w, h), - _ => {} - } - match (self.min_width, self.min_height) { - (Some(w), Some(h)) => return (w, h), - _ => {} - } - - (80, 24) - } -} - #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct CursorShapeAndPosition { pub shape: CursorShape, @@ -57,19 +33,25 @@ pub struct CursorShapeAndPosition { pub trait WidgetImpl { /// Called once by the widget manager to inform the widget /// of its identifier - fn set_widget_id(&mut self, id: WidgetId); + fn set_widget_id(&mut self, _id: WidgetId) {} /// Handle an event - fn process_event(&mut self, event: &WidgetEvent) -> EventDisposition; + fn process_event(&mut self, _event: &WidgetEvent) -> EventDisposition { + EventDisposition::Propagate + } /// Interrogates the widget to ask if it has any sizing constraints - fn get_size_constraints(&self) -> SizeConstraints; + fn get_size_constraints(&self) -> layout::Constraints { + Default::default() + } fn render_to_surface(&self, surface: &mut Surface); /// Called for the focused widget to determine how to render /// the cursor. - fn get_cursor_shape_and_position(&self) -> CursorShapeAndPosition; + fn get_cursor_shape_and_position(&self) -> CursorShapeAndPosition { + Default::default() + } } /// Relative to the top left of the parent container @@ -141,6 +123,26 @@ impl WidgetHandle { } } +impl PartialEq for WidgetHandle { + fn eq(&self, other: &WidgetHandle) -> bool { + self.inner.as_ptr() == other.inner.as_ptr() + } +} + +impl Eq for WidgetHandle {} + +impl Hash for WidgetHandle { + fn hash(&self, state: &mut H) { + self.inner.as_ptr().hash(state) + } +} + +impl Debug for WidgetHandle { + fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> { + write!(f, "WidgetHandle({:?})", self.inner.as_ptr()) + } +} + /// An identifier assigned by the widget management machinery. #[derive(Clone)] pub struct WidgetId { @@ -159,7 +161,7 @@ impl WidgetId { impl Widget { pub fn new(widget: Box) -> WidgetHandle { - let (width, height) = widget.get_size_constraints().deduce_initial_size(); + let (width, height) = (80, 24); //widget.get_size_constraints().deduce_initial_size(); let surface = Surface::new(width, height); let coordinates = ParentRelativeCoords::new(0, 0); let children = Vec::new(); @@ -189,6 +191,10 @@ impl Widget { self.inner.process_event(event) } + pub fn get_size_constraints(&self) -> layout::Constraints { + self.inner.get_size_constraints() + } + pub fn render_to_screen(&mut self, screen: &mut Surface) { self.inner.render_to_surface(&mut self.surface); for child in &mut self.children { @@ -199,6 +205,21 @@ impl Widget { screen.draw_from_screen(&self.surface, self.coordinates.x, self.coordinates.y); } + pub fn get_size_and_position(&self) -> (usize, usize, ParentRelativeCoords) { + let (width, height) = self.surface.dimensions(); + (width, height, self.coordinates) + } + + pub fn set_size_and_position( + &mut self, + width: usize, + height: usize, + coords: ParentRelativeCoords, + ) { + self.surface.resize(width, height); + self.coordinates = coords; + } + pub fn parent(&self) -> Option { self.parent.handle() } @@ -274,7 +295,10 @@ impl Screen { /// Rendering starts at the root (which can be considered the lowest layer), /// and then progresses up through its children. - pub fn render_to_screen(&mut self, screen: &mut Surface) { + pub fn render_to_screen(&mut self, screen: &mut Surface) -> Result<(), Error> { + let (width, height) = screen.dimensions(); + layout::LayoutState::compute_layout(width, height, &self.root_widget)?; + self.root_widget.borrow_mut().render_to_screen(screen); let focused = self.focused_widget.borrow(); @@ -289,5 +313,7 @@ impl Screen { y: Position::Absolute(coords.y), }, ]); + + Ok(()) } }