Add FixedLayout

This commit is contained in:
Alexandre Bury 2020-06-29 15:24:41 -07:00
parent 28cd51c265
commit bd6386fd74
11 changed files with 434 additions and 9 deletions

View File

@ -154,6 +154,14 @@ impl Direction {
}
}
/// Returns the direction opposite `self`.
pub fn opposite(self) -> Self {
match self {
Direction::Abs(abs) => Direction::Abs(abs.opposite()),
Direction::Rel(rel) => Direction::Rel(rel.swap()),
}
}
/// Shortcut to create `Direction::Rel(Relative::Back)`
pub fn back() -> Self {
Direction::Rel(Relative::Back)
@ -191,7 +199,7 @@ impl Direction {
}
/// Direction relative to an orientation.
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Relative {
// TODO: handle right-to-left? (Arabic, ...)
/// Front relative direction.
@ -217,6 +225,39 @@ impl Relative {
(Orientation::Vertical, Relative::Back) => Absolute::Down,
}
}
/// Picks one of the two values in a tuple.
///
/// First one is `self` is `Front`, second one if `self` is `Back`.
pub fn pick<T>(self, (front, back): (T, T)) -> T {
match self {
Relative::Front => front,
Relative::Back => back,
}
}
/// Returns the other relative direction.
pub fn swap(self) -> Self {
match self {
Relative::Front => Relative::Back,
Relative::Back => Relative::Front,
}
}
/// Returns the relative position of `a` to `b`.
///
/// If `a < b`, it would be `Front`.
/// If `a > b`, it would be `Back`.
/// If `a == b`, returns `None`.
pub fn a_to_b(a: usize, b: usize) -> Option<Self> {
use std::cmp::Ordering;
match a.cmp(&b) {
Ordering::Less => Some(Relative::Front),
Ordering::Greater => Some(Relative::Back),
Ordering::Equal => None,
}
}
}
/// Absolute direction (up, down, left, right).
@ -251,4 +292,29 @@ impl Absolute {
_ => None,
}
}
/// Returns the direction opposite `self`.
pub fn opposite(self) -> Self {
match self {
Absolute::Left => Absolute::Right,
Absolute::Right => Absolute::Left,
Absolute::Up => Absolute::Down,
Absolute::Down => Absolute::Up,
Absolute::None => Absolute::None,
}
}
/// Splits this absolute direction into an orientation and relative direction.
///
/// For example, `Right` will give `(Horizontal, Back)`.
pub fn split(self) -> (Orientation, Relative) {
match self {
Absolute::Left => (Orientation::Horizontal, Relative::Front),
Absolute::Right => (Orientation::Horizontal, Relative::Back),
Absolute::Up => (Orientation::Vertical, Relative::Front),
Absolute::Down => (Orientation::Vertical, Relative::Back),
// TODO: Remove `Absolute::None`
Absolute::None => panic!("None direction not supported here"),
}
}
}

View File

