From b38995b90688296201446bf5740702c82d01a8fc Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Wed, 3 Aug 2016 21:55:41 -0700 Subject: [PATCH] Refactor `BoxView` Add `Boxable` trait. --- examples/key_codes.rs | 2 +- examples/linear.rs | 8 +- examples/select.rs | 9 +- examples/text_area.rs | 7 +- src/prelude.rs | 2 +- src/view/boxable.rs | 76 +++++++++++++++ src/view/identifiable.rs | 14 +++ src/view/mod.rs | 76 ++------------- src/view/size_cache.rs | 55 +++++++++++ src/view/size_constraint.rs | 50 ++++++++++ src/views/box_view.rs | 178 ++++++++++++++++++++++++++---------- src/xy.rs | 5 + 12 files changed, 355 insertions(+), 127 deletions(-) create mode 100644 src/view/boxable.rs create mode 100644 src/view/identifiable.rs create mode 100644 src/view/size_cache.rs create mode 100644 src/view/size_constraint.rs diff --git a/examples/key_codes.rs b/examples/key_codes.rs index d961c6f..48debce 100644 --- a/examples/key_codes.rs +++ b/examples/key_codes.rs @@ -6,7 +6,7 @@ use cursive::event::EventResult; fn main() { let mut siv = Cursive::new(); - siv.add_layer(BoxView::fixed_size((30, 10), KeyCodeView::new(10))); + siv.add_layer(KeyCodeView::new(10).fixed_size((30, 10))); siv.run(); } diff --git a/examples/linear.rs b/examples/linear.rs index 645a708..5985968 100644 --- a/examples/linear.rs +++ b/examples/linear.rs @@ -16,10 +16,10 @@ fn main() { .child(TextView::new("Title").h_align(HAlign::Center)) // Box the textview, so it doesn't get too wide. // A 0 height value means it will be unconstrained. - .child(BoxView::fixed_width(30, TextView::new(text).scrollable(false))) - .child(BoxView::fixed_width(30, TextView::new(text))) - .child(BoxView::fixed_width(30, TextView::new(text))) - .child(BoxView::fixed_width(30, TextView::new(text)))) + .child(TextView::new(text).scrollable(false).fixed_width(30)) + .child(TextView::new(text).fixed_width(30)) + .child(TextView::new(text).fixed_width(30)) + .child(TextView::new(text).fixed_width(30))) .button("Quit", |s| s.quit()) .h_align(HAlign::Center)); diff --git a/examples/select.rs b/examples/select.rs index bb47598..8ba9d3b 100644 --- a/examples/select.rs +++ b/examples/select.rs @@ -17,8 +17,8 @@ fn main() { let mut siv = Cursive::new(); // Let's add a BoxView to keep the list at a reasonable size - it can scroll anyway. - siv.add_layer(Dialog::new(BoxView::fixed_size((20, 10), select)) - .title("Where are you from?")); + siv.add_layer(Dialog::new(select.fixed_size((20, 10))) + .title("Where are you from?")); siv.run(); } @@ -26,6 +26,7 @@ fn main() { // Let's put the callback in a separate function to keep it clean, but it's not required. fn show_next_window(siv: &mut Cursive, city: &String) { siv.pop_layer(); - siv.add_layer(Dialog::new(TextView::new(format!("{} is a great city!", city))) - .button("Quit", |s| s.quit())); + let text = format!("{} is a great city!", city); + siv.add_layer(Dialog::new(TextView::new(text)) + .button("Quit", |s| s.quit())); } diff --git a/examples/text_area.rs b/examples/text_area.rs index 4339ad6..028f781 100644 --- a/examples/text_area.rs +++ b/examples/text_area.rs @@ -7,9 +7,10 @@ fn main() { siv.add_layer(Dialog::empty() .title("Describe your issue") - .padding((1,1,1,0)) - .content(BoxView::fixed_size((30, 5), - TextArea::new().with_id("text"))) + .padding((1, 1, 1, 0)) + .content(TextArea::new() + .with_id("text") + .fixed_size((30, 5))) .button("Ok", Cursive::quit)); siv.run(); diff --git a/src/prelude.rs b/src/prelude.rs index 8cdac79..d9e90b9 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -8,7 +8,7 @@ pub use {Cursive, Printer, With}; pub use event::{Event, Key}; -pub use view::{Identifiable, Selector, View}; +pub use view::{Boxable, Identifiable, Selector, View}; pub use views::{BoxView, Button, Checkbox, Dialog, EditView, FullView, IdView, KeyEventView, LinearLayout, ListView, Panel, ProgressBar, SelectView, TextArea, TextView}; diff --git a/src/view/boxable.rs b/src/view/boxable.rs new file mode 100644 index 0000000..09cd182 --- /dev/null +++ b/src/view/boxable.rs @@ -0,0 +1,76 @@ +use view::{SizeConstraint, View}; +use views::BoxView; +use vec::Vec2; + +/// Makes a view wrappable in a [`BoxView`]. +/// +/// [`BoxView`]: ../views/struct.BoxView.html +pub trait Boxable: View + Sized { + + /// Wraps `self` in a `BoxView` with the given size constraints. + fn boxed(self, width: SizeConstraint, height: SizeConstraint) -> BoxView { + BoxView::new(width, height, self) + } + + /// Wraps `self` into a fixed-size `BoxView`. + fn fixed_size>(self, size: S) -> BoxView { + BoxView::with_fixed_size(size, self) + } + + /// Wraps `self` into a fixed-width `BoxView`. + fn fixed_width(self, width: usize) -> BoxView { + BoxView::with_fixed_width(width, self) + } + + /// Wraps `self` into a fixed-width `BoxView`. + fn fixed_height(self, height: usize) -> BoxView { + BoxView::with_fixed_height(height, self) + } + + /// Wraps `self` into a full-screen `BoxView`. + fn full_screen(self) -> BoxView { + BoxView::with_full_screen(self) + } + + /// Wraps `self` into a full-width `BoxView`. + fn full_width(self) -> BoxView { + BoxView::with_full_width(self) + } + + /// Wraps `self` into a full-height `BoxView`. + fn full_height(self) -> BoxView { + BoxView::with_full_height(self) + } + + /// Wraps `self` into a limited-size `BoxView`. + fn max_size>(self, size: S) -> BoxView { + BoxView::with_max_size(size, self) + } + + /// Wraps `self` into a limited-width `BoxView`. + fn max_width(self, max_width: usize) -> BoxView { + BoxView::with_max_width(max_width, self) + } + + /// Wraps `self` into a limited-height `BoxView`. + fn max_height(self, max_height: usize) -> BoxView { + BoxView::with_max_height(max_height, self) + } + + /// Wraps `self` into a `BoxView` at least sized `size`. + fn min_size>(self, size: S) -> BoxView { + BoxView::with_min_size(size, self) + } + + /// Wraps `self` in a `BoxView` at least `min_width` wide. + fn min_width(self, min_width: usize) -> BoxView { + BoxView::with_min_width(min_width, self) + } + + /// Wraps `self` in a `BoxView` at least `min_height` tall. + fn min_height(self, min_height: usize) -> BoxView { + BoxView::with_min_height(min_height, self) + } +} + +impl Boxable for T {} diff --git a/src/view/identifiable.rs b/src/view/identifiable.rs new file mode 100644 index 0000000..7c4fe0c --- /dev/null +++ b/src/view/identifiable.rs @@ -0,0 +1,14 @@ +use views::IdView; +use view::View; + +/// Makes a view wrappable in an [`IdView`]. +/// +/// [`IdView`]: ../views/struct.IdView.html +pub trait Identifiable: View + Sized { + /// Wraps this view into an IdView with the given id. + fn with_id(self, id: &str) -> IdView { + IdView::new(id, self) + } +} + +impl Identifiable for T {} diff --git a/src/view/mod.rs b/src/view/mod.rs index ccb99c1..38fc83e 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -40,17 +40,18 @@ mod view_wrapper; // Essentials components mod position; +mod size_cache; +mod size_constraint; mod view_path; // Helper bases mod scroll; - -// Views +mod identifiable; +mod boxable; use std::any::Any; -use XY; use direction::Direction; use event::{Event, EventResult}; use vec::Vec2; @@ -60,8 +61,12 @@ pub use self::position::{Offset, Position}; pub use self::scroll::ScrollBase; +pub use self::size_cache::SizeCache; +pub use self::size_constraint::SizeConstraint; pub use self::view_path::ViewPath; pub use self::view_wrapper::ViewWrapper; +pub use self::identifiable::Identifiable; +pub use self::boxable::Boxable; /// Main trait defining a view behaviour. @@ -121,61 +126,6 @@ pub trait View { } } - -/// Cache around a one-dimensional layout result. -/// -/// This is not a View, but something to help you if you create your own Views. -#[derive(PartialEq, Debug, Clone, Copy)] -pub struct SizeCache { - /// Cached value - pub value: usize, - /// `true` if the last size was constrained. - /// - /// If unconstrained, any request larger than this value - /// would return the same size. - pub constrained: bool, -} - -impl SizeCache { - /// Creates a new sized cache - pub fn new(value: usize, constrained: bool) -> Self { - SizeCache { - value: value, - constrained: constrained, - } - } - - /// Returns `true` if `self` is still valid for the given `request`. - pub fn accept(self, request: usize) -> bool { - if request < self.value { - false - } else if request == self.value { - true - } else { - !self.constrained - } - } - - /// Creates a new bi-dimensional cache. - /// - /// It will stay valid for the same request, and compatible ones. - /// - /// A compatible request is one where, for each axis, either: - /// - /// * the request is equal to the cached size, or - /// * the request is larger than the cached size and the cache is unconstrained - /// - /// Notes: - /// - /// * `size` must fit inside `req`. - /// * for each dimension, `constrained = (size == req)` - pub fn build(size: Vec2, req: Vec2) -> XY { - XY::new(SizeCache::new(size.x, size.x >= req.x), - SizeCache::new(size.y, size.y >= req.y)) - } -} - - /// Selects a single view (if any) in the tree. pub enum Selector<'a> { /// Selects a view from its ID. @@ -183,13 +133,3 @@ pub enum Selector<'a> { /// Selects a view from its path. Path(&'a ViewPath), } - -/// Makes a view wrappable in an `IdView`. -pub trait Identifiable: View + Sized { - /// Wraps this view into an IdView with the given id. - fn with_id(self, id: &str) -> ::views::IdView { - ::views::IdView::new(id, self) - } -} - -impl Identifiable for T {} diff --git a/src/view/size_cache.rs b/src/view/size_cache.rs new file mode 100644 index 0000000..ca99e21 --- /dev/null +++ b/src/view/size_cache.rs @@ -0,0 +1,55 @@ +use XY; +use vec::Vec2; + +/// Cache around a one-dimensional layout result. +/// +/// This is not a View, but something to help you if you create your own Views. +#[derive(PartialEq, Debug, Clone, Copy)] +pub struct SizeCache { + /// Cached value + pub value: usize, + /// `true` if the last size was constrained. + /// + /// If unconstrained, any request larger than this value + /// would return the same size. + pub constrained: bool, +} + +impl SizeCache { + /// Creates a new sized cache + pub fn new(value: usize, constrained: bool) -> Self { + SizeCache { + value: value, + constrained: constrained, + } + } + + /// Returns `true` if `self` is still valid for the given `request`. + pub fn accept(self, request: usize) -> bool { + if request < self.value { + false + } else if request == self.value { + true + } else { + !self.constrained + } + } + + /// Creates a new bi-dimensional cache. + /// + /// It will stay valid for the same request, and compatible ones. + /// + /// A compatible request is one where, for each axis, either: + /// + /// * the request is equal to the cached size, or + /// * the request is larger than the cached size and the cache is unconstrained + /// + /// Notes: + /// + /// * `size` must fit inside `req`. + /// * for each dimension, `constrained = (size == req)` + pub fn build(size: Vec2, req: Vec2) -> XY { + XY::new(SizeCache::new(size.x, size.x >= req.x), + SizeCache::new(size.y, size.y >= req.y)) + } +} diff --git a/src/view/size_constraint.rs b/src/view/size_constraint.rs new file mode 100644 index 0000000..2b21a1e --- /dev/null +++ b/src/view/size_constraint.rs @@ -0,0 +1,50 @@ +use std::cmp::min; + +/// Single-dimensional constraint on a view size. +/// +/// This describes a possible behaviour for a [`BoxView`]. +/// +/// [`BoxView`]: ../views/struct.BoxView.html +#[derive(Debug, Clone, Copy)] +pub enum SizeConstraint { + /// No constraint imposed, the child view's response is used. + Free, + /// Tries to take all available space, no matter what the child needs. + Full, + /// Always return the included size, no matter what the child needs. + Fixed(usize), + /// Returns the minimum of the included value and the child view's size. + AtMost(usize), + /// Returns the maximum of the included value and the child view's size. + AtLeast(usize), +} + +impl SizeConstraint { + + /// Returns the size to be given to the child. + /// + /// When `available` is offered to the `BoxView`. + pub fn available(self, available: usize) -> usize { + match self { + SizeConstraint::Free | + SizeConstraint::Full | + SizeConstraint::AtLeast(_) => available, + // If the available space is too small, always give in. + SizeConstraint::Fixed(value) | + SizeConstraint::AtMost(value) => min(value, available), + } + } + + /// Returns the size the child view should actually use. + /// + /// When it said it wanted `result`. + pub fn result(self, (result, available): (usize, usize)) -> usize { + match self { + SizeConstraint::AtLeast(value) if result < value => value, + SizeConstraint::AtMost(value) if result > value => value, + SizeConstraint::Fixed(value) => value, + SizeConstraint::Full => available, + _ => result, + } + } +} diff --git a/src/views/box_view.rs b/src/views/box_view.rs index 71c706d..4793274 100644 --- a/src/views/box_view.rs +++ b/src/views/box_view.rs @@ -1,64 +1,150 @@ -use std::cmp; - use XY; use vec::Vec2; -use view::{View, ViewWrapper}; +use view::{SizeConstraint, View, ViewWrapper}; -/// Wrapper around another view, with a fixed size. +/// Wrapper around another view, with a controlled size. /// -/// Each axis can be enabled independantly. +/// Each axis can independently be set to: /// -/// * If both axis are fixed, the view always asks for this size. -/// * If both axis are left free, the wrapper has no effect and the underlying -/// view is directly queried. -/// * If only one axis is fixed, it will override the size request when -/// querying the wrapped view. +/// * Keep a **fixed** size +/// * Use **all** available size +/// * Use **at most** a given size +/// * Use **at least** a given size +/// * Let the wrapped view decide. /// /// # Examples /// /// ``` /// # use cursive::views::{BoxView,TextView}; /// // Creates a 20x4 BoxView with a TextView content. -/// let view = BoxView::fixed_size((20,4), TextView::new("Hello!")); +/// let view = BoxView::with_fixed_size((20,4), TextView::new("Hello!")); /// ``` pub struct BoxView { - size: XY>, + /// Constraint on each axis + size: XY, + + /// `true` if the view can be squished. + /// + /// This means if the required size is less than the computed size, + /// consider returning a smaller size. + /// For instance, try to return the child's desires size. + squishable: bool, + + /// The actual view we're wrapping. view: T, } impl BoxView { - /// Wraps `view` in a new `BoxView` with the given size. - pub fn fixed_size>(size: S, view: T) -> Self { - let size = size.into(); - - BoxView::new(Some(size.x), Some(size.y), view) - } - /// Creates a new `BoxView` with the given width and height requirements. /// /// `None` values will use the wrapped view's preferences. - pub fn new(width: Option, height: Option, view: T) -> Self { + pub fn new(width: SizeConstraint, height: SizeConstraint, view: T) -> Self { BoxView { size: (width, height).into(), + squishable: false, view: view, } } + /// Sets `self` to be squishable. + /// + /// A squishable `BoxView` will take a smaller size than it should when + /// the available space is too small. In that case, it will allow the + /// child view to contract, if it can. + /// + /// More specifically, if the available space is less than the size we + /// would normally ask for, return the child size. + pub fn squishable(mut self) -> Self { + self.squishable = true; + self + } + + /// Wraps `view` in a new `BoxView` with the given size. + pub fn with_fixed_size>(size: S, view: T) -> Self { + let size = size.into(); + + BoxView::new(SizeConstraint::Fixed(size.x), + SizeConstraint::Fixed(size.y), + view) + } + /// Wraps `view` in a new `BoxView` with fixed width. - pub fn fixed_width(width: usize, view: T) -> Self { - BoxView::new(Some(width), None, view) + pub fn with_fixed_width(width: usize, view: T) -> Self { + BoxView::new(SizeConstraint::Fixed(width), SizeConstraint::Free, view) } /// Wraps `view` in a new `BoxView` with fixed height. - pub fn fixed_height(height: usize, view: T) -> Self { - BoxView::new(None, Some(height), view) + pub fn with_fixed_height(height: usize, view: T) -> Self { + BoxView::new(SizeConstraint::Free, SizeConstraint::Fixed(height), view) } -} -fn min(a: T, b: Option) -> T { - match b { - Some(b) => cmp::min(a, b), - None => a, + /// Wraps `view` in a `BoxView` which will take all available space. + pub fn with_full_screen(view: T) -> Self { + BoxView::new(SizeConstraint::Full, SizeConstraint::Full, view) + } + + /// Wraps `view` in a `BoxView` which will take all available width. + pub fn with_full_width(view: T) -> Self { + BoxView::new(SizeConstraint::Full, SizeConstraint::Free, view) + } + + /// Wraps `view` in a `BoxView` which will take all available height. + pub fn with_full_height(view: T) -> Self { + BoxView::new(SizeConstraint::Free, SizeConstraint::Full, view) + } + + /// Wraps `view` in a `BoxView` which will never be bigger than `size`. + pub fn with_max_size>(size: S, view: T) -> Self { + let size = size.into(); + + BoxView::new(SizeConstraint::AtMost(size.x), + SizeConstraint::AtMost(size.y), + view) + } + + /// Wraps `view` in a `BoxView` which will enforce a maximum width. + /// + /// The resulting width will never be more than `max_width`. + pub fn with_max_width(max_width: usize, view: T) -> Self { + BoxView::new(SizeConstraint::AtMost(max_width), + SizeConstraint::Free, + view) + } + + /// Wraps `view` in a `BoxView` which will enforce a maximum height. + /// + /// The resulting height will never be more than `max_height`. + pub fn with_max_height(max_height: usize, view: T) -> Self { + BoxView::new(SizeConstraint::Free, + SizeConstraint::AtMost(max_height), + view) + } + + /// Wraps `view` in a `BoxView` which will never be smaller than `size`. + pub fn with_min_size>(size: S, view: T) -> Self { + let size = size.into(); + + BoxView::new(SizeConstraint::AtLeast(size.x), + SizeConstraint::AtLeast(size.y), + view) + } + + /// Wraps `view` in a `BoxView` which will enforce a minimum width. + /// + /// The resulting width will never be less than `min_width`. + pub fn with_min_width(min_width: usize, view: T) -> Self { + BoxView::new(SizeConstraint::AtLeast(min_width), + SizeConstraint::Free, + view) + } + + /// Wraps `view` in a `BoxView` which will enforce a minimum height. + /// + /// The resulting height will never be less than `min_height`. + pub fn with_min_height(min_height: usize, view: T) -> Self { + BoxView::new(SizeConstraint::Free, + SizeConstraint::AtLeast(min_height), + view) } } @@ -67,24 +153,24 @@ impl ViewWrapper for BoxView { fn wrap_get_min_size(&mut self, req: Vec2) -> Vec2 { - if let (Some(w), Some(h)) = self.size.pair() { - // If we know everything already, no need to ask - Vec2::new(w, h) + let req = self.size.zip_map(req, SizeConstraint::available); + let child_size = self.view.get_min_size(req); + let result = self.size + .zip_map(child_size.zip(req), SizeConstraint::result); + + if self.squishable { + // We respect the request if we're less or equal. + let respect_req = result.zip_map(req, |res, req| res <= req); + result.zip_map(respect_req.zip(child_size), + |res, (respect, child)| if respect { + // If we respect the request, keep the result + res + } else { + // Otherwise, take the child as squish attempt. + child + }) } else { - // If req < self.size in any axis, we're screwed. - // TODO: handle insufficient space - // (should probably show an error message or a blank canvas) - - // From now on, we assume req >= self.size. - - // Override the request on the restricted axis - let req = req.zip_map(self.size, min); - - // Back in my time, we didn't ask kids for their opinions! - let child_size = self.view.get_min_size(req); - - // This calls unwrap_or on each axis - self.size.unwrap_or(child_size) + result } } } diff --git a/src/xy.rs b/src/xy.rs index b0934b4..31a6031 100644 --- a/src/xy.rs +++ b/src/xy.rs @@ -45,6 +45,11 @@ impl XY { } } + /// Returns a new `XY` of tuples made by zipping `self` and `other`. + pub fn zip(self, other: XY) -> XY<(T, U)> { + XY::new((self.x, other.x), (self.y, other.y)) + } + /// Returns a new `XY` by calling `f` on `self` and `other` for each axis. pub fn zip_map V>(self, other: XY, f: F) -> XY { XY::new(f(self.x, other.x), f(self.y, other.y))