use std::collections::HashMap;
use ribir_geom::ZERO_SIZE;
use super::{Lerp, WidgetId, WidgetTree};
use crate::prelude::{INFINITY_SIZE, Point, Size};
#[derive(Debug, Clone, PartialEq, Copy, Lerp)]
pub struct BoxClamp {
pub min: Size,
pub max: Size,
}
impl BoxClamp {
pub const EXPAND_X: BoxClamp =
BoxClamp { min: Size::new(f32::INFINITY, 0.), max: Size::new(f32::INFINITY, f32::INFINITY) };
pub const EXPAND_Y: BoxClamp =
BoxClamp { min: Size::new(0., f32::INFINITY), max: Size::new(f32::INFINITY, f32::INFINITY) };
pub const EXPAND_BOTH: BoxClamp = BoxClamp {
min: Size::new(f32::INFINITY, f32::INFINITY),
max: Size::new(f32::INFINITY, f32::INFINITY),
};
pub const fn fixed_width(width: f32) -> Self {
BoxClamp { min: Size::new(width, 0.), max: Size::new(width, f32::INFINITY) }
}
pub const fn fixed_height(height: f32) -> Self {
BoxClamp { min: Size::new(0., height), max: Size::new(f32::INFINITY, height) }
}
pub const fn fixed_size(size: Size) -> Self { BoxClamp { min: size, max: size } }
pub const fn min_width(width: f32) -> Self {
let mut clamp = BoxClamp::EXPAND_BOTH;
clamp.min.width = width;
clamp
}
pub const fn min_height(height: f32) -> Self {
let mut clamp = BoxClamp::EXPAND_BOTH;
clamp.min.height = height;
clamp
}
pub fn with_fixed_height(mut self, height: f32) -> Self {
self.min.height = height;
self.max.height = height;
self
}
pub fn with_fixed_width(mut self, width: f32) -> Self {
self.min.width = width;
self.max.width = width;
self
}
}
#[derive(Debug, Default, Clone)]
pub struct LayoutInfo {
pub clamp: BoxClamp,
pub size: Option<Size>,
pub pos: Point,
}
#[derive(Default)]
pub(crate) struct LayoutStore {
data: HashMap<WidgetId, LayoutInfo, ahash::RandomState>,
}
impl LayoutStore {
pub(crate) fn force_layout(&mut self, id: WidgetId) -> Option<LayoutInfo> { self.remove(id) }
pub(crate) fn remove(&mut self, id: WidgetId) -> Option<LayoutInfo> { self.data.remove(&id) }
pub(crate) fn layout_box_size(&self, id: WidgetId) -> Option<Size> {
self.layout_info(id).and_then(|info| info.size)
}
pub(crate) fn layout_box_pos(&self, id: WidgetId) -> Option<Point> {
self.layout_info(id).map(|info| info.pos)
}
pub(crate) fn layout_info(&self, id: WidgetId) -> Option<&LayoutInfo> { self.data.get(&id) }
pub(crate) fn layout_info_or_default(&mut self, id: WidgetId) -> &mut LayoutInfo {
self.data.entry(id).or_default()
}
}
impl WidgetTree {
pub(crate) fn map_to_parent(&self, id: WidgetId, pos: Point) -> Point {
self
.store
.layout_box_pos(id)
.map_or(pos, |offset| {
let pos = id
.assert_get(self)
.get_transform()
.map_or(pos, |t| t.transform_point(pos));
pos + offset.to_vector()
})
}
pub(crate) fn map_from_parent(&self, id: WidgetId, pos: Point) -> Point {
self
.store
.layout_box_pos(id)
.map_or(pos, |offset| {
let pos = pos - offset.to_vector();
id.assert_get(self)
.get_transform()
.map_or(pos, |t| {
t.inverse()
.map_or(pos, |t| t.transform_point(pos))
})
})
}
pub(crate) fn map_to_global(&self, pos: Point, widget: WidgetId) -> Point {
widget
.ancestors(self)
.fold(pos, |pos, p| self.map_to_parent(p, pos))
}
pub(crate) fn map_from_global(&self, pos: Point, widget: WidgetId) -> Point {
let stack = widget.ancestors(self).collect::<Vec<_>>();
stack
.iter()
.rev()
.fold(pos, |pos, p| self.map_from_parent(*p, pos))
}
}
impl BoxClamp {
#[inline]
pub fn clamp(self, size: Size) -> Size { size.clamp(self.min, self.max) }
#[inline]
pub fn expand(mut self) -> Self {
self.max = INFINITY_SIZE;
self
}
#[inline]
pub fn loose(mut self) -> Self {
self.min = ZERO_SIZE;
self
}
}
impl Default for BoxClamp {
fn default() -> Self {
Self { min: Size::new(0., 0.), max: Size::new(f32::INFINITY, f32::INFINITY) }
}
}
impl std::ops::Deref for LayoutStore {
type Target = HashMap<WidgetId, LayoutInfo, ahash::RandomState>;
fn deref(&self) -> &Self::Target { &self.data }
}
impl std::ops::DerefMut for LayoutStore {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.data }
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use super::*;
use crate::{prelude::*, reset_test_env, test_helper::*};
#[derive(Declare, Clone, SingleChild)]
struct OffsetBox {
pub offset: Point,
pub size: Size,
}
impl Render for OffsetBox {
fn perform_layout(&self, mut clamp: BoxClamp, ctx: &mut LayoutCtx) -> Size {
clamp.max = clamp.max.min(self.size);
let child = ctx.assert_single_child();
ctx.perform_child_layout(child, clamp);
ctx.update_position(child, self.offset);
self.size
}
#[inline]
fn only_sized_by_parent(&self) -> bool { true }
#[inline]
fn paint(&self, _: &mut PaintingCtx) {}
}
#[test]
fn fix_incorrect_relayout_root() {
reset_test_env!();
let child_box = Stateful::new(MockBox { size: Size::zero() });
let c_child_box = child_box.clone_writer();
let (layout_cnt, w_layout_cnt) = split_value(0);
let w = fn_widget! {
let child_box = child_box.clone_writer();
@MockMulti {
on_performed_layout: move |_| *$w_layout_cnt.write() += 1,
@ {
pipe!($child_box.size.is_empty())
.map(move|b| if b {
MockBox { size: Size::new(1., 1.) }.into_widget()
} else {
child_box.clone_writer().into_widget()
})
}
}
};
let mut wnd = TestWindow::new(w);
wnd.draw_frame();
assert_eq!(*layout_cnt.read(), 1);
{
c_child_box.write().size = Size::new(2., 2.);
}
wnd.draw_frame();
assert_eq!(*layout_cnt.read(), 2);
}
#[test]
fn layout_list_from_root_to_leaf() {
reset_test_env!();
let layout_order = Stateful::new(vec![]);
let trigger = Stateful::new(Size::zero());
let order = layout_order.clone_writer();
let size = trigger.clone_watcher();
let w = fn_widget! {
@MockBox {
size: pipe!(*$size),
on_performed_layout: move |_| $order.write().push(1),
@MockBox {
size: pipe!(*$size),
on_performed_layout: move |_| $order.write().push(2),
@MockBox {
size: pipe!(*$size),
on_performed_layout: move |_| $order.write().push(3),
}
}
}
};
let mut wnd = TestWindow::new(w);
wnd.draw_frame();
assert_eq!([3, 2, 1], &**layout_order.read());
{
*trigger.write() = Size::new(1., 1.);
}
wnd.draw_frame();
assert_eq!([3, 2, 1, 3, 2, 1], &**layout_order.read());
}
#[test]
fn relayout_size() {
reset_test_env!();
let trigger = Stateful::new(Size::zero());
let size = trigger.clone_watcher();
let w = fn_widget! {
@OffsetBox {
size: Size::new(100., 100.),
offset: Point::new(50., 50.),
@MockBox {
size: Size::new(50., 50.),
@MockBox { size: pipe!(*$size) }
}
}
};
#[track_caller]
fn assert_rect_by_path(wnd: &TestWindow, path: &[usize], rect: Rect) {
let info = wnd.layout_info_by_path(path).unwrap();
assert_eq!(info.pos, rect.origin);
assert_eq!(info.size.unwrap(), rect.size);
}
let mut wnd = TestWindow::new(w);
wnd.draw_frame();
assert_rect_by_path(&wnd, &[0, 0], ribir_geom::rect(50., 50., 50., 50.));
assert_rect_by_path(&wnd, &[0, 0, 0], ribir_geom::rect(0., 0., 0., 0.));
{
*trigger.write() = Size::new(10., 10.);
}
wnd.draw_frame();
assert_rect_by_path(&wnd, &[0, 0], ribir_geom::rect(50., 50., 50., 50.));
assert_rect_by_path(&wnd, &[0, 0, 0], ribir_geom::rect(0., 0., 10., 10.));
}
#[test]
fn relayout_from_parent() {
reset_test_env!();
let (cnt, w_cnt) = split_value(0);
let (size, w_size) = split_value(Size::zero());
let w = fn_widget! {
@MockBox {
size: Size::new(50., 50.),
on_performed_layout: move |_| *$w_cnt.write() += 1,
@MockBox { size: pipe!(*$size) }
}
};
let mut wnd = TestWindow::new(w);
wnd.draw_frame();
assert_eq!(*cnt.read(), 1);
*w_size.write() = Size::new(10., 10.);
wnd.draw_frame();
assert_eq!(*cnt.read(), 2);
}
#[test]
fn layout_visit_prev_position() {
reset_test_env!();
#[derive(Declare)]
struct MockWidget {
pos: Cell<Point>,
size: Size,
}
impl Render for MockWidget {
fn perform_layout(&self, _: BoxClamp, ctx: &mut LayoutCtx) -> Size {
self.pos.set(ctx.box_pos().unwrap_or_default());
self.size
}
#[inline]
fn only_sized_by_parent(&self) -> bool { true }
#[inline]
fn paint(&self, _: &mut PaintingCtx) {}
}
let (pos, w_pos) = split_value(Point::zero());
let (size, w_size) = split_value(Size::zero());
let w = fn_widget! {
let w = @MockWidget {
size: pipe!(*$size),
pos: Cell::new(Point::zero()),
};
@MockMulti {
@MockBox{ size: Size::new(50., 50.) }
@$w {
on_performed_layout: move |_| *$w_pos.write() = $w.pos.get()
}
}
};
let mut wnd = TestWindow::new(w);
wnd.draw_frame();
*w_size.write() = Size::new(1., 1.);
wnd.draw_frame();
assert_eq!(*pos.read(), Point::new(50., 0.));
}
}