@ -522,6 +522,8 @@ pub enum Event {
// Having a doc-hidden event prevents people from having exhaustive
// matches, allowing us to add events in the future.
//
// In addition we may not want people to listen to the exit event?
#[doc(hidden)]
/// The application is about to exit.
Exit,

View File

@ -2,12 +2,14 @@
use crate::backend::Backend;
use crate::direction::Orientation;
use crate::rect::Rect;
use crate::theme::{
BorderStyle, ColorStyle, Effect, PaletteColor, Style, Theme,
};
use crate::utils::lines::simple::{prefix, suffix};
use crate::with::With;
use crate::Vec2;
use enumset::EnumSet;
use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation;
@ -541,6 +543,13 @@ impl<'a, 'b> Printer<'a, 'b> {
self.clone().with(|s| s.enabled &= enabled)
}
/// Returns a new sub-printer for the given viewport.
///
/// This is a combination of offset + cropped.
pub fn windowed(&self, viewport: Rect) -> Self {
self.offset(viewport.top_left()).cropped(viewport.size())
}
/// Returns a new sub-printer with a cropped area.
///
/// The new printer size will be the minimum of `size` and its current size.
@ -601,6 +610,9 @@ impl<'a, 'b> Printer<'a, 'b> {
}
/// Returns a new sub-printer with a content offset.
///
/// This is useful for parent views that only show a subset of their
/// child, like `ScrollView`.
pub fn content_offset<S>(&self, offset: S) -> Self
where
S: Into<Vec2>,

View File

@ -1,5 +1,7 @@
//! Rectangles on the 2D character grid.
use crate::direction::{Absolute, Orientation};
use crate::Vec2;
use std::ops::Add;
/// A non-empty rectangle on the 2D grid.
@ -7,6 +9,7 @@ use std::ops::Add;
pub struct Rect {
/// Top-left corner, inclusive
top_left: Vec2,
/// Bottom-right corner, inclusive
bottom_right: Vec2,
}
@ -90,6 +93,30 @@ impl Rect {
self
}
/// Returns the start and end coordinate of one side of this rectangle.
///
/// Both start and end are inclusive.
pub fn side(self, orientation: Orientation) -> (usize, usize) {
match orientation {
Orientation::Vertical => (self.top(), self.bottom()),
Orientation::Horizontal => (self.left(), self.right()),
}
}
/// Returns the coordinate of the given edge.
///
/// All edges are inclusive.
pub fn edge(self, side: Absolute) -> usize {
match side {
Absolute::Left => self.left(),
Absolute::Right => self.right(),
Absolute::Up => self.top(),
Absolute::Down => self.bottom(),
// TODO: Remove `None` from `Absolute` enum
Absolute::None => panic!("None is not a valid edge."),
}
}
/// Adds the given offset to this rectangle.
pub fn offset<V>(&mut self, offset: V)
where

View File

@ -87,6 +87,21 @@ impl XY<usize> {
})
}
/// Checked addition with a signed vec.
///
/// Will return `None` if any coordinates exceeds bounds.
pub fn checked_add<O: Into<XY<isize>>>(&self, other: O) -> Option<Self> {
let other = other.into();
self.zip_map(other, |s, o| {
if o > 0 {
s.checked_add(o as usize)
} else {
s.checked_sub((-o) as usize)
}
})
.both()
}
/// Term-by-term integer division that rounds up.
///
/// # Examples

View File

