diff --git a/cursive-core/src/direction.rs b/cursive-core/src/direction.rs index 9e7230b..101d18f 100644 --- a/cursive-core/src/direction.rs +++ b/cursive-core/src/direction.rs @@ -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(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 { + 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"), + } + } } diff --git a/cursive-core/src/event.rs b/cursive-core/src/event.rs index 3c09a41..154e530 100644 --- a/cursive-core/src/event.rs +++ b/cursive-core/src/event.rs @@ -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, diff --git a/cursive-core/src/printer.rs b/cursive-core/src/printer.rs index 1ccce8c..23aa2ab 100644 --- a/cursive-core/src/printer.rs +++ b/cursive-core/src/printer.rs @@ -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(&self, offset: S) -> Self where S: Into, diff --git a/cursive-core/src/rect.rs b/cursive-core/src/rect.rs index f3ed61d..b310d3d 100644 --- a/cursive-core/src/rect.rs +++ b/cursive-core/src/rect.rs @@ -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(&mut self, offset: V) where diff --git a/cursive-core/src/vec.rs b/cursive-core/src/vec.rs index 01c3ff4..be03e68 100644 --- a/cursive-core/src/vec.rs +++ b/cursive-core/src/vec.rs @@ -87,6 +87,21 @@ impl XY { }) } + /// Checked addition with a signed vec. + /// + /// Will return `None` if any coordinates exceeds bounds. + pub fn checked_add>>(&self, other: O) -> Option { + 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 diff --git a/cursive-core/src/view/view_trait.rs b/cursive-core/src/view/view_trait.rs index f794781..50923b3 100644 --- a/cursive-core/src/view/view_trait.rs +++ b/cursive-core/src/view/view_trait.rs @@ -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::() } diff --git a/cursive-core/src/views/fixed_layout.rs b/cursive-core/src/views/fixed_layout.rs new file mode 100644 index 0000000..10a7334 --- /dev/null +++ b/cursive-core/src/views/fixed_layout.rs @@ -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, + focus: usize, +} + +struct Child { + view: Box, + 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(self, position: Rect, view: V) -> Self { + self.with(|s| s.add_child(position, view)) + } + + /// Adds a child. + pub fn add_child(&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> { + 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 + '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 + } + } + } +} diff --git a/cursive-core/src/views/linear_layout.rs b/cursive-core/src/views/linear_layout.rs index 4e1c031..8d4a7df 100644 --- a/cursive-core/src/views/linear_layout.rs +++ b/cursive-core/src/views/linear_layout.rs @@ -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, diff --git a/cursive-core/src/views/mod.rs b/cursive-core/src/views/mod.rs index 4a3bb27..4cdde9b 100644 --- a/cursive-core/src/views/mod.rs +++ b/cursive-core/src/views/mod.rs @@ -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; diff --git a/cursive-core/src/xy.rs b/cursive-core/src/xy.rs index 00abe0b..f19c7f9 100644 --- a/cursive-core/src/xy.rs +++ b/cursive-core/src/xy.rs @@ -437,6 +437,25 @@ impl XY> { pub fn unwrap_or(self, other: XY) -> XY { 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> { + match self { + XY { + x: Some(x), + y: Some(y), + } => Some(XY::new(x, y)), + _ => None, + } + } } impl XY { diff --git a/examples/src/bin/fixed_layout.rs b/examples/src/bin/fixed_layout.rs new file mode 100644 index 0000000..900d785 --- /dev/null +++ b/examples/src/bin/fixed_layout.rs @@ -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(); +}