Skip to content

Commit

Permalink
Merge pull request #431 from kas-gui/work2
Browse files Browse the repository at this point in the history
Add Collection trait
  • Loading branch information
dhardy authored Dec 20, 2023
2 parents 137e11d + 90fbfbd commit 82fde11
Show file tree
Hide file tree
Showing 17 changed files with 869 additions and 452 deletions.
196 changes: 196 additions & 0 deletions crates/kas-core/src/core/collection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License in the LICENSE-APACHE file or at:
// https://www.apache.org/licenses/LICENSE-2.0

//! The [`Collection`] trait
use crate::{Layout, Node, Widget};
use std::ops::RangeBounds;

/// A collection of (child) widgets
///
/// Essentially, implementating types are lists of widgets. Simple examples are
/// `Vec<W>` and `[W; N]` where `W: Widget` and `const N: usize`. A more complex
/// example would be a custom struct where each field is a widget.
pub trait Collection {
/// The associated data type
type Data;

/// True if the collection is empty
fn is_empty(&self) -> bool {
self.len() == 0
}

/// The number of widgets
fn len(&self) -> usize;

/// Get a widget as a [`Layout`]
fn get_layout(&self, index: usize) -> Option<&dyn Layout>;

/// Get a widget as a mutable [`Layout`]
fn get_mut_layout(&mut self, index: usize) -> Option<&mut dyn Layout>;

/// Operate on a widget as a [`Node`]
fn for_node(
&mut self,
data: &Self::Data,
index: usize,
closure: Box<dyn FnOnce(Node<'_>) + '_>,
);

/// Iterate over elements as [`Layout`] items within `range`
///
/// Note: there is currently no mutable equivalent due to the streaming
/// iterator problem.
fn iter_layout(&self, range: impl RangeBounds<usize>) -> CollectionIterLayout<'_, Self> {
use std::ops::Bound::{Excluded, Included, Unbounded};
let start = match range.start_bound() {
Included(start) => *start,
Excluded(start) => *start + 1,
Unbounded => 0,
};
let end = match range.end_bound() {
Included(end) => *end + 1,
Excluded(end) => *end,
Unbounded => self.len(),
};
CollectionIterLayout {
start,
end,
collection: self,
}
}

/// Binary searches this collection with a comparator function.
///
/// Similar to [`slice::binary_search_by`][<[()]>::binary_search_by], the
/// comparator function should return whether the element is `Less` than,
/// `Equal` to, or `Greater` than the desired target, and the collection
/// should be sorted by this comparator (if not, the result is meaningless).
///
/// Returns:
///
/// - `Some(Ok(index))` if an `Equal` element is found at `index`
/// - `Some(Err(index))` if no `Equal` element is found; in this case such
/// an element could be inserted at `index`
/// - `None` if [`Collection::get_layout`] returns `None` for some
/// `index` less than [`Collection::len`]. This is an error case that
/// should not occur.
fn binary_search_by<'a, F>(&'a self, mut f: F) -> Option<Result<usize, usize>>
where
F: FnMut(&'a dyn Layout) -> std::cmp::Ordering,
{
use std::cmp::Ordering::{Greater, Less};

// INVARIANTS:
// - 0 <= left <= left + size = right <= self.len()
// - f returns Less for everything in self[..left]
// - f returns Greater for everything in self[right..]
let mut size = self.len();
let mut left = 0;
let mut right = size;
while left < right {
let mid = left + size / 2;

let cmp = f(self.get_layout(mid)?);

if cmp == Less {
left = mid + 1;
} else if cmp == Greater {
right = mid;
} else {
return Some(Ok(mid));
}

size = right - left;
}

Some(Err(left))
}
}

/// An iterator over a [`Collection`] as [`Layout`] elements
pub struct CollectionIterLayout<'a, C: Collection + ?Sized> {
start: usize,
end: usize,
collection: &'a C,
}

impl<'a, C: Collection + ?Sized> Iterator for CollectionIterLayout<'a, C> {
type Item = &'a dyn Layout;

fn next(&mut self) -> Option<Self::Item> {
let index = self.start;
if index < self.end {
self.start += 1;
self.collection.get_layout(index)
} else {
None
}
}
}

impl<'a, C: Collection + ?Sized> DoubleEndedIterator for CollectionIterLayout<'a, C> {
fn next_back(&mut self) -> Option<Self::Item> {
if self.start < self.end {
let index = self.end - 1;
self.end = index;
self.collection.get_layout(index)
} else {
None
}
}
}

impl<'a, C: Collection + ?Sized> ExactSizeIterator for CollectionIterLayout<'a, C> {}

