Fix linear layout in constrained space

This commit is contained in:
Alexandre Bury 2016-07-13 01:19:05 -07:00
parent 12593c8ea8
commit 03c400ad44
9 changed files with 231 additions and 65 deletions

View File

@ -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.

View File

@ -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
}
}

View File

@ -175,11 +175,11 @@ impl Printer {
/// Returns a printer on a subset of this one's area.
pub fn sub_printer<S: Into<Vec2>>(&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(),
}

View File

@ -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<T: Into<Vec2>>(&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 {

View File

@ -68,6 +68,6 @@ impl<T: View> ViewWrapper for BoxView<T> {
Vec2::new(self.size.x.unwrap_or(child_size.x),
self.size.y.unwrap_or(child_size.y))
}
}.or_min(req)
}
}

View File

@ -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<Child>,
orientation: Orientation,
focus: usize,
last_size: Option<Vec2>,
cache: Option<XY<SizeCache>>,
}
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<Vec2> {
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<Vec2> = 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<Vec2> = 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<Vec2> = 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<Vec2> = 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<Vec2> = 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 {

View File

@ -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<Self> {
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

View File

@ -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<SizeCache> {
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 {

View File

@ -15,6 +15,11 @@ impl<T> XY<T> {
XY { x: x, y: y }
}
/// Creates a new `XY` by applying `f` to `x` and `y`.
pub fn map<U, F: Fn(T) -> U>(self, f: F) -> XY<U> {
XY::new(f(self.x), f(self.y))
}
/// Destructure self into a pair.
pub fn pair(self) -> (T, T) {
(self.x, self.y)