From 03c400ad44b168018f4acac93a8b56786fce0292 Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Wed, 13 Jul 2016 01:19:05 -0700 Subject: [PATCH] Fix linear layout in constrained space --- examples/linear.rs | 10 +-- src/orientation.rs | 7 ++ src/printer.rs | 6 +- src/vec.rs | 16 ++++ src/view/box_view.rs | 2 +- src/view/linear_layout.rs | 169 +++++++++++++++++++++++++++++++++----- src/view/mod.rs | 44 ++++++++++ src/view/text_view.rs | 37 +-------- src/xy.rs | 5 ++ 9 files changed, 231 insertions(+), 65 deletions(-) diff --git a/examples/linear.rs b/examples/linear.rs index 906d324..0a5341d 100644 --- a/examples/linear.rs +++ b/examples/linear.rs @@ -1,19 +1,19 @@ extern crate cursive; use cursive::Cursive; -use cursive::view::{Dialog,TextView,LinearLayout,BoxView}; +use cursive::view::{BoxView, Dialog, LinearLayout, TextView}; use cursive::align::HAlign; fn main() { let mut siv = Cursive::new(); // Some description text - let text = "This is a very simple example of linear layout. Two views are present, a short title above, and this text. The text has a fixed width, and the title is centered horizontally."; + let text = "This is a very simple example of linear layout. Two views \ + are present, a short title above, and this text. The text \ + has a fixed width, and the title is centered horizontally."; // We'll create a dialog with a TextView serving as a title - siv.add_layer( - Dialog::new( - LinearLayout::vertical() + siv.add_layer(Dialog::new(LinearLayout::vertical() .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. diff --git a/src/orientation.rs b/src/orientation.rs index 8b26282..f2e6ac5 100644 --- a/src/orientation.rs +++ b/src/orientation.rs @@ -54,4 +54,11 @@ impl Orientation { } } } + + /// Creates a new `Vec2` with `value` in `self`'s axis. + pub fn make_vec(&self, value: usize) -> Vec2 { + let mut result = Vec2::zero(); + *self.get_ref(&mut result) = value; + result + } } diff --git a/src/printer.rs b/src/printer.rs index a556348..03451ec 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -175,11 +175,11 @@ impl Printer { /// Returns a printer on a subset of this one's area. pub fn sub_printer>(&self, offset: S, size: S, focused: bool) -> Printer { - let offset_v = offset.into(); + let offset = offset.into().or_min(self.size); Printer { - offset: self.offset + offset_v, + offset: self.offset + offset, // We can't be larger than what remains - size: Vec2::min(self.size - offset_v, size.into()), + size: Vec2::min(self.size - offset, size.into()), focused: self.focused && focused, theme: self.theme.clone(), } diff --git a/src/vec.rs b/src/vec.rs index 6d6fd8a..a40f258 100644 --- a/src/vec.rs +++ b/src/vec.rs @@ -1,5 +1,6 @@ //! Points on the 2D character grid. use XY; +use orientation::Orientation; use std::ops::{Add, Div, Mul, Sub}; use std::cmp::{Ordering, max, min}; @@ -70,6 +71,21 @@ impl Vec2 { pub fn stack_horizontal(&self, other: &Vec2) -> Vec2 { Vec2::new(self.x + other.x, max(self.y, other.y)) } + + /// Returns `true` if `self` could fit inside `other`. + /// + /// Shortcut for `self.x <= other.x && self.y <= other.y`. + pub fn fits_in>(&self, other: T) -> bool { + let other = other.into(); + self.x <= other.x && self.y <= other.y + } + + /// Returns a new `Vec2` with the axis `o` set to `value`. + pub fn with(&self, o: Orientation, value: usize) -> Self { + let mut other = self.clone(); + *o.get_ref(&mut other) = value; + other + } } impl From<(i32, i32)> for Vec2 { diff --git a/src/view/box_view.rs b/src/view/box_view.rs index 989dfb7..0910832 100644 --- a/src/view/box_view.rs +++ b/src/view/box_view.rs @@ -68,6 +68,6 @@ impl ViewWrapper for BoxView { Vec2::new(self.size.x.unwrap_or(child_size.x), self.size.y.unwrap_or(child_size.y)) - } + }.or_min(req) } } diff --git a/src/view/linear_layout.rs b/src/view/linear_layout.rs index 8a79015..dd1b235 100644 --- a/src/view/linear_layout.rs +++ b/src/view/linear_layout.rs @@ -1,16 +1,20 @@ +use XY; use view::View; +use view::SizeCache; use vec::Vec2; use printer::Printer; use orientation::Orientation; use event::{Event, EventResult, Key}; +use std::cmp::min; + /// Arranges its children linearly according to its orientation. pub struct LinearLayout { children: Vec, orientation: Orientation, focus: usize, - last_size: Option, + cache: Option>, } struct Child { @@ -19,6 +23,13 @@ struct Child { weight: usize, } +impl Child { + fn get_min_size(&mut self, req: Vec2) -> Vec2 { + self.size = self.view.get_min_size(req); + self.size + } +} + impl LinearLayout { /// Creates a new layout with the given orientation. pub fn new(orientation: Orientation) -> Self { @@ -26,7 +37,7 @@ impl LinearLayout { children: Vec::new(), orientation: orientation, focus: 0, - last_size: None, + cache: None, } } @@ -46,11 +57,16 @@ impl LinearLayout { size: Vec2::zero(), weight: 0, }); - self.last_size = None; + self.invalidate(); self } + // Invalidate the view, to request a layout next time + fn invalidate(&mut self) { + self.cache = None; + } + /// Creates a new vertical layout. pub fn vertical() -> Self { LinearLayout::new(Orientation::Vertical) @@ -60,6 +76,31 @@ impl LinearLayout { pub fn horizontal() -> Self { LinearLayout::new(Orientation::Horizontal) } + + // If the cache can be used, return the cached size. + // Otherwise, return None. + fn get_cache(&self, req: Vec2) -> Option { + match self.cache { + None => None, + Some(ref cache) => { + // Is our cache even valid? + // Also, is any child invalidating the layout? + if cache.x.accept(req.x) && cache.y.accept(req.y) && + self.children_are_sleeping() { + Some(cache.map(|s| s.value)) + } else { + None + } + } + } + } + + fn children_are_sleeping(&self) -> bool { + !self.children + .iter() + .map(|c| &*c.view) + .any(View::needs_relayout) + } } /// Returns the index of the maximum element. @@ -129,23 +170,29 @@ impl View for LinearLayout { } fn needs_relayout(&self) -> bool { - if self.last_size == None { + if self.cache.is_none() { return true; } - for child in &self.children { - if child.view.needs_relayout() { - return true; - } - } - - false + !self.children_are_sleeping() } fn layout(&mut self, size: Vec2) { - // Compute the very minimal required size - // Look how mean we are: we offer the whole size to every child. - // As if they could get it all. + // If we can get away without breaking a sweat, you can bet we will. + if self.get_cache(size).is_none() { + self.get_min_size(size); + } + + for child in &mut self.children { + // println_stderr!("Child size: {:?}", child.size); + child.view.layout(child.size); + } + + /* + + // Need to compute things again... + self.get_min_size(size); + let min_sizes: Vec = self.children .iter_mut() .map(|child| Vec2::min(size, child.view.get_min_size(size))) @@ -180,26 +227,104 @@ impl View for LinearLayout { child.size = child_size; child.view.layout(child_size); } + */ } fn get_min_size(&mut self, req: Vec2) -> Vec2 { + // Did anything change since last time? + if let Some(size) = self.get_cache(req) { + return size; + } + // First, make a naive scenario: everything will work fine. let sizes: Vec = self.children .iter_mut() - .map(|view| view.view.get_min_size(req)) + .map(|c| c.get_min_size(req)) .collect(); - self.orientation.stack(sizes.iter()) + // println_stderr!("Ideal sizes: {:?}", sizes); + let ideal = self.orientation.stack(sizes.iter()); + // println_stderr!("Ideal result: {:?}", ideal); - // Did it work? Champagne! + // Does it fit? + if ideal.fits_in(req) { + // Champagne! + self.cache = Some(SizeCache::build(ideal, req)); + return ideal; + } + + // Ok, so maybe it didn't. + // Budget cuts, everyone. + let budget_req = req.with(self.orientation, 1); + // println_stderr!("Budget req: {:?}", budget_req); + + let min_sizes: Vec = self.children + .iter_mut() + .map(|c| c.get_min_size(budget_req)) + .collect(); + let desperate = self.orientation.stack(min_sizes.iter()); + // println_stderr!("Min sizes: {:?}", min_sizes); + // println_stderr!("Desperate: {:?}", desperate); + + // I really hope it fits this time... + if !desperate.fits_in(req) { + // Just give up... + // println_stderr!("Seriously? {:?} > {:?}???", desperate, req); + self.cache = Some(SizeCache::build(desperate, req)); + return desperate; + } + + // This here is how much we're generously offered + let mut available = self.orientation.get(&(req - desperate)); + // println_stderr!("Available: {:?}", available); + + // Here, we have to make a compromise between the ideal + // and the desperate solutions. + let mut overweight: Vec<(usize, usize)> = sizes.iter() + .map(|v| self.orientation.get(v)) + .zip(min_sizes.iter().map(|v| self.orientation.get(v))) + .map(|(a, b)| a - b) + .enumerate() + .collect(); + // println_stderr!("Overweight: {:?}", overweight); + + // So... distribute `available` to reduce the overweight... + // TODO: use child weight in the distribution... + overweight.sort_by_key(|&(_, weight)| weight); + let mut allocations = vec![0; overweight.len()]; + + for (i, &(j, weight)) in overweight.iter().enumerate() { + let remaining = overweight.len() - i; + let budget = available / remaining; + let spent = min(budget, weight); + allocations[j] = spent; + available -= spent; + } + // println_stderr!("Allocations: {:?}", allocations); + + // Final lengths are the minimum ones + allocations + let final_lengths: Vec = min_sizes.iter() + .map(|v| self.orientation.get(v)) + .zip(allocations.iter()) + .map(|(a, b)| a + b) + .map(|l| req.with(self.orientation, l)) + .collect(); + // println_stderr!("Final sizes: {:?}", final_lengths); + + let final_sizes: Vec = self.children + .iter_mut() + .enumerate() + .map(|(i, c)| { + c.get_min_size(final_lengths[i]) + }) + .collect(); + // println_stderr!("Final sizes2: {:?}", final_sizes); - // TODO: Ok, so maybe it didn't. - // Last chance: did someone lie about his needs? - // Could we squash him a little? - // (Maybe he'll just scroll and it'll be fine?) + let compromise = self.orientation.stack(final_sizes.iter()); + self.cache = Some(SizeCache::build(compromise, req)); - // Find out who's fluid, if any. + compromise } fn on_event(&mut self, event: Event) -> EventResult { diff --git a/src/view/mod.rs b/src/view/mod.rs index 15062ec..c92bb42 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -30,6 +30,7 @@ mod tracked_view; use std::any::Any; +use XY; use event::{Event, EventResult}; use vec::Vec2; use printer::Printer; @@ -105,6 +106,49 @@ pub trait View { } } +/// Cache around a one-dimensional layout result +#[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. + /// + /// * `size` must fit inside `req`. + /// * for each dimension, `constrained = (size == req)` + 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 diff --git a/src/view/text_view.rs b/src/view/text_view.rs index d204cf0..25064bd 100644 --- a/src/view/text_view.rs +++ b/src/view/text_view.rs @@ -1,6 +1,7 @@ use XY; use vec::Vec2; use view::View; +use view::SizeCache; use printer::Printer; use align::*; use event::*; @@ -9,38 +10,6 @@ use super::scroll::ScrollBase; use unicode_width::UnicodeWidthStr; use unicode_segmentation::UnicodeSegmentation; -#[derive(PartialEq, Debug, Clone)] -struct SizeCache { - value: usize, - // If unconstrained, any request larger than this value - // would return the same size. - constrained: bool, -} - -impl SizeCache { - fn new(value: usize, constrained: bool) -> Self { - SizeCache { - value: value, - constrained: constrained, - } - } - - fn accept(&self, size: usize) -> bool { - if size < self.value { - false - } else if size == self.value { - true - } else { - !self.constrained - } - } -} - -fn cache_size(size: Vec2, req: Vec2) -> XY { - XY::new(SizeCache::new(size.x, size.x == req.x), - SizeCache::new(size.y, size.y == req.y)) - -} /// A simple view showing a fixed text pub struct TextView { @@ -147,7 +116,7 @@ impl TextView { // Our resulting size. let my_size = size.or_min((self.width.unwrap_or(0), self.rows.len())); - self.last_size = Some(cache_size(my_size, size)); + self.last_size = Some(SizeCache::build(my_size, size)); } } @@ -267,7 +236,7 @@ impl View for TextView { fn get_min_size(&mut self, size: Vec2) -> Vec2 { self.compute_rows(size); - Vec2::new(self.width.unwrap_or(0), self.rows.len()) + size.or_min((self.width.unwrap_or(0), self.rows.len())) } fn take_focus(&mut self) -> bool { diff --git a/src/xy.rs b/src/xy.rs index 79bc264..106b2fd 100644 --- a/src/xy.rs +++ b/src/xy.rs @@ -15,6 +15,11 @@ impl XY { XY { x: x, y: y } } + /// Creates a new `XY` by applying `f` to `x` and `y`. + pub fn map U>(self, f: F) -> XY { + XY::new(f(self.x), f(self.y)) + } + /// Destructure self into a pair. pub fn pair(self) -> (T, T) { (self.x, self.y)