macro_rules! impl_slice {
(($($gg:tt)*) for $t:ty) => {
impl<$($gg)*> Collection for $t {
type Data = W::Data;

#[inline]
fn len(&self) -> usize {
<[W]>::len(self)
}

#[inline]
fn get_layout(&self, index: usize) -> Option<&dyn Layout> {
self.get(index).map(|w| w as &dyn Layout)
}

#[inline]
fn get_mut_layout(&mut self, index: usize) -> Option<&mut dyn Layout> {
self.get_mut(index).map(|w| w as &mut dyn Layout)
}

#[inline]
fn for_node(
&mut self,
data: &W::Data,
index: usize,
closure: Box<dyn FnOnce(Node<'_>) + '_>,
) {
if let Some(w) = self.get_mut(index) {
closure(w.as_node(data));
}
}

#[inline]
fn binary_search_by<'a, F>(&'a self, mut f: F) -> Option<Result<usize, usize>>
where
F: FnMut(&'a dyn Layout) -> std::cmp::Ordering,
{
Some(<[W]>::binary_search_by(self, move |w| f(w.as_layout())))
}
}
};
}

// NOTE: If Rust had better lifetime analysis we could replace
// the following impls with a single one:
// impl<W: Widget, T: std::ops::Deref<Target = [W]> + ?Sized> Collection for T
impl_slice!((const N: usize, W: Widget) for [W; N]);
impl_slice!((W: Widget) for [W]);
impl_slice!((W: Widget) for Vec<W>);
2 changes: 2 additions & 0 deletions crates/kas-core/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

//! Core widget types
mod collection;
mod data;
mod layout;
mod node;
Expand All @@ -16,6 +17,7 @@ mod widget_id;
#[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
pub mod impls;

pub use collection::Collection;
pub use data::*;
pub use layout::*;
pub use node::Node;
Expand Down
107 changes: 65 additions & 42 deletions crates/kas-core/src/layout/row_solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use super::{AxisInfo, SizeRules};
use super::{RowStorage, RowTemp, RulesSetter, RulesSolver};
use crate::dir::{Direction, Directional};
use crate::geom::{Coord, Rect};
use crate::Layout;
use crate::{Collection, Layout};

/// A [`RulesSolver`] for rows (and, without loss of generality, for columns).
///
Expand Down Expand Up @@ -241,9 +241,7 @@ impl<D: Directional, T: RowTemp, S: RowStorage> RulesSetter for RowSetter<D, T,
/// Allows efficient implementations of `draw` / event handlers based on the
/// layout representation.
///
/// This is only applicable where child widgets are contained in a slice of type
/// `W: Layout` (which may be `Box<dyn Widget>`). In other cases, the naive
/// implementation (test all items) must be used.
/// This is only applicable where child widgets are contained in a [`Collection`].
#[derive(Clone, Copy, Debug)]
pub struct RowPositionSolver<D: Directional> {
direction: D,
Expand All @@ -255,10 +253,14 @@ impl<D: Directional> RowPositionSolver<D> {
RowPositionSolver { direction }
}

fn binary_search<W: Layout>(self, widgets: &[W], coord: Coord) -> Result<usize, usize> {
fn binary_search<C: Collection + ?Sized>(
self,
widgets: &C,
coord: Coord,
) -> Option<Result<usize, usize>> {
match self.direction.as_direction() {
Direction::Right => widgets.binary_search_by_key(&coord.0, |w| w.rect().pos.0),
Direction::Down => widgets.binary_search_by_key(&coord.1, |w| w.rect().pos.1),
Direction::Right => widgets.binary_search_by(|w| w.rect().pos.0.cmp(&coord.0)),
Direction::Down => widgets.binary_search_by(|w| w.rect().pos.1.cmp(&coord.1)),
Direction::Left => widgets.binary_search_by(|w| w.rect().pos.0.cmp(&coord.0).reverse()),
Direction::Up => widgets.binary_search_by(|w| w.rect().pos.1.cmp(&coord.1).reverse()),
}
Expand All @@ -268,18 +270,24 @@ impl<D: Directional> RowPositionSolver<D> {
///
/// Returns `None` when the coordinates lie within the margin area or
/// outside of the parent widget.
pub fn find_child_index<W: Layout>(self, widgets: &[W], coord: Coord) -> Option<usize> {
match self.binary_search(widgets, coord) {
/// Also returns `None` if [`Collection::get_layout`] returns `None` for
/// some index less than `len` (a theoretical but unexpected error case).
pub fn find_child_index<C: Collection + ?Sized>(
self,
widgets: &C,
coord: Coord,
) -> Option<usize> {
match self.binary_search(widgets, coord)? {
Ok(i) => Some(i),
Err(i) if self.direction.is_reversed() => {
if i == widgets.len() || !widgets[i].rect().contains(coord) {
if i == widgets.len() || !widgets.get_layout(i)?.rect().contains(coord) {
None
} else {
Some(i)
}
}
Err(i) => {
if i == 0 || !widgets[i - 1].rect().contains(coord) {
if i == 0 || !widgets.get_layout(i - 1)?.rect().contains(coord) {
None
} else {
Some(i - 1)
Expand All @@ -292,25 +300,38 @@ impl<D: Directional> RowPositionSolver<D> {
///
/// Returns `None` when the coordinates lie within the margin area or
/// outside of the parent widget.
/// Also returns `None` if [`Collection::get_layout`] returns `None` for
/// some index less than `len` (a theoretical but unexpected error case).
#[inline]
pub fn find_child<W: Layout>(self, widgets: &[W], coord: Coord) -> Option<&W> {
self.find_child_index(widgets, coord).map(|i| &widgets[i])
pub fn find_child<C: Collection + ?Sized>(
self,
widgets: &C,
coord: Coord,
) -> Option<&dyn Layout> {
self.find_child_index(widgets, coord)
.and_then(|i| widgets.get_layout(i))
}

/// Find the child containing the given coordinates
///
/// Returns `None` when the coordinates lie within the margin area or
/// outside of the parent widget.
/// Also returns `None` if [`Collection::get_layout`] returns `None` for
/// some index less than `len` (a theoretical but unexpected error case).
#[inline]
pub fn find_child_mut<W: Layout>(self, widgets: &mut [W], coord: Coord) -> Option<&mut W> {
pub fn find_child_mut<C: Collection + ?Sized>(
self,
widgets: &mut C,
coord: Coord,
) -> Option<&mut dyn Layout> {
self.find_child_index(widgets, coord)
.map(|i| &mut widgets[i])
.and_then(|i| widgets.get_mut_layout(i))
}

/// Call `f` on each child intersecting the given `rect`
pub fn for_children<W: Layout, F: FnMut(&mut W)>(
pub fn for_children_mut<C: Collection + ?Sized, F: FnMut(&mut dyn Layout)>(
self,
widgets: &mut [W],
widgets: &mut C,
rect: Rect,
mut f: F,
) {
Expand All @@ -319,36 +340,38 @@ impl<D: Directional> RowPositionSolver<D> {
true => (rect.pos2(), rect.pos),
};
let start = match self.binary_search(widgets, pos) {
Ok(i) => i,
Err(i) if i > 0 => {
let j = i - 1;
let rect = widgets[j].rect();
let cond = match self.direction.as_direction() {
Direction::Right => pos.0 < rect.pos2().0,
Direction::Down => pos.1 < rect.pos2().1,
Direction::Left => rect.pos.0 <= pos.0,
Direction::Up => rect.pos.1 <= pos.1,
};
if cond {
j
} else {
i
Some(Ok(i)) => i,
Some(Err(i)) if i > 0 => {
let mut j = i - 1;
if let Some(rect) = widgets.get_layout(j).map(|l| l.rect()) {
let cond = match self.direction.as_direction() {
Direction::Right => pos.0 < rect.pos2().0,
Direction::Down => pos.1 < rect.pos2().1,
Direction::Left => rect.pos.0 <= pos.0,
Direction::Up => rect.pos.1 <= pos.1,
};
if !cond {
j = i;
}
}
j
}
Err(_) => 0,
_ => 0,
};

for child in widgets[start..].iter_mut() {
let do_break = match self.direction.as_direction() {
Direction::Right => child.rect().pos.0 >= end.0,
Direction::Down => child.rect().pos.1 >= end.1,
Direction::Left => child.rect().pos2().0 < end.0,
Direction::Up => child.rect().pos2().1 < end.1,
};
if do_break {
break;
for i in start..widgets.len() {
if let Some(child) = widgets.get_mut_layout(i) {
let do_break = match self.direction.as_direction() {
Direction::Right => child.rect().pos.0 >= end.0,
Direction::Down => child.rect().pos.1 >= end.1,
Direction::Left => child.rect().pos2().0 < end.0,
Direction::Up => child.rect().pos2().1 < end.1,
};
if do_break {
break;
}
f(child);
}
f(child);
}
}
}
Loading

0 comments on commit 82fde11

Please sign in to comment.