use frust_core::{
BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent, LayoutCtx,
PaintCtx, PaintScene, SemanticsCtx, View, Widget, mark_focus_orphaned,
};
use kurbo::Size;
pub enum Either<L, R> {
Left(L),
Right(R),
}
pub fn either<L, R>(cond: bool, l: impl FnOnce() -> L, r: impl FnOnce() -> R) -> Either<L, R> {
if cond {
Either::Left(l())
} else {
Either::Right(r())
}
}
pub enum EitherWidget<L, R> {
Left(L),
Right(R),
}
impl<L: Widget, R: Widget> Widget for EitherWidget<L, R> {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
match self {
Self::Left(w) => w.layout(ctx, bc),
Self::Right(w) => w.layout(ctx, bc),
}
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
match self {
Self::Left(w) => w.paint(ctx, scene),
Self::Right(w) => w.paint(ctx, scene),
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
match self {
Self::Left(w) => w.event(ctx, event),
Self::Right(w) => w.event(ctx, event),
}
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
match self {
Self::Left(w) => w.semantics(ctx),
Self::Right(w) => w.semantics(ctx),
}
}
fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
match self {
Self::Left(w) => w.visit_children(visitor),
Self::Right(w) => w.visit_children(visitor),
}
}
fn type_name(&self) -> &'static str {
match self {
Self::Left(w) => w.type_name(),
Self::Right(w) => w.type_name(),
}
}
}
impl<State: 'static, L: View<State>, R: View<State>> View<State> for Either<L, R> {
type Element = EitherWidget<L::Element, R::Element>;
fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
match self {
Self::Left(v) => EitherWidget::Left(v.build(ctx)),
Self::Right(v) => EitherWidget::Right(v.build(ctx)),
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut Self::Element,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
match (self, prev, &mut *element) {
(Self::Left(v), Self::Left(p), EitherWidget::Left(w)) => v.rebuild(p, w, ctx),
(Self::Right(v), Self::Right(p), EitherWidget::Right(w)) => v.rebuild(p, w, ctx),
_ => {
prev.teardown(element, ctx);
ctx.mark_element_replaced();
if ctx.has_focus() {
mark_focus_orphaned();
}
*element = self.build(ctx);
ChangeFlags::LAYOUT | ChangeFlags::PAINT
}
}
}
fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
match (self, element) {
(Self::Left(v), EitherWidget::Left(w)) => v.teardown(w, ctx),
(Self::Right(v), EitherWidget::Right(w)) => v.teardown(w, ctx),
_ => {}
}
}
fn message(&self, element: &mut Self::Element, state: &mut State) {
match (self, element) {
(Self::Left(v), EitherWidget::Left(w)) => v.message(w, state),
(Self::Right(v), EitherWidget::Right(w)) => v.message(w, state),
_ => {}
}
}
}
macro_rules! either_of {
($(#[$meta:meta])* $arity:literal, $E:ident, $W:ident; $($V:ident),+ $(,)?) => {
$(#[$meta])*
#[doc = ""]
#[doc = concat!("A view that is one of ", $arity, " statically-typed arms. Arm swaps behave as for [`Either`]: the old arm is torn down, the new one built fresh, and `LAYOUT | PAINT` is reported. See the [module docs](self).")]
pub enum $E<$($V),+> {
$(
#[doc = concat!("Arm `", stringify!($V), "`.")]
$V($V),
)+
}
#[doc = concat!("Retained widget for [`", stringify!($E), "`]: the live arm's widget, delegated to for every pass.")]
pub enum $W<$($V),+> {
$(
#[doc = concat!("Arm `", stringify!($V), "`'s widget.")]
$V($V),
)+
}
impl<$($V: Widget),+> Widget for $W<$($V),+> {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
match self { $(Self::$V(w) => w.layout(ctx, bc),)+ }
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
match self { $(Self::$V(w) => w.paint(ctx, scene),)+ }
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
match self { $(Self::$V(w) => w.event(ctx, event),)+ }
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
match self { $(Self::$V(w) => w.semantics(ctx),)+ }
}
fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
match self { $(Self::$V(w) => w.visit_children(visitor),)+ }
}
fn type_name(&self) -> &'static str {
match self { $(Self::$V(w) => w.type_name(),)+ }
}
}
impl<State: 'static, $($V: View<State>),+> View<State> for $E<$($V),+> {
type Element = $W<$($V::Element),+>;
fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
match self { $(Self::$V(v) => $W::$V(v.build(ctx)),)+ }
}
fn rebuild(
&self,
prev: &Self,
element: &mut Self::Element,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
match (self, prev, &mut *element) {
$((Self::$V(v), Self::$V(p), $W::$V(w)) => v.rebuild(p, w, ctx),)+
_ => {
prev.teardown(element, ctx);
ctx.mark_element_replaced();
if ctx.has_focus() {
mark_focus_orphaned();
}
*element = self.build(ctx);
ChangeFlags::LAYOUT | ChangeFlags::PAINT
}
}
}
fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
match (self, element) {
$((Self::$V(v), $W::$V(w)) => v.teardown(w, ctx),)+
_ => {}
}
}
fn message(&self, element: &mut Self::Element, state: &mut State) {
match (self, element) {
$((Self::$V(v), $W::$V(w)) => v.message(w, state),)+
_ => {}
}
}
}
};
}
either_of! {
"three", EitherOf3, EitherOf3Widget; A, B, C
}
either_of! { "four", EitherOf4, EitherOf4Widget; A, B, C, D }
either_of! { "five", EitherOf5, EitherOf5Widget; A, B, C, D, E }
either_of! { "six", EitherOf6, EitherOf6Widget; A, B, C, D, E, F }
either_of! { "seven", EitherOf7, EitherOf7Widget; A, B, C, D, E, F, G }
either_of! { "eight", EitherOf8, EitherOf8Widget; A, B, C, D, E, F, G, H }
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use frust_core::accesskit::Role;
use frust_core::{DiscardScene, FrameTime, RenderRoot, SemanticsUpdate, take_focus_orphaned};
use frust_text::TextContext;
use super::*;
use crate::{ButtonView, CheckboxView, button, checkbox};
type Log = Rc<RefCell<Vec<String>>>;
struct Arm {
tag: &'static str,
size: Size,
log: Log,
}
struct ArmWidget {
tag: &'static str,
size: Size,
log: Log,
painted: u32,
}
impl Drop for ArmWidget {
fn drop(&mut self) {
self.log.borrow_mut().push(format!("drop {}", self.tag));
}
}
impl View<()> for Arm {
type Element = ArmWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> ArmWidget {
self.log.borrow_mut().push(format!("build {}", self.tag));
ArmWidget {
tag: self.tag,
size: self.size,
log: Rc::clone(&self.log),
painted: 0,
}
}
fn rebuild(
&self,
_prev: &Self,
el: &mut ArmWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
el.size = self.size;
ChangeFlags::NONE
}
fn teardown(&self, _el: &mut ArmWidget, _ctx: &mut BuildCtx<'_>) {
self.log.borrow_mut().push(format!("teardown {}", self.tag));
}
}
impl Widget for ArmWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(self.size)
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
self.painted += 1;
self.log
.borrow_mut()
.push(format!("paint {} #{}", self.tag, self.painted));
}
}
fn arm(tag: &'static str, w: f64, h: f64, log: &Log) -> Arm {
Arm {
tag,
size: Size::new(w, h),
log: Rc::clone(log),
}
}
fn new_log() -> Log {
Rc::new(RefCell::new(Vec::new()))
}
fn drain(log: &Log) -> Vec<String> {
std::mem::take(&mut *log.borrow_mut())
}
#[test]
fn helper_is_lazy_and_picks_the_arm() {
let built = Rc::new(RefCell::new(Vec::new()));
let (b1, b2) = (Rc::clone(&built), Rc::clone(&built));
let v = either(
true,
move || {
b1.borrow_mut().push("l");
1u8
},
move || {
b2.borrow_mut().push("r");
2u8
},
);
assert!(matches!(v, Either::Left(1)));
assert_eq!(*built.borrow(), vec!["l"], "only the chosen arm runs");
assert!(matches!(either(false, || 1u8, || 2u8), Either::Right(2)));
}
macro_rules! swap_matrix {
($m:ident, $E:ident, $W:ident, $a:ident, $b:ident, $arms:ty, $ui:ty) => {
mod $m {
use super::*;
type Arms = $arms;
type Ui = $ui;
fn pick(first: bool, log: &Log, sa: (f64, f64), sb: (f64, f64)) -> Arms {
if first {
$E::$a(arm("a", sa.0, sa.1, log))
} else {
$E::$b(arm("b", sb.0, sb.1, log))
}
}
#[test]
fn layout_and_paint_delegate_to_the_live_variant() {
let log = new_log();
let mut root: RenderRoot<(), Arms> = RenderRoot::new();
let mut state = ();
let mut logic = |_: &mut ()| pick(false, &log, (10.0, 10.0), (30.0, 20.0));
root.rebuild(&mut logic, &mut state);
root.layout(Size::new(200.0, 200.0));
root.paint(&mut DiscardScene, FrameTime::from_nanos(0));
let nodes = root.inspect();
assert!(
nodes[0].type_name.contains(stringify!($W)),
"root is the enum widget, got {}",
nodes[0].type_name
);
assert_eq!(nodes[0].bounds.size(), Size::new(30.0, 20.0));
let seen = drain(&log);
assert!(seen.contains(&"build b".to_string()));
assert!(seen.contains(&"paint b #1".to_string()));
assert!(
!seen.iter().any(|e| e.contains(" a")),
"inactive variant untouched: {seen:?}"
);
}
#[test]
fn same_variant_rebuild_keeps_state_swap_resets_it() {
let log = new_log();
let mut root: RenderRoot<(), Arms> = RenderRoot::new();
let mut state = ();
let first = std::cell::Cell::new(true);
let mut logic =
|_: &mut ()| pick(first.get(), &log, (10.0, 10.0), (10.0, 10.0));
root.rebuild(&mut logic, &mut state);
root.layout(Size::new(100.0, 100.0));
root.paint(&mut DiscardScene, FrameTime::from_nanos(0));
root.paint(&mut DiscardScene, FrameTime::from_nanos(1));
assert_eq!(drain(&log), ["build a", "paint a #1", "paint a #2"]);
root.rebuild(&mut logic, &mut state);
root.layout(Size::new(100.0, 100.0));
root.paint(&mut DiscardScene, FrameTime::from_nanos(2));
assert_eq!(drain(&log), ["paint a #3"]);
first.set(false);
root.rebuild(&mut logic, &mut state);
root.layout(Size::new(100.0, 100.0));
root.paint(&mut DiscardScene, FrameTime::from_nanos(3));
assert_eq!(
drain(&log),
["teardown a", "build b", "drop a", "paint b #1"]
);
first.set(true);
root.rebuild(&mut logic, &mut state);
root.paint(&mut DiscardScene, FrameTime::from_nanos(4));
assert_eq!(
drain(&log),
["teardown b", "build a", "drop b", "paint a #1"]
);
}
#[test]
fn swap_reports_full_rebuild_flags_and_same_variant_does_not() {
let log = new_log();
let mut counter = 0u64;
let mut ctx = BuildCtx::new(&mut counter);
let prev = pick(true, &log, (1.0, 1.0), (1.0, 1.0));
let mut el = prev.build(&mut ctx);
let same = pick(true, &log, (1.0, 1.0), (1.0, 1.0));
assert_eq!(same.rebuild(&prev, &mut el, &mut ctx), ChangeFlags::NONE);
let next = pick(false, &log, (1.0, 1.0), (1.0, 1.0));
let flags = next.rebuild(&same, &mut el, &mut ctx);
assert_eq!(flags, ChangeFlags::LAYOUT | ChangeFlags::PAINT);
assert!(matches!(el, $W::$b(_)));
}
#[test]
fn an_arm_swap_reports_element_replaced_and_same_variant_does_not() {
let log = new_log();
let mut counter = 0u64;
let mut ctx = BuildCtx::new(&mut counter);
let prev = pick(true, &log, (1.0, 1.0), (1.0, 1.0));
let mut el = prev.build(&mut ctx);
let same = pick(true, &log, (2.0, 2.0), (1.0, 1.0));
let (_, replaced) =
ctx.track_element_replaced(|ctx| same.rebuild(&prev, &mut el, ctx));
assert!(!replaced, "a same-variant rebuild keeps the widget");
let next = pick(false, &log, (1.0, 1.0), (1.0, 1.0));
let (_, replaced) =
ctx.track_element_replaced(|ctx| next.rebuild(&same, &mut el, ctx));
assert!(replaced, "an arm swap reports the replaced widget");
}
#[test]
fn swap_under_a_live_focus_chain_orphans_focus_only_then() {
let log = new_log();
let mut counter = 0u64;
let mut ctx = BuildCtx::new(&mut counter);
let a = pick(true, &log, (1.0, 1.0), (1.0, 1.0));
let b = pick(false, &log, (1.0, 1.0), (1.0, 1.0));
let _ = take_focus_orphaned();
ctx.set_has_focus(false);
let mut el = a.build(&mut ctx);
b.rebuild(&a, &mut el, &mut ctx);
assert!(!take_focus_orphaned(), "unfocused swap marks nothing");
ctx.set_has_focus(true);
let b2 = pick(false, &log, (1.0, 1.0), (1.0, 1.0));
b2.rebuild(&b, &mut el, &mut ctx);
assert!(!take_focus_orphaned(), "same-variant rebuild marks nothing");
a.rebuild(&b2, &mut el, &mut ctx);
assert!(take_focus_orphaned(), "focused swap orphans focus");
}
fn ui(show_button: bool) -> Ui {
if show_button {
$E::$a(button::<(), _>("Go", |_| {}))
} else {
$E::$b(checkbox::<(), _>(true, "Agree", |_, _| {}))
}
}
fn roles(u: &SemanticsUpdate) -> Vec<Role> {
u.nodes.iter().map(|(_, n)| n.role()).collect()
}
#[test]
fn semantics_reflect_the_live_variant_and_follow_a_swap() {
let mut root: RenderRoot<(), Ui> = RenderRoot::new();
let mut state = ();
let show_button = std::cell::Cell::new(true);
let mut logic = |_: &mut ()| ui(show_button.get());
let mut tcx = TextContext::new();
root.rebuild(&mut logic, &mut state);
root.layout_with_text(
Size::new(300.0, 300.0),
&mut tcx as &mut dyn std::any::Any,
);
let before = root.semantics();
let top = &before
.nodes
.iter()
.find(|(id, _)| *id == before.root)
.unwrap()
.1;
assert_eq!(
top.children().len(),
1,
"exactly the live variant contributes"
);
show_button.set(false);
root.rebuild(&mut logic, &mut state);
root.layout_with_text(
Size::new(300.0, 300.0),
&mut tcx as &mut dyn std::any::Any,
);
let after = root.semantics();
assert!(roles(&before).contains(&Role::Button));
assert!(!roles(&before).contains(&Role::CheckBox));
assert!(roles(&after).contains(&Role::CheckBox));
assert!(!roles(&after).contains(&Role::Button));
}
}
};
}
swap_matrix!(
either_matrix,
Either,
EitherWidget,
Left,
Right,
Either<Arm, Arm>,
Either<ButtonView<()>, CheckboxView<()>>
);
swap_matrix!(
either_of_3_matrix,
EitherOf3,
EitherOf3Widget,
A,
B,
EitherOf3<Arm, Arm, Arm>,
EitherOf3<ButtonView<()>, CheckboxView<()>, ButtonView<()>>
);
swap_matrix!(
either_of_8_matrix,
EitherOf8,
EitherOf8Widget,
A,
B,
EitherOf8<Arm, Arm, Arm, Arm, Arm, Arm, Arm, Arm>,
EitherOf8<
ButtonView<()>,
CheckboxView<()>,
ButtonView<()>,
ButtonView<()>,
ButtonView<()>,
ButtonView<()>,
ButtonView<()>,
ButtonView<()>,
>
);
#[test]
fn eight_variant_instance_swaps_a_to_h() {
type Eight = EitherOf8<Arm, Arm, Arm, Arm, Arm, Arm, Arm, Arm>;
let log = new_log();
let mut counter = 0u64;
let mut ctx = BuildCtx::new(&mut counter);
let a: Eight = EitherOf8::A(arm("a", 1.0, 1.0, &log));
let h: Eight = EitherOf8::H(arm("h", 5.0, 5.0, &log));
let mut el = a.build(&mut ctx);
assert!(matches!(el, EitherOf8Widget::A(_)));
let flags = h.rebuild(&a, &mut el, &mut ctx);
assert_eq!(flags, ChangeFlags::LAYOUT | ChangeFlags::PAINT);
assert!(matches!(el, EitherOf8Widget::H(_)));
assert_eq!(drain(&log), ["build a", "teardown a", "build h", "drop a"]);
h.teardown(&mut el, &mut ctx);
assert_eq!(drain(&log), ["teardown h"]);
}
}