@ -83,9 +83,7 @@ pub trait View: Any + AnyView {
/// View groups should implement this to forward the call to each children.
///
/// Default implementation is a no-op.
fn call_on_any<'a>(&mut self, _: &Selector<'_>, _: AnyCb<'a>) {
// TODO: FnMut -> FnOnce once it works
}
fn call_on_any<'a>(&mut self, _: &Selector<'_>, _: AnyCb<'a>) {}
/// Moves the focus to the view identified by the given selector.
///
@ -99,12 +97,10 @@ pub trait View: Any + AnyView {
/// This view is offered focus. Will it take it?
///
/// `source` indicates where the focus comes from.
/// When the source is unclear, `Front` is usually used.
/// When the source is unclear (for example mouse events),
/// `Direction::none()` can be used.
///
/// Default implementation always return `false`.
///
/// If the source is `Direction::Abs(Absolute::None)`, it is _recommended_
/// not to change the current focus selection.
fn take_focus(&mut self, source: Direction) -> bool {
let _ = source;
false
@ -125,6 +121,8 @@ pub trait View: Any + AnyView {
/// Returns the type of this view.
///
/// Useful when you have a `&dyn View`.
///
/// View implementation don't usually have to override this.
fn type_name(&self) -> &'static str {
std::any::type_name::<Self>()
}

View File

@ -0,0 +1,269 @@
use crate::direction::{Absolute, Direction, Relative};
use crate::event::{Event, EventResult, Key};
use crate::rect::Rect;
use crate::view::IntoBoxedView;
use crate::{Printer, Vec2, View, With};
/// Arranges its children in a fixed layout.
///
/// Usually meant to use an external layout engine.
pub struct FixedLayout {
children: Vec<Child>,
focus: usize,
}
struct Child {
view: Box<dyn View>,
position: Rect,
}
new_default!(FixedLayout);
impl FixedLayout {
/// Returns a new, empty `FixedLayout`.
pub fn new() -> Self {
FixedLayout {
children: Vec::new(),
focus: 0,
}
}
/// Adds a child. Chainable variant.
pub fn child<V: IntoBoxedView>(self, position: Rect, view: V) -> Self {
self.with(|s| s.add_child(position, view))
}
/// Adds a child.
pub fn add_child<V: IntoBoxedView>(&mut self, position: Rect, view: V) {
self.children.push(Child {
view: view.as_boxed_view(),
position,
});
}
/// Returns index of focused inner view
pub fn get_focus_index(&self) -> usize {
self.focus
}
/// Attemps to set the focus on the given child.
///
/// Returns `Err(())` if `index >= self.len()`, or if the view at the
/// given index does not accept focus.
pub fn set_focus_index(&mut self, index: usize) -> Result<(), ()> {
if self
.children
.get_mut(index)
.map(|child| child.view.take_focus(Direction::none()))
.unwrap_or(false)
{
self.focus = index;
Ok(())
} else {
Err(())
}
}
/// How many children are in this view.
pub fn len(&self) -> usize {
self.children.len()
}
/// Returns `true` if this view has no children.
pub fn is_empty(&self) -> bool {
self.children.is_empty()
}
/// Returns a reference to a child.
pub fn get_child(&self, i: usize) -> Option<&dyn View> {
self.children.get(i).map(|c| &*c.view)
}
/// Returns a mutable reference to a child.
pub fn get_child_mut(&mut self, i: usize) -> Option<&mut dyn View> {
self.children.get_mut(i).map(|c| &mut *c.view)
}
/// Sets the position for the given child.
pub fn set_child_position(&mut self, i: usize, position: Rect) {
self.children[i].position = position;
}
/// Removes a child.
///
/// If `i` is within bounds, the removed child will be returned.
pub fn remove_child(&mut self, i: usize) -> Option<Box<dyn View>> {
if i >= self.len() {
return None;
}
if self.focus > i
|| (self.focus != 0 && self.focus == self.children.len() - 1)
{
self.focus -= 1;
}
Some(self.children.remove(i).view)
}
fn iter_mut<'a>(
source: Direction,
children: &'a mut [Child],
) -> Box<dyn Iterator<Item = (usize, &mut Child)> + 'a> {
let children = children.iter_mut().enumerate();
match source {
Direction::Rel(Relative::Front) => Box::new(children),
Direction::Rel(Relative::Back) => Box::new(children.rev()),
Direction::Abs(abs) => {
// Sort children by the given direction
let mut children: Vec<_> = children.collect();
children.sort_by_key(|(_, c)| c.position.edge(abs));
Box::new(children.into_iter())
}
}
}
fn move_focus(&mut self, target: Absolute) -> EventResult {
let source = Direction::Abs(target.opposite());
let (orientation, rel) = target.split();
fn intersects(a: (usize, usize), b: (usize, usize)) -> bool {
a.1 >= b.0 && a.0 <= b.1
}
let current_position = self.children[self.focus].position;
let current_side = current_position.side(orientation.swap());
let current_edge = current_position.edge(target);
let children =
Self::iter_mut(source, &mut self.children).filter(|(_, c)| {
// Only select children actually aligned with us
Some(rel)
== Relative::a_to_b(current_edge, c.position.edge(target))
&& intersects(
c.position.side(orientation.swap()),
current_side,
)
});
for (i, c) in children {
if c.view.take_focus(source) {
self.focus = i;
return EventResult::Consumed(None);
}
}
EventResult::Ignored
}
fn check_focus_grab(&mut self, event: &Event) {
if let Event::Mouse {
offset,
position,
event,
} = *event
{
if !event.grabs_focus() {
return;
}
let position = match position.checked_sub(offset) {
None => return,
Some(pos) => pos,
};
for (i, child) in self.children.iter_mut().enumerate() {
if child.position.contains(position)
&& child.view.take_focus(Direction::none())
{
self.focus = i;
}
}
}
}
}
impl View for FixedLayout {
fn draw(&self, printer: &Printer) {
for child in &self.children {
child.view.draw(&printer.windowed(child.position));
}
}
fn layout(&mut self, _size: Vec2) {
// TODO: re-compute children positions?
for child in &mut self.children {
child.view.layout(child.position.size());
}
}
fn on_event(&mut self, event: Event) -> EventResult {
if self.is_empty() {
return EventResult::Ignored;
}
self.check_focus_grab(&event);
let child = &mut self.children[self.focus];
let result = child
.view
.on_event(event.relativized(child.position.top_left()));
match result {
EventResult::Ignored => match event {
Event::Key(Key::Tab) => unimplemented!(),
Event::Key(Key::Left) => self.move_focus(Absolute::Left),
Event::Key(Key::Right) => self.move_focus(Absolute::Right),
Event::Key(Key::Up) => self.move_focus(Absolute::Up),
Event::Key(Key::Down) => self.move_focus(Absolute::Down),
_ => EventResult::Ignored,
},
res => res,
}
}
fn important_area(&self, size: Vec2) -> Rect {
if self.is_empty() {
return Rect::from_size((0, 0), size);
}
let child = &self.children[self.focus];
child.view.important_area(child.position.size())
+ child.position.top_left()
}
fn required_size(&mut self, _constraint: Vec2) -> Vec2 {
self.children
.iter()
.map(|c| c.position.bottom_left() + (1, 1))
.fold(Vec2::zero(), Vec2::max)
}
fn take_focus(&mut self, source: Direction) -> bool {
// TODO: what if source = None?
match source {
Direction::Abs(Absolute::None) => {
// For now, take focus if any view is focusable.
for child in &mut self.children {
if child.view.take_focus(source) {
return true;
}
}
false
}
source => {
for (i, c) in Self::iter_mut(source, &mut self.children) {
if c.view.take_focus(source) {
self.focus = i;
return true;
}
}
false
}
}
}
}

View File

@ -319,7 +319,7 @@ impl LinearLayout {
.any(View::needs_relayout)
}
/// Returns a cyclic mutable iterator starting with the child in focus
/// Returns a mutable iterator starting with the child in focus
fn iter_mut<'a>(
&'a mut self,
from_focus: bool,

View File

@ -69,6 +69,7 @@ mod dialog;
mod dummy;
mod edit_view;
mod enableable_view;
mod fixed_layout;
mod hideable_view;
mod last_size_view;
mod layer;
@ -103,6 +104,7 @@ pub use self::dialog::{Dialog, DialogFocus};
pub use self::dummy::DummyView;
pub use self::edit_view::EditView;
pub use self::enableable_view::EnableableView;
pub use self::fixed_layout::FixedLayout;
pub use self::hideable_view::HideableView;
pub use self::last_size_view::LastSizeView;
pub use self::layer::Layer;

View File

@ -437,6 +437,25 @@ impl<T> XY<Option<T>> {
pub fn unwrap_or(self, other: XY<T>) -> XY<T> {
self.zip_map(other, Option::unwrap_or)
}
/// Returns a new `XY` if both components are present in `self`.
///
/// # Examples
///
/// ```rust
/// # use cursive_core::XY;
/// assert_eq!(XY::new(Some(1), None).both(), None);
/// assert_eq!(XY::new(Some(1), Some(2)).both(), Some(XY::new(1, 2)));
/// ```
pub fn both(self) -> Option<XY<T>> {
match self {
XY {
x: Some(x),
y: Some(y),
} => Some(XY::new(x, y)),
_ => None,
}
}
}
impl XY<bool> {

View File

@ -0,0 +1,15 @@
fn main() {
let mut siv = cursive::default();
siv.add_layer(
cursive::views::Dialog::around(
cursive::views::FixedLayout::new().child(
cursive::Rect::from_size((0, 0), (10, 1)),
cursive::views::TextView::new("abc"),
),
)
.button("Quit", |s| s.quit()),
);
siv.run();
}