use std::any::Any;
use std::borrow::Cow;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::num::NonZeroU64;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
pub use frust_scene::{CornerRadii, DashPattern};
use frust_scene::{GlyphRun, SceneBuilder, ShaderProgram};
use kurbo::{Affine, BezPath, Point, Rect, Size};
use peniko::{Brush, Color};
use crate::anim::FrameTime;
use crate::event::{EventCtx, EventResult, ImeState, InputEvent};
use crate::insets::WindowInsets;
use crate::layout::BoxConstraints;
use crate::semantics::SemanticsCtx;
pub trait PaintScene {
fn fill_rect(&mut self, origin: Point, size: Size, color: Color);
fn fill_rounded_rect(&mut self, _origin: Point, _size: Size, _radius: f64, _color: Color) {}
fn fill_rounded_rect_radii(
&mut self,
origin: Point,
size: Size,
radii: CornerRadii,
color: Color,
) {
self.fill_rounded_rect(origin, size, radii.largest(), color);
}
fn stroke_line(&mut self, _p0: Point, _p1: Point, _width: f64, _color: Color) {}
fn push_clip(&mut self, _origin: Point, _size: Size) {}
fn push_clip_rounded(&mut self, _origin: Point, _size: Size, _radius: f64) {}
fn push_clip_rounded_radii(&mut self, origin: Point, size: Size, radii: CornerRadii) {
self.push_clip_rounded(origin, size, radii.largest());
}
fn pop_clip(&mut self) {}
fn draw_text(&mut self, origin: Point, text: &str);
fn draw_glyph_run(&mut self, _run: GlyphRun) {}
fn draw_image(&mut self, _data: &peniko::ImageData, _dest: Rect) {}
fn draw_shader(&mut self, _program: &ShaderProgram, _dest: Rect, _time: f32) {}
fn draw_scene_texture(&mut self, _id: u64, _dest: Rect) {}
fn draw_shadow(
&mut self,
_origin: Point,
_size: Size,
_radius: f64,
_std_dev: f64,
_color: Color,
) {
}
fn fill_rect_brush(&mut self, origin: Point, size: Size, brush: &Brush) {
let color = match brush {
Brush::Solid(color) => *color,
_ => Color::TRANSPARENT,
};
self.fill_rect(origin, size, color);
}
fn fill_rounded_rect_brush(
&mut self,
_origin: Point,
_size: Size,
_radius: f64,
_brush: &Brush,
) {
}
fn push_layer(&mut self, _origin: Point, _size: Size, _alpha: f32) {}
fn pop_layer(&mut self) {}
fn clear_rect(&mut self, _origin: Point, _size: Size) {}
fn fill_path(&mut self, _origin: Point, _path: &BezPath, _brush: &Brush) {}
fn stroke_path(&mut self, _origin: Point, _path: &BezPath, _width: f64, _brush: &Brush) {}
fn stroke_path_dashed(
&mut self,
origin: Point,
path: &BezPath,
width: f64,
_dash: DashPattern,
brush: &Brush,
) {
self.stroke_path(origin, path, width, brush);
}
fn push_transform(&mut self, _transform: Affine) {}
fn pop_transform(&mut self) {}
fn push_snapshot(&mut self, _key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
let center = Point::new(origin.x + size.width / 2.0, origin.y + size.height / 2.0);
self.push_transform(Affine::scale_about(scale, center));
self.push_layer(origin, size, alpha);
}
fn pop_snapshot(&mut self) {
self.pop_layer();
self.pop_transform();
}
}
pub struct DiscardScene;
impl PaintScene for DiscardScene {
fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
fn draw_text(&mut self, _origin: Point, _text: &str) {}
}
impl PaintScene for SceneBuilder<'_> {
fn fill_rect(&mut self, origin: Point, size: Size, color: Color) {
SceneBuilder::fill_rect(self, rect_at(origin, size), Brush::Solid(color));
}
fn fill_rounded_rect(&mut self, origin: Point, size: Size, radius: f64, color: Color) {
SceneBuilder::fill_rounded_rect(self, rect_at(origin, size), radius, Brush::Solid(color));
}
fn fill_rounded_rect_radii(
&mut self,
origin: Point,
size: Size,
radii: CornerRadii,
color: Color,
) {
SceneBuilder::fill_rounded_rect_radii(
self,
rect_at(origin, size),
radii,
Brush::Solid(color),
);
}
fn stroke_line(&mut self, p0: Point, p1: Point, width: f64, color: Color) {
SceneBuilder::stroke_line(self, p0, p1, width, Brush::Solid(color));
}
fn push_clip(&mut self, origin: Point, size: Size) {
SceneBuilder::push_clip(self, rect_at(origin, size));
}
fn push_clip_rounded(&mut self, origin: Point, size: Size, radius: f64) {
SceneBuilder::push_clip_rounded(self, rect_at(origin, size), radius);
}
fn push_clip_rounded_radii(&mut self, origin: Point, size: Size, radii: CornerRadii) {
SceneBuilder::push_clip_rounded_radii(self, rect_at(origin, size), radii);
}
fn pop_clip(&mut self) {
SceneBuilder::pop_clip(self);
}
fn draw_text(&mut self, _origin: Point, _text: &str) {
}
fn draw_glyph_run(&mut self, run: GlyphRun) {
SceneBuilder::draw_glyph_run(self, run);
}
fn draw_image(&mut self, data: &peniko::ImageData, dest: Rect) {
SceneBuilder::draw_image(self, data, dest);
}
fn draw_shader(&mut self, program: &ShaderProgram, dest: Rect, time: f32) {
SceneBuilder::draw_shader(self, program, dest, time);
}
fn draw_scene_texture(&mut self, id: u64, dest: Rect) {
SceneBuilder::scene_texture(self, id, dest);
}
fn draw_shadow(&mut self, origin: Point, size: Size, radius: f64, std_dev: f64, color: Color) {
SceneBuilder::draw_blurred_rounded_rect(
self,
rect_at(origin, size),
radius,
std_dev,
color,
);
}
fn fill_rect_brush(&mut self, origin: Point, size: Size, brush: &Brush) {
SceneBuilder::fill_rect(self, rect_at(origin, size), brush.clone());
}
fn fill_rounded_rect_brush(&mut self, origin: Point, size: Size, radius: f64, brush: &Brush) {
SceneBuilder::fill_rounded_rect(self, rect_at(origin, size), radius, brush.clone());
}
fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
SceneBuilder::push_layer(self, rect_at(origin, size), alpha);
}
fn pop_layer(&mut self) {
SceneBuilder::pop_layer(self);
}
fn clear_rect(&mut self, origin: Point, size: Size) {
SceneBuilder::clear_rect(self, rect_at(origin, size));
}
fn fill_path(&mut self, origin: Point, path: &BezPath, brush: &Brush) {
SceneBuilder::fill_path(self, path_at(origin, path), brush.clone());
}
fn stroke_path(&mut self, origin: Point, path: &BezPath, width: f64, brush: &Brush) {
SceneBuilder::stroke_path(self, path_at(origin, path), width, brush.clone());
}
fn stroke_path_dashed(
&mut self,
origin: Point,
path: &BezPath,
width: f64,
dash: DashPattern,
brush: &Brush,
) {
SceneBuilder::stroke_path_dashed(self, path_at(origin, path), width, dash, brush.clone());
}
fn push_transform(&mut self, transform: Affine) {
SceneBuilder::push_transform(self, transform);
}
fn pop_transform(&mut self) {
SceneBuilder::pop_transform(self);
}
fn push_snapshot(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
SceneBuilder::push_snapshot(self, key, rect_at(origin, size), alpha, scale);
}
fn pop_snapshot(&mut self) {
SceneBuilder::pop_snapshot(self);
}
}
fn rect_at(origin: Point, size: Size) -> Rect {
Rect::new(
origin.x,
origin.y,
origin.x + size.width,
origin.y + size.height,
)
}
fn path_at(origin: Point, path: &BezPath) -> BezPath {
Affine::translate((origin.x, origin.y)) * path.clone()
}
pub struct LayoutCtx<'a> {
text_ctx: Option<&'a mut dyn Any>,
theme: Option<&'a dyn Any>,
window_insets: WindowInsets,
window_size: Size,
}
impl<'a> LayoutCtx<'a> {
pub fn new() -> LayoutCtx<'static> {
LayoutCtx {
text_ctx: None,
theme: None,
window_insets: WindowInsets::default(),
window_size: Size::ZERO,
}
}
pub fn with_text_context(text_ctx: &'a mut dyn Any) -> Self {
LayoutCtx {
text_ctx: Some(text_ctx),
theme: None,
window_insets: WindowInsets::default(),
window_size: Size::ZERO,
}
}
pub fn with_resources(text_ctx: Option<&'a mut dyn Any>, theme: Option<&'a dyn Any>) -> Self {
LayoutCtx {
text_ctx,
theme,
window_insets: WindowInsets::default(),
window_size: Size::ZERO,
}
}
pub fn with_theme(mut self, theme: &'a dyn Any) -> Self {
self.theme = Some(theme);
self
}
pub fn text_context<T: Any>(&mut self) -> &mut T {
self.text_ctx
.as_deref_mut()
.expect("no text context threaded into this layout pass")
.downcast_mut::<T>()
.expect("threaded layout resource is not the expected text-context type")
}
pub fn theme_as<T: Any>(&self) -> Option<&T> {
self.theme?.downcast_ref::<T>()
}
pub fn window_insets(&self) -> WindowInsets {
self.window_insets
}
pub(crate) fn set_window_insets(&mut self, insets: WindowInsets) {
self.window_insets = insets;
}
pub fn with_window_insets<R>(
&mut self,
insets: WindowInsets,
f: impl FnOnce(&mut Self) -> R,
) -> R {
let saved = std::mem::replace(&mut self.window_insets, insets);
let result = f(self);
self.window_insets = saved;
result
}
pub fn window_size(&self) -> Size {
self.window_size
}
pub(crate) fn set_window_size(&mut self, size: Size) {
self.window_size = size;
}
}
impl Default for LayoutCtx<'static> {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TickClass {
CosmeticLoop,
Transition,
}
pub struct PaintCtx<'a> {
origin: Point,
size: Size,
needs_frame: bool,
needs_layout: bool,
frame_unpaced: bool,
paced_interval: Option<Duration>,
ime_state: Option<ImeState>,
has_focus: bool,
hovered: bool,
hover_epoch: u64,
focus_epoch: u64,
frame_time: FrameTime,
theme: Option<&'a dyn Any>,
window_insets: WindowInsets,
presented_frames: Option<u64>,
hero: Option<&'a RefCell<HeroFrames>>,
visible_rect: Option<Rect>,
platform_views: Vec<PlatformViewFrame>,
input_shields: Vec<Rect>,
translucent: bool,
}
impl<'a> PaintCtx<'a> {
pub fn new(origin: Point, size: Size) -> Self {
Self {
origin,
size,
needs_frame: false,
needs_layout: false,
frame_unpaced: false,
paced_interval: None,
ime_state: None,
has_focus: false,
hovered: false,
hover_epoch: 0,
focus_epoch: 0,
frame_time: FrameTime::ZERO,
theme: None,
window_insets: WindowInsets::default(),
presented_frames: None,
hero: None,
visible_rect: None,
platform_views: Vec::new(),
input_shields: Vec::new(),
translucent: false,
}
}
pub fn with_theme(mut self, theme: &'a dyn Any) -> Self {
self.theme = Some(theme);
self
}
pub fn with_translucent(mut self, translucent: bool) -> Self {
self.translucent = translucent;
self
}
#[cfg(any(test, feature = "test-support"))]
pub fn for_test(origin: Point, size: Size, frame_time: FrameTime) -> Self {
let mut ctx = Self::new(origin, size);
ctx.frame_time = frame_time;
ctx
}
pub fn theme_as<T: Any>(&self) -> Option<&T> {
self.theme?.downcast_ref::<T>()
}
pub(crate) fn set_theme(&mut self, theme: Option<&'a dyn Any>) {
self.theme = theme;
}
pub(crate) fn theme_ref(&self) -> Option<&'a dyn Any> {
self.theme
}
pub fn window_insets(&self) -> WindowInsets {
self.window_insets
}
pub(crate) fn set_window_insets(&mut self, insets: WindowInsets) {
self.window_insets = insets;
}
pub(crate) fn window_insets_ref(&self) -> WindowInsets {
self.window_insets
}
pub fn with_window_insets<R>(
&mut self,
insets: WindowInsets,
f: impl FnOnce(&mut Self) -> R,
) -> R {
let saved = std::mem::replace(&mut self.window_insets, insets);
let result = f(self);
self.window_insets = saved;
result
}
pub fn presented_frames(&self) -> Option<u64> {
self.presented_frames
}
pub(crate) fn set_presented_frames(&mut self, presented: Option<u64>) {
self.presented_frames = presented;
}
pub fn origin(&self) -> Point {
self.origin
}
pub fn size(&self) -> Size {
self.size
}
pub fn has_focus(&self) -> bool {
self.has_focus
}
pub(crate) fn set_has_focus(&mut self, has_focus: bool) {
self.has_focus = has_focus;
}
pub fn is_hovered(&self) -> bool {
self.hovered
}
pub(crate) fn set_hovered(&mut self, hovered: bool) {
self.hovered = hovered;
}
pub(crate) fn set_hover_epoch(&mut self, epoch: u64) {
self.hover_epoch = epoch;
}
pub(crate) fn hover_epoch(&self) -> u64 {
self.hover_epoch
}
pub(crate) fn set_focus_epoch(&mut self, epoch: u64) {
self.focus_epoch = epoch;
}
pub(crate) fn focus_epoch(&self) -> u64 {
self.focus_epoch
}
pub fn frame_time(&self) -> FrameTime {
self.frame_time
}
pub(crate) fn set_frame_time(&mut self, frame_time: FrameTime) {
self.frame_time = frame_time;
}
pub fn request_frame(&mut self) {
self.request_frame_class(TickClass::Transition);
}
pub fn request_frame_paced(&mut self) {
self.request_frame_paced_at(Duration::ZERO);
}
pub fn request_frame_paced_at(&mut self, interval: Duration) {
self.needs_frame = true;
self.merge_paced_interval(interval.min(Self::MAX_PACED_INTERVAL));
}
pub const MAX_PACED_INTERVAL: Duration = Duration::from_secs(10);
pub fn request_frame_class(&mut self, class: TickClass) {
match class {
TickClass::Transition => {
self.needs_frame = true;
self.frame_unpaced = true;
}
TickClass::CosmeticLoop => self.request_frame_paced(),
}
}
fn merge_paced_interval(&mut self, interval: Duration) {
self.paced_interval = Some(match self.paced_interval {
Some(current) => current.min(interval),
None => interval,
});
}
fn absorb_paced_interval(&mut self, interval: Option<Duration>) {
if let Some(interval) = interval {
self.merge_paced_interval(interval);
}
}
pub fn needs_frame(&self) -> bool {
self.needs_frame
}
pub fn frame_class(&self) -> Option<TickClass> {
if !self.needs_frame {
None
} else if self.frame_unpaced {
Some(TickClass::Transition)
} else {
Some(TickClass::CosmeticLoop)
}
}
pub fn needs_frame_paced_only(&self) -> bool {
self.needs_frame && !self.frame_unpaced
}
pub fn paced_interval(&self) -> Option<Duration> {
self.paced_interval
}
pub fn request_layout(&mut self) {
self.needs_layout = true;
self.request_frame_class(TickClass::Transition);
}
pub fn needs_layout(&self) -> bool {
self.needs_layout
}
pub fn publish_ime_state(&mut self, state: ImeState) {
self.ime_state = Some(state);
}
pub fn take_ime_state(&mut self) -> Option<ImeState> {
self.ime_state.take()
}
pub fn publish_platform_view(&mut self, frame: PlatformViewFrame) {
self.platform_views.push(frame);
}
pub fn take_platform_views(&mut self) -> Vec<PlatformViewFrame> {
std::mem::take(&mut self.platform_views)
}
pub fn report_input_shield(&mut self, rect: Rect) {
self.input_shields.push(rect);
}
pub fn take_input_shields(&mut self) -> Vec<Rect> {
std::mem::take(&mut self.input_shields)
}
pub fn register_overlay(&mut self, entry: crate::overlay::OverlayEntry) {
crate::overlay::register(entry);
}
pub fn publish_selection_toolbar(
&mut self,
request: crate::selection_toolbar::SelectionToolbarRequest,
) {
crate::selection_toolbar::publish(request);
}
pub fn report_hero(&mut self, tag: &str, bounds: Rect) -> HeroDirective {
match self.hero {
Some(cell) => {
let mut frames = cell.borrow_mut();
let local = Rect::from_origin_size(
bounds.origin() - frames.reference.to_vec2(),
bounds.size(),
);
frames.captured.insert(tag.to_string(), local);
frames
.directives
.get(tag)
.copied()
.unwrap_or(HeroDirective::Normal)
}
None => HeroDirective::Normal,
}
}
pub fn hero_active(&self) -> bool {
self.hero.is_some()
}
pub fn with_hero_registry(
&mut self,
registry: &RefCell<HeroFrames>,
f: impl FnOnce(&mut PaintCtx),
) {
let mut child = PaintCtx {
origin: self.origin,
size: self.size,
needs_frame: false,
needs_layout: false,
frame_unpaced: false,
paced_interval: None,
ime_state: None,
has_focus: self.has_focus,
hovered: self.hovered,
hover_epoch: self.hover_epoch,
focus_epoch: self.focus_epoch,
frame_time: self.frame_time,
theme: self.theme,
window_insets: self.window_insets,
presented_frames: self.presented_frames,
hero: Some(registry),
visible_rect: self.visible_rect,
platform_views: Vec::new(),
input_shields: Vec::new(),
translucent: self.translucent,
};
f(&mut child);
if child.needs_frame {
self.needs_frame = true;
}
if child.needs_layout {
self.needs_layout = true;
}
if child.frame_unpaced {
self.frame_unpaced = true;
}
self.absorb_paced_interval(child.paced_interval);
if let Some(ime) = child.ime_state.take() {
self.ime_state = Some(ime);
}
self.platform_views.extend(child.platform_views);
self.input_shields.extend(child.input_shields);
}
pub(crate) fn set_hero(&mut self, hero: Option<&'a RefCell<HeroFrames>>) {
self.hero = hero;
}
pub(crate) fn hero_ref(&self) -> Option<&'a RefCell<HeroFrames>> {
self.hero
}
pub fn visible_rect(&self) -> Option<Rect> {
self.visible_rect
}
pub fn constrain_visible_rect(&mut self, rect: Rect) {
self.visible_rect = Some(match self.visible_rect {
Some(existing) => existing.intersect(rect),
None => rect,
});
}
pub(crate) fn set_visible_rect(&mut self, rect: Option<Rect>) {
self.visible_rect = rect;
}
pub(crate) fn visible_rect_ref(&self) -> Option<Rect> {
self.visible_rect
}
pub fn is_translucent(&self) -> bool {
self.translucent
}
pub(crate) fn set_translucent(&mut self, translucent: bool) {
self.translucent = translucent;
}
}
#[derive(Debug, Default)]
pub struct HeroFrames {
reference: Point,
captured: HashMap<String, Rect>,
directives: HashMap<String, HeroDirective>,
}
impl HeroFrames {
pub fn new(reference: Point, directives: HashMap<String, HeroDirective>) -> Self {
Self {
reference,
captured: HashMap::new(),
directives,
}
}
pub fn captured(&self) -> &HashMap<String, Rect> {
&self.captured
}
pub fn into_captured(self) -> HashMap<String, Rect> {
self.captured
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum HeroDirective {
Normal,
Suppress,
Morph {
dest: Rect,
},
}
static NEXT_SLOT_ID: AtomicU64 = AtomicU64::new(1);
pub fn next_slot_id() -> u64 {
NEXT_SLOT_ID.fetch_add(1, Ordering::Relaxed)
}
const MAX_PENDING_RETIRED_SLOTS: usize = 256;
static RETIRED_SLOTS: Mutex<Vec<u64>> = Mutex::new(Vec::new());
pub fn report_retired_slot(slot_id: u64) {
let mut pending = RETIRED_SLOTS.lock().unwrap_or_else(|e| e.into_inner());
if pending.len() >= MAX_PENDING_RETIRED_SLOTS {
pending.remove(0);
}
pending.push(slot_id);
}
pub fn take_retired_slots() -> Vec<u64> {
let mut pending = RETIRED_SLOTS.lock().unwrap_or_else(|e| e.into_inner());
std::mem::take(&mut *pending)
}
#[derive(Clone, Debug, PartialEq)]
pub struct PlatformViewFrame {
pub slot_id: u64,
pub view_type: String,
pub params_json: String,
pub params_generation: u64,
pub rect: Rect,
pub clip: Option<Rect>,
pub visible: bool,
pub interactive: bool,
pub shields: Vec<Rect>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PaintOutcome {
pub needs_frame: bool,
pub needs_layout: bool,
pub needs_frame_paced_only: bool,
pub paced_interval: Option<Duration>,
}
pub trait Widget: Any {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size;
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene);
fn event(&mut self, _ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
EventResult::Ignored
}
fn semantics(&self, _ctx: &mut SemanticsCtx) {}
fn type_name(&self) -> &'static str {
core::any::type_name::<Self>()
}
fn visit_children(&self, _visitor: &mut dyn FnMut(&ChildPod)) {}
}
impl dyn Widget {
pub fn downcast_mut<W: Widget>(&mut self) -> Option<&mut W> {
let any: &mut dyn Any = self;
any.downcast_mut::<W>()
}
}
impl Widget for Box<dyn Widget> {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
(**self).layout(ctx, bc)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
(**self).paint(ctx, scene);
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
(**self).event(ctx, event)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
(**self).semantics(ctx);
}
fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
(**self).visit_children(visitor);
}
fn type_name(&self) -> &'static str {
(**self).type_name()
}
}
pub struct ChildPod {
widget: Box<dyn Widget>,
origin: Point,
size: Size,
transform: Option<Affine>,
active: bool,
contacts_captor: bool,
contacts_path: bool,
focused: bool,
focus_epoch: u64,
focus_root: u64,
hover_epoch: u64,
hover_root: u64,
semantics_id: Cell<Option<NonZeroU64>>,
debug_label: Option<Cow<'static, str>>,
inspect_id: Cell<Option<NonZeroU64>>,
}
thread_local! {
static NEXT_INSPECT_ID: Cell<u64> = const { Cell::new(ChildPod::INSPECT_ID_BASE) };
static LIVE_FOCUS_SESSION: Cell<(u64, u64, u64)> = const { Cell::new((0, 0, 0)) };
}
pub(crate) fn set_live_focus_session(root: u64, live: u64, claim: u64) {
LIVE_FOCUS_SESSION.with(|slot| slot.set((root, live, claim)));
}
fn focus_claim_stamp() -> (u64, u64) {
let (root, _live, claim) = LIVE_FOCUS_SESSION.with(|slot| slot.get());
(root, claim)
}
fn names_live_focus_session(root: u64, epoch: u64) -> bool {
let (live_root, live, claim) = LIVE_FOCUS_SESSION.with(|slot| slot.get());
root == live_root && (epoch == live || epoch == claim)
}
impl Drop for ChildPod {
fn drop(&mut self) {
if crate::event::live_hover_link_is(self.hover_root, self.hover_epoch) {
crate::event::mark_hover_orphaned(self.hover_root);
}
if self.focused
&& self.focus_epoch != 0
&& names_live_focus_session(self.focus_root, self.focus_epoch)
{
crate::event::mark_focus_orphaned();
}
}
}
impl ChildPod {
pub const INSPECT_ID_BASE: u64 = 1 << 48;
pub fn new(widget: Box<dyn Widget>) -> Self {
Self {
widget,
origin: Point::ZERO,
size: Size::ZERO,
transform: None,
active: false,
contacts_captor: false,
contacts_path: false,
focused: false,
focus_epoch: 0,
focus_root: 0,
hover_epoch: 0,
hover_root: 0,
semantics_id: Cell::new(None),
debug_label: None,
inspect_id: Cell::new(None),
}
}
pub fn type_name(&self) -> &'static str {
self.widget.type_name()
}
pub fn debug_label(&self) -> Option<&str> {
self.debug_label.as_deref()
}
pub fn set_debug_label(&mut self, label: impl Into<Cow<'static, str>>) {
self.debug_label = Some(label.into());
}
pub fn clear_debug_label(&mut self) {
self.debug_label = None;
}
pub(crate) fn debug_label_cow(&self) -> Option<Cow<'static, str>> {
self.debug_label.clone()
}
pub fn inspect_id(&self) -> crate::view::WidgetId {
let id = match self.inspect_id.get() {
Some(id) => id,
None => {
let id = NEXT_INSPECT_ID.with(|next| {
let id = next.get();
next.set(id + 1);
NonZeroU64::new(id).expect("the allocator starts at 2^48, never zero")
});
self.inspect_id.set(Some(id));
id
}
};
crate::view::WidgetId(id.get())
}
pub fn widget(&self) -> &dyn Widget {
&*self.widget
}
pub fn widget_mut(&mut self) -> &mut dyn Widget {
&mut *self.widget
}
pub fn set_widget(&mut self, widget: Box<dyn Widget>) {
self.widget = widget;
}
pub fn origin(&self) -> Point {
self.origin
}
pub fn size(&self) -> Size {
self.size
}
pub fn set_origin(&mut self, origin: Point) {
self.origin = origin;
}
pub fn transform(&self) -> Option<Affine> {
self.transform
}
pub fn set_transform(&mut self, transform: Option<Affine>) {
self.transform = transform;
}
fn local_to_container(&self, transform: Affine) -> Affine {
Affine::translate(self.origin.to_vec2()) * transform
}
pub fn is_active(&self) -> bool {
self.active
}
pub fn set_active(&mut self, active: bool) {
if !active && crate::event::in_secondary_contact_pass() {
return;
}
self.active = active;
if !active {
self.contacts_captor = false;
self.contacts_path = false;
}
}
pub(crate) fn holds_contact_opt_in(&self) -> bool {
self.active && self.contacts_path
}
pub fn is_focused(&self) -> bool {
self.focused
}
pub fn set_focused(&mut self, focused: bool) {
self.focused = focused;
if focused {
let (root, epoch) = focus_claim_stamp();
self.focus_root = root;
self.focus_epoch = epoch;
}
}
pub fn focus_epoch(&self) -> u64 {
self.focus_epoch
}
pub fn holds_live_focus(&self) -> bool {
self.focused && names_live_focus_session(self.focus_root, self.focus_epoch)
}
pub fn retire_stale_focus_link(&mut self) -> bool {
if self.focused && !self.holds_live_focus() {
self.focused = false;
true
} else {
false
}
}
pub fn hover_epoch(&self) -> u64 {
self.hover_epoch
}
pub fn layout_child(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let size = self.widget.layout(ctx, bc);
self.size = size;
size
}
pub fn paint_child(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
let Some(transform) = self.transform else {
let visible_rect = ctx.visible_rect_ref();
self.paint_child_in(ctx, scene, visible_rect);
return;
};
if crate::hit::checked_inverse(&transform).is_none() {
return;
}
let abs = (ctx.origin() + self.origin.to_vec2()).to_vec2();
let around = Affine::translate(abs) * transform * Affine::translate(-abs);
let visible_rect = match (ctx.visible_rect_ref(), crate::hit::checked_inverse(&around)) {
(Some(rect), Some(inverse)) => Some(inverse.transform_rect_bbox(rect)),
_ => None,
};
scene.push_transform(around);
self.paint_child_in(ctx, scene, visible_rect);
scene.pop_transform();
}
fn paint_child_in(
&mut self,
ctx: &mut PaintCtx,
scene: &mut dyn PaintScene,
visible_rect: Option<Rect>,
) {
let child_origin = ctx.origin() + self.origin.to_vec2();
let mut child_ctx = PaintCtx::new(child_origin, self.size);
child_ctx.set_frame_time(ctx.frame_time());
child_ctx.set_theme(ctx.theme_ref());
child_ctx.set_window_insets(ctx.window_insets_ref());
child_ctx.set_presented_frames(ctx.presented_frames());
child_ctx.set_hero(ctx.hero_ref());
child_ctx.set_visible_rect(visible_rect);
child_ctx.set_translucent(ctx.is_translucent());
child_ctx.set_focus_epoch(ctx.focus_epoch());
child_ctx.set_has_focus(
self.focused && self.focus_epoch == ctx.focus_epoch() && ctx.has_focus(),
);
child_ctx.set_hover_epoch(ctx.hover_epoch());
child_ctx.set_hovered(self.hover_epoch == ctx.hover_epoch() && ctx.is_hovered());
self.widget.paint(&mut child_ctx, scene);
match child_ctx.frame_class() {
Some(TickClass::Transition) => ctx.request_frame_class(TickClass::Transition),
Some(TickClass::CosmeticLoop) => {
ctx.needs_frame = true;
debug_assert!(
child_ctx.paced_interval().is_some(),
"CosmeticLoop frame_class with no merged paced_interval — \
a new caller must be bypassing request_frame_paced_at"
);
ctx.absorb_paced_interval(child_ctx.paced_interval());
}
None => {}
}
if child_ctx.needs_layout() {
ctx.request_layout();
}
if let Some(ime) = child_ctx.take_ime_state() {
ctx.publish_ime_state(ime);
}
ctx.platform_views.extend(child_ctx.take_platform_views());
ctx.input_shields.extend(child_ctx.take_input_shields());
}
pub fn semantics_child(&self, ctx: &mut SemanticsCtx) {
let base = self.semantics_base(ctx);
let (offset, size) = match self.transform {
None => (self.origin.to_vec2(), self.size),
Some(transform) => {
let bounds = self
.local_to_container(transform)
.transform_rect_bbox(self.size.to_rect());
if bounds.is_finite() {
(bounds.origin().to_vec2(), bounds.size())
} else {
(self.origin.to_vec2(), Size::ZERO)
}
}
};
ctx.descend_into_pod(base, offset, size, |ctx| {
self.widget.semantics(ctx);
});
}
fn semantics_base(&self, ctx: &mut SemanticsCtx) -> NonZeroU64 {
match self.semantics_id.get() {
Some(id) => id,
None => {
let id = ctx.alloc_base();
self.semantics_id.set(Some(id));
id
}
}
}
pub fn event_child(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if let Some(real) = crate::event::secondary_walk_event(event) {
return self.walk_secondary(ctx, &real);
}
let local = self.localize(event);
self.dispatch_local(ctx, &local)
}
fn localize(&self, event: &InputEvent) -> InputEvent {
match self.transform {
None => event.translated(-self.origin.to_vec2()),
Some(transform) => {
match crate::hit::checked_inverse(&self.local_to_container(transform)) {
Some(inverse) => event.transformed(&inverse),
None => event.translated(-self.origin.to_vec2()),
}
}
}
}
fn walk_secondary(&mut self, ctx: &mut EventCtx, real: &InputEvent) -> EventResult {
if !self.active {
return EventResult::Ignored;
}
let local = self.localize(real);
let result = if self.contacts_captor {
crate::event::without_secondary_walk(|| self.dispatch_local(ctx, &local))
} else {
let carrier = crate::event::secondary_walk_carrier();
let (_, delivered) = crate::event::run_secondary_walk(local.clone(), || {
self.dispatch_local(ctx, &carrier)
});
match delivered {
Some(result) => result,
None => crate::event::without_secondary_walk(|| self.dispatch_local(ctx, &local)),
}
};
crate::event::note_secondary_delivered(result);
result
}
fn dispatch_local(&mut self, ctx: &mut EventCtx, local: &InputEvent) -> EventResult {
let hovered = self.hover_epoch == ctx.hover_epoch() && ctx.is_hovered();
let hover_eligible = ctx.is_hover_eligible() && !ctx.is_hover_claimed() && !self.active;
let claim_epoch = ctx.hover_claim_epoch();
let (
(opted_in, opted_in_at_or_below),
captured,
contacts,
released,
hover_claimed,
focus_req,
focus_rel,
redraw,
ime,
result,
) = {
let mut child_ctx = ctx.child_ctx(
self.origin,
self.size,
self.focused,
hovered,
hover_eligible,
);
let frame = crate::event::ContactFrame::enter(self.active && self.contacts_captor);
let result = self.widget.event(&mut child_ctx, local);
let (opted_in, opted_in_at_or_below) = frame.close();
(
(opted_in, opted_in_at_or_below),
child_ctx.is_pointer_captured(),
child_ctx.is_contact_capture_requested(),
child_ctx.is_capture_released(),
child_ctx.is_hover_claimed(),
child_ctx.is_focus_requested(),
child_ctx.is_focus_released(),
child_ctx.needs_redraw(),
child_ctx.take_ime_state(),
result,
)
};
if captured {
if !self.active {
self.contacts_captor = false;
self.contacts_path = false;
}
self.active = true;
self.contacts_captor |= opted_in;
self.contacts_path |= opted_in_at_or_below;
}
if hover_claimed {
self.hover_epoch = claim_epoch;
self.hover_root = ctx.hover_root();
}
if focus_rel {
self.set_focused(false);
}
if focus_req {
self.set_focused(true);
}
ctx.absorb_child(
redraw,
captured,
contacts,
released,
hover_claimed,
focus_req,
focus_rel,
ime,
);
result
}
pub fn contains(&self, point: Point) -> bool {
if let Some(transform) = self.transform {
return crate::hit::point_in_transformed_rect(
point,
self.size.to_rect(),
&self.local_to_container(transform),
);
}
point.x >= self.origin.x
&& point.x < self.origin.x + self.size.width
&& point.y >= self.origin.y
&& point.y < self.origin.y + self.size.height
}
}
#[cfg(test)]
mod focus_link_tests {
use super::*;
struct Session(u64, u64, u64);
impl Session {
fn enter(root: u64, live: u64, claim: u64) -> Self {
let previous = LIVE_FOCUS_SESSION.with(|slot| slot.get());
set_live_focus_session(root, live, claim);
Session(previous.0, previous.1, previous.2)
}
}
impl Drop for Session {
fn drop(&mut self) {
set_live_focus_session(self.0, self.1, self.2);
}
}
struct Inert;
impl Widget for Inert {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(10.0, 10.0))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
}
fn pod() -> ChildPod {
ChildPod::new(Box::new(Inert))
}
#[test]
fn a_recorded_link_counts_only_while_its_stamp_names_the_live_session() {
let _session = Session::enter(7, 3, 3);
let mut pod = pod();
pod.set_focused(true);
assert!(pod.holds_live_focus(), "recorded against the live session");
assert_eq!(pod.focus_epoch(), 3);
set_live_focus_session(7, 4, 4);
assert!(
pod.is_focused(),
"the raw record survives, as it always did"
);
assert!(
!pod.holds_live_focus(),
"but it no longer names the session the root has"
);
}
#[test]
fn a_link_of_another_root_never_counts_however_the_epochs_line_up() {
let _session = Session::enter(7, 3, 3);
let mut pod = pod();
pod.set_focused(true);
set_live_focus_session(8, 3, 3);
assert!(!pod.holds_live_focus());
}
#[test]
fn a_claim_recorded_during_a_dispatch_counts_before_the_dispatch_commits_it() {
let _session = Session::enter(7, 3, 4);
let mut claimed = pod();
claimed.set_focused(true);
assert_eq!(claimed.focus_epoch(), 4, "stamped with the candidate");
assert!(
claimed.holds_live_focus(),
"and observable to the container still unwinding around the claim"
);
let mut standing = pod();
standing.focused = true;
standing.focus_root = 7;
standing.focus_epoch = 3;
assert!(
standing.holds_live_focus(),
"while a link recorded before this dispatch still reads live"
);
set_live_focus_session(7, 4, 4);
assert!(claimed.holds_live_focus());
assert!(!standing.holds_live_focus());
}
#[test]
fn retiring_a_link_drops_only_one_the_live_session_has_already_stranded() {
let _session = Session::enter(7, 3, 3);
let mut pod = pod();
pod.set_focused(true);
assert!(
!pod.retire_stale_focus_link(),
"a live link is never retired by a pass that merely walked past it"
);
assert!(pod.is_focused());
set_live_focus_session(7, 4, 4);
assert!(pod.retire_stale_focus_link(), "a stranded link is dropped");
assert!(
!pod.is_focused(),
"so the raw flag and the stamp tell one story"
);
assert!(
!pod.retire_stale_focus_link(),
"and retiring is idempotent — there is nothing left to drop"
);
}
#[test]
fn a_pod_that_never_held_a_link_reports_nothing_to_retire() {
let _session = Session::enter(7, 3, 3);
let mut pod = pod();
assert!(!pod.holds_live_focus());
assert!(!pod.retire_stale_focus_link());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{PointerButton, PointerEvent, PointerPhase};
struct FixedBox {
intrinsic: Size,
}
impl Widget for FixedBox {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(self.intrinsic)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
}
}
struct Animator;
impl Widget for Animator {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
ctx.request_frame();
}
}
struct LayoutAnimator;
impl Widget for LayoutAnimator {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
ctx.request_layout();
}
}
struct PacedAnimator;
impl Widget for PacedAnimator {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
ctx.request_frame_paced();
}
}
const SLOW_PACE: Duration = Duration::from_millis(500);
struct SlowPacedAnimator;
impl Widget for SlowPacedAnimator {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
ctx.request_frame_paced_at(SLOW_PACE);
}
}
#[derive(Default)]
struct RecordingScene {
rects: Vec<(Point, Size)>,
texts: Vec<(Point, String)>,
shaders: Vec<(u64, Rect, f32)>,
scene_textures: Vec<(u64, Rect)>,
}
impl PaintScene for RecordingScene {
fn fill_rect(&mut self, origin: Point, size: Size, _color: Color) {
self.rects.push((origin, size));
}
fn draw_text(&mut self, origin: Point, text: &str) {
self.texts.push((origin, text.to_string()));
}
fn draw_shader(&mut self, program: &ShaderProgram, dest: Rect, time: f32) {
self.shaders.push((program.id(), dest, time));
}
fn draw_scene_texture(&mut self, id: u64, dest: Rect) {
self.scene_textures.push((id, dest));
}
}
struct Probe {
last_pos: Option<Point>,
capture_on_down: bool,
}
impl Widget for Probe {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
self.last_pos = Some(event.position());
*ctx.state_mut::<u32>() += 1;
ctx.request_redraw();
if self.capture_on_down
&& matches!(
event,
InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Down,
..
})
)
{
ctx.capture_pointer();
}
EventResult::Handled
}
}
fn down(x: f64, y: f64) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Down,
position: Point::new(x, y),
button: PointerButton::Primary,
})
}
#[derive(Default)]
struct FocusProbe {
saw_key: bool,
had_focus_on_key: bool,
}
impl Widget for FocusProbe {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &crate::event::InputEvent) -> EventResult {
match event {
InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Down,
..
}) => {
ctx.request_focus();
EventResult::Handled
}
InputEvent::Key(_) | InputEvent::Ime(_) => {
self.saw_key = true;
self.had_focus_on_key = ctx.has_focus();
EventResult::Handled
}
_ => EventResult::Ignored,
}
}
}
fn key_enter() -> InputEvent {
InputEvent::Key(crate::event::KeyEvent {
key: crate::event::Key::Named(crate::event::NamedKey::Enter),
modifiers: crate::event::Modifiers::default(),
repeat: false,
})
}
#[test]
fn event_child_records_focus_on_request() {
let mut pod = ChildPod::new(Box::new(FocusProbe::default()));
pod.set_origin(Point::new(5.0, 5.0));
assert!(!pod.is_focused());
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(50.0, 50.0));
pod.event_child(&mut ctx, &down(6.0, 6.0));
assert!(pod.is_focused());
assert!(ctx.is_focus_requested());
}
#[test]
fn event_child_threads_has_focus_into_child() {
let mut pod = ChildPod::new(Box::new(FocusProbe::default()));
pod.set_focused(true);
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(10.0, 10.0));
pod.event_child(&mut ctx, &key_enter());
let probe = pod.widget_mut().downcast_mut::<FocusProbe>().unwrap();
assert!(probe.saw_key);
assert!(probe.had_focus_on_key);
}
#[test]
fn widget_layout_respects_constraints() {
let mut w = FixedBox {
intrinsic: Size::new(1000.0, 1000.0),
};
let mut ctx = LayoutCtx::new();
let bc = BoxConstraints::loose(Size::new(200.0, 100.0));
assert_eq!(w.layout(&mut ctx, &bc), Size::new(200.0, 100.0));
}
#[test]
fn with_window_insets_scopes_and_restores_on_both_contexts() {
use crate::insets::EdgeInsets;
let root = WindowInsets::new(EdgeInsets::new(1.0, 2.0, 3.0, 4.0), EdgeInsets::ZERO);
let scoped = root.consuming(true, true, true, true);
let mut lctx = LayoutCtx::new();
lctx.set_window_insets(root);
let inside = lctx.with_window_insets(scoped, |ctx| ctx.window_insets());
assert_eq!(inside, scoped);
assert_eq!(lctx.window_insets(), root, "layout override restored");
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
pctx.set_window_insets(root);
let inside = pctx.with_window_insets(scoped, |ctx| ctx.window_insets());
assert_eq!(inside, scoped);
assert_eq!(pctx.window_insets(), root, "paint override restored");
}
#[test]
fn widget_paint_emits_into_scene() {
let mut w = FixedBox {
intrinsic: Size::new(50.0, 20.0),
};
let mut scene = RecordingScene::default();
let mut ctx = PaintCtx::new(Point::new(5.0, 7.0), Size::new(50.0, 20.0));
w.paint(&mut ctx, &mut scene);
assert_eq!(
scene.rects,
vec![(Point::new(5.0, 7.0), Size::new(50.0, 20.0))]
);
}
#[test]
fn widget_paint_emits_shader_into_scene() {
struct ShaderWidget {
program: ShaderProgram,
dest: Rect,
time: f32,
}
impl Widget for ShaderWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
scene.draw_shader(&self.program, self.dest, self.time);
}
}
let program = ShaderProgram::new("fn main() {}");
let dest = Rect::new(10.0, 20.0, 100.0, 150.0);
let time = 1.5;
let mut w = ShaderWidget {
program: program.clone(),
dest,
time,
};
let mut scene = RecordingScene::default();
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(200.0, 200.0));
w.paint(&mut ctx, &mut scene);
assert_eq!(scene.shaders.len(), 1);
assert_eq!(scene.shaders[0].0, program.id());
assert_eq!(scene.shaders[0].1, dest);
assert_eq!(scene.shaders[0].2, time);
}
#[test]
fn widget_paint_emits_scene_texture_into_scene() {
struct SceneTextureWidget {
id: u64,
dest: Rect,
}
impl Widget for SceneTextureWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
scene.draw_scene_texture(self.id, self.dest);
}
}
let id = 42u64;
let dest = Rect::new(10.0, 20.0, 100.0, 150.0);
let mut w = SceneTextureWidget { id, dest };
let mut scene = RecordingScene::default();
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(200.0, 200.0));
w.paint(&mut ctx, &mut scene);
assert_eq!(scene.scene_textures.len(), 1);
assert_eq!(scene.scene_textures[0].0, id);
assert_eq!(scene.scene_textures[0].1, dest);
}
#[test]
fn downcast_recovers_concrete_widget() {
let mut boxed: Box<dyn Widget> = Box::new(FixedBox {
intrinsic: Size::new(3.0, 4.0),
});
let concrete = boxed.downcast_mut::<FixedBox>().expect("downcast");
assert_eq!(concrete.intrinsic, Size::new(3.0, 4.0));
}
#[test]
fn boxed_widget_delegates_every_pass() {
let mut boxed: Box<dyn Widget> = Box::new(Probe {
last_pos: None,
capture_on_down: false,
});
let mut ctx = LayoutCtx::new();
assert_eq!(
boxed.layout(&mut ctx, &BoxConstraints::tight(Size::new(4.0, 5.0))),
Size::new(4.0, 5.0)
);
let mut count = 0u32;
let mut ectx = EventCtx::new(&mut count, Point::ZERO, Size::new(4.0, 5.0));
assert_eq!(
boxed.event(&mut ectx, &down(1.0, 1.0)),
EventResult::Handled
);
assert_eq!(count, 1);
}
#[test]
fn child_pod_layout_and_paint_offset_by_origin() {
let mut pod = ChildPod::new(Box::new(FixedBox {
intrinsic: Size::new(30.0, 10.0),
}));
pod.set_origin(Point::new(12.0, 8.0));
let mut lctx = LayoutCtx::new();
let size = pod.layout_child(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 100.0)));
assert_eq!(size, Size::new(30.0, 10.0));
assert_eq!(pod.size(), Size::new(30.0, 10.0));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::new(100.0, 200.0), Size::new(300.0, 300.0));
pod.paint_child(&mut pctx, &mut scene);
assert_eq!(
scene.rects,
vec![(Point::new(112.0, 208.0), Size::new(30.0, 10.0))]
);
}
#[test]
fn child_pod_translates_event_into_child_space() {
let mut pod = ChildPod::new(Box::new(Probe {
last_pos: None,
capture_on_down: false,
}));
pod.set_origin(Point::new(10.0, 20.0));
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(200.0, 200.0));
let result = pod.event_child(&mut ctx, &down(25.0, 35.0));
assert_eq!(result, EventResult::Handled);
assert_eq!(count, 1); let probe = pod.widget_mut().downcast_mut::<Probe>().unwrap();
assert_eq!(probe.last_pos, Some(Point::new(15.0, 15.0)));
}
#[test]
fn child_pod_propagates_capture_flag() {
let mut pod = ChildPod::new(Box::new(Probe {
last_pos: None,
capture_on_down: true,
}));
pod.set_origin(Point::new(5.0, 5.0));
assert!(!pod.is_active());
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(50.0, 50.0));
pod.event_child(&mut ctx, &down(6.0, 6.0));
assert!(pod.is_active());
assert!(ctx.is_pointer_captured());
assert!(ctx.needs_redraw());
}
#[test]
fn child_pod_bubbles_needs_frame_from_child_paint() {
let mut still = ChildPod::new(Box::new(FixedBox {
intrinsic: Size::new(10.0, 10.0),
}));
let mut lctx = LayoutCtx::new();
still.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
still.paint_child(&mut pctx, &mut scene);
assert!(!pctx.needs_frame());
let mut anim = ChildPod::new(Box::new(Animator));
anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
assert!(!pctx2.needs_frame());
anim.paint_child(&mut pctx2, &mut scene);
assert!(pctx2.needs_frame());
}
struct PlatformViewProbe {
slot_id: u64,
should_publish: bool,
}
impl Widget for PlatformViewProbe {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
if self.should_publish {
ctx.publish_platform_view(PlatformViewFrame {
slot_id: self.slot_id,
view_type: "dev.frust.Probe".to_string(),
params_json: String::new(),
params_generation: 0,
rect: Rect::from_origin_size(ctx.origin(), ctx.size()),
clip: None,
visible: true,
interactive: false,
shields: Vec::new(),
});
}
}
}
#[test]
fn child_pod_extends_platform_views_never_overwrites() {
let mut lctx = LayoutCtx::new();
let mut first = ChildPod::new(Box::new(PlatformViewProbe {
slot_id: 1,
should_publish: true,
}));
first.set_origin(Point::new(0.0, 0.0));
first.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut second = ChildPod::new(Box::new(PlatformViewProbe {
slot_id: 2,
should_publish: true,
}));
second.set_origin(Point::new(20.0, 0.0));
second.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 100.0));
first.paint_child(&mut pctx, &mut scene);
second.paint_child(&mut pctx, &mut scene);
let frames = pctx.take_platform_views();
assert_eq!(
frames.len(),
2,
"both slots' frames must survive, not just the last-painted one"
);
assert_eq!(frames[0].slot_id, 1);
assert_eq!(frames[1].slot_id, 2);
}
struct TranslatingWrapper {
child: ChildPod,
}
impl Widget for TranslatingWrapper {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.child.layout_child(ctx, bc)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.child.paint_child(ctx, scene);
}
}
#[test]
fn platform_view_frame_rect_is_absolute_under_nested_translation() {
let mut lctx = LayoutCtx::new();
let inner = ChildPod::new(Box::new(PlatformViewProbe {
slot_id: 9,
should_publish: true,
}));
let mut wrapper = TranslatingWrapper { child: inner };
wrapper.child.set_origin(Point::new(5.0, 7.0));
wrapper
.child
.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut outer = ChildPod::new(Box::new(wrapper));
outer.set_origin(Point::new(100.0, 200.0));
outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
outer.paint_child(&mut pctx, &mut scene);
let frames = pctx.take_platform_views();
assert_eq!(frames.len(), 1);
assert_eq!(
frames[0].rect,
Rect::from_origin_size(Point::new(105.0, 207.0), Size::new(10.0, 10.0))
);
}
#[test]
fn paint_ctx_origin_is_absolute_through_nested_offsets() {
use std::cell::Cell;
struct OriginRecorder {
recorded_origin: Cell<Option<Point>>,
}
impl Widget for OriginRecorder {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
self.recorded_origin.set(Some(ctx.origin()));
}
}
let mut lctx = LayoutCtx::new();
let recorder = OriginRecorder {
recorded_origin: Cell::new(None),
};
let inner = ChildPod::new(Box::new(recorder));
let mut middle = TranslatingWrapper { child: inner };
middle.child.set_origin(Point::new(20.0, 30.0));
middle
.child
.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut outer = ChildPod::new(Box::new(middle));
outer.set_origin(Point::new(50.0, 70.0));
outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
outer.paint_child(&mut pctx, &mut scene);
let leaf = outer
.widget_mut()
.downcast_mut::<TranslatingWrapper>()
.unwrap()
.child
.widget_mut()
.downcast_mut::<OriginRecorder>()
.unwrap();
assert_eq!(
leaf.recorded_origin.get(),
Some(Point::new(70.0, 100.0)),
"leaf must observe summed absolute origin"
);
}
#[test]
fn request_frame_alone_does_not_set_needs_layout() {
let mut anim = ChildPod::new(Box::new(Animator));
let mut lctx = LayoutCtx::new();
anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
anim.paint_child(&mut pctx, &mut scene);
assert!(pctx.needs_frame(), "request_frame sets needs_frame");
assert!(
!pctx.needs_layout(),
"request_frame alone must NOT set needs_layout"
);
}
#[test]
fn request_layout_implies_needs_frame() {
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
assert!(!ctx.needs_frame());
assert!(!ctx.needs_layout());
ctx.request_layout();
assert!(ctx.needs_layout());
assert!(ctx.needs_frame());
}
#[test]
fn child_pod_bubbles_needs_layout_from_child_paint() {
let mut still = ChildPod::new(Box::new(FixedBox {
intrinsic: Size::new(10.0, 10.0),
}));
let mut lctx = LayoutCtx::new();
still.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
still.paint_child(&mut pctx, &mut scene);
assert!(!pctx.needs_layout());
let mut anim = ChildPod::new(Box::new(LayoutAnimator));
anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut pctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
assert!(!pctx2.needs_layout());
anim.paint_child(&mut pctx2, &mut scene);
assert!(pctx2.needs_layout());
assert!(pctx2.needs_frame());
}
#[test]
fn needs_layout_bubbles_through_nested_containers() {
struct SingleChildContainer {
child: ChildPod,
}
impl Widget for SingleChildContainer {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.child.layout_child(ctx, bc)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.child.paint_child(ctx, scene);
}
}
let inner = SingleChildContainer {
child: ChildPod::new(Box::new(LayoutAnimator)),
};
let mut outer = ChildPod::new(Box::new(SingleChildContainer {
child: ChildPod::new(Box::new(inner)),
}));
let mut lctx = LayoutCtx::new();
outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
outer.paint_child(&mut pctx, &mut scene);
assert!(
pctx.needs_layout(),
"needs_layout bubbles up nested containers"
);
assert!(pctx.needs_frame());
}
struct SingleChildContainer {
child: ChildPod,
}
impl Widget for SingleChildContainer {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.child.layout_child(ctx, bc)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.child.paint_child(ctx, scene);
}
}
#[test]
fn no_request_yields_no_frame_class() {
let ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
assert!(ctx.frame_class().is_none());
assert!(!ctx.needs_frame());
assert!(!ctx.needs_frame_paced_only());
}
#[test]
fn request_frame_is_transition_unpaced() {
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx.request_frame();
assert!(ctx.needs_frame());
assert_eq!(ctx.frame_class(), Some(TickClass::Transition));
assert!(
!ctx.needs_frame_paced_only(),
"request_frame stays unpaced (Transition), unchanged behavior"
);
}
#[test]
fn request_frame_paced_is_cosmetic_loop() {
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx.request_frame_paced();
assert!(ctx.needs_frame());
assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
assert!(ctx.needs_frame_paced_only());
assert_eq!(ctx.paced_interval(), Some(Duration::ZERO));
}
#[test]
fn request_frame_paced_at_carries_its_interval() {
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx.request_frame_paced_at(Duration::from_millis(500));
assert!(ctx.needs_frame());
assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
assert!(ctx.needs_frame_paced_only());
assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(500)));
}
#[test]
fn request_frame_paced_at_clamps_past_the_ten_second_ceiling() {
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx.request_frame_paced_at(Duration::from_secs(10) + Duration::from_secs(1));
assert_eq!(ctx.paced_interval(), Some(PaintCtx::MAX_PACED_INTERVAL));
}
#[test]
fn request_frame_paced_at_leaves_a_real_cadence_untouched() {
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx.request_frame_paced_at(Duration::from_millis(550));
assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(550)));
}
#[test]
fn paced_intervals_aggregate_on_the_min_lattice() {
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx.request_frame_paced_at(Duration::from_millis(500));
ctx.request_frame_paced_at(Duration::from_millis(33));
assert_eq!(ctx.paced_interval(), Some(Duration::from_millis(33)));
assert!(ctx.needs_frame_paced_only());
let mut reversed = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
reversed.request_frame_paced_at(Duration::from_millis(33));
reversed.request_frame_paced_at(Duration::from_millis(500));
assert_eq!(reversed.paced_interval(), Some(Duration::from_millis(33)));
let mut shimmer_and_caret = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
shimmer_and_caret.request_frame_paced_at(Duration::from_millis(500));
shimmer_and_caret.request_frame_paced();
assert_eq!(shimmer_and_caret.paced_interval(), Some(Duration::ZERO));
}
#[test]
fn request_frame_class_cosmetic_matches_the_bare_paced_request() {
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx.request_frame_class(TickClass::CosmeticLoop);
assert_eq!(ctx.frame_class(), Some(TickClass::CosmeticLoop));
assert_eq!(ctx.paced_interval(), Some(Duration::ZERO));
}
#[test]
fn no_paced_request_names_no_interval() {
let ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
assert_eq!(ctx.paced_interval(), None);
let mut transition = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
transition.request_frame();
assert_eq!(transition.paced_interval(), None);
}
#[test]
fn transition_dominates_cosmetic_regardless_of_order() {
let mut a = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
a.request_frame_paced();
a.request_frame();
assert_eq!(a.frame_class(), Some(TickClass::Transition));
assert!(!a.needs_frame_paced_only());
let mut b = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
b.request_frame();
b.request_frame_paced();
assert_eq!(b.frame_class(), Some(TickClass::Transition));
assert!(!b.needs_frame_paced_only());
}
#[test]
fn request_layout_implies_transition_class() {
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx.request_frame_paced();
assert!(ctx.needs_frame_paced_only());
ctx.request_layout();
assert_eq!(ctx.frame_class(), Some(TickClass::Transition));
assert!(
!ctx.needs_frame_paced_only(),
"request_layout implies Transition, leaving the frame unpaced"
);
}
#[test]
fn child_pod_bubbles_paced_class_from_child_paint() {
let mut anim = ChildPod::new(Box::new(PacedAnimator));
let mut lctx = LayoutCtx::new();
anim.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
anim.paint_child(&mut pctx, &mut scene);
assert!(pctx.needs_frame());
assert_eq!(pctx.frame_class(), Some(TickClass::CosmeticLoop));
assert!(pctx.needs_frame_paced_only());
}
#[test]
fn paced_class_bubbles_through_nested_containers() {
let inner = SingleChildContainer {
child: ChildPod::new(Box::new(PacedAnimator)),
};
let mut outer = ChildPod::new(Box::new(SingleChildContainer {
child: ChildPod::new(Box::new(inner)),
}));
let mut lctx = LayoutCtx::new();
outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
outer.paint_child(&mut pctx, &mut scene);
assert!(pctx.needs_frame());
assert!(
pctx.needs_frame_paced_only(),
"a nested cosmetic-loop leaf stays paced-only up the tree"
);
}
#[test]
fn child_pod_bubbles_a_named_interval_unchanged() {
let mut outer = ChildPod::new(Box::new(SingleChildContainer {
child: ChildPod::new(Box::new(SlowPacedAnimator)),
}));
let mut lctx = LayoutCtx::new();
outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
outer.paint_child(&mut pctx, &mut scene);
assert!(pctx.needs_frame_paced_only());
assert_eq!(
pctx.paced_interval(),
Some(SLOW_PACE),
"a nested slow paced loop keeps its own cadence up the tree"
);
}
#[test]
fn absorb_paced_interval_skips_on_none_without_tightening_to_zero() {
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx.absorb_paced_interval(None);
assert_eq!(
ctx.paced_interval(),
None,
"a None interval must not tighten the aggregate to Duration::ZERO"
);
let mut ctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx2.request_frame_paced_at(Duration::from_millis(500));
ctx2.absorb_paced_interval(None);
assert_eq!(
ctx2.paced_interval(),
Some(Duration::from_millis(500)),
"a None fold must not override an already-merged interval either"
);
}
#[test]
fn sibling_paced_intervals_fold_to_the_tightest() {
struct TwoChildContainer {
a: ChildPod,
b: ChildPod,
}
impl Widget for TwoChildContainer {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.a.layout_child(ctx, bc);
self.b.layout_child(ctx, bc)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.a.paint_child(ctx, scene);
self.b.paint_child(ctx, scene);
}
}
let mut outer = ChildPod::new(Box::new(TwoChildContainer {
a: ChildPod::new(Box::new(SlowPacedAnimator)),
b: ChildPod::new(Box::new(PacedAnimator)),
}));
let mut lctx = LayoutCtx::new();
outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
outer.paint_child(&mut pctx, &mut scene);
assert!(pctx.needs_frame_paced_only());
assert_eq!(
pctx.paced_interval(),
Some(Duration::ZERO),
"the tightest sibling request (the theme cap) wins the fold"
);
}
#[test]
fn mixed_sibling_requests_aggregate_to_unpaced() {
struct TwoChildContainer {
a: ChildPod,
b: ChildPod,
}
impl Widget for TwoChildContainer {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.a.layout_child(ctx, bc);
self.b.layout_child(ctx, bc)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.a.paint_child(ctx, scene);
self.b.paint_child(ctx, scene);
}
}
let mut outer = ChildPod::new(Box::new(TwoChildContainer {
a: ChildPod::new(Box::new(PacedAnimator)),
b: ChildPod::new(Box::new(Animator)),
}));
let mut lctx = LayoutCtx::new();
outer.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut pctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
outer.paint_child(&mut pctx, &mut scene);
assert!(pctx.needs_frame());
assert_eq!(pctx.frame_class(), Some(TickClass::Transition));
assert!(
!pctx.needs_frame_paced_only(),
"a mixed paced+transition sibling set aggregates to unpaced"
);
}
#[test]
fn with_hero_registry_bubbles_paced_class() {
let registry = RefCell::new(HeroFrames::default());
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx.with_hero_registry(®istry, |child| {
child.request_frame_paced();
});
assert!(ctx.needs_frame());
assert!(
ctx.needs_frame_paced_only(),
"with_hero_registry bubbles the paced class up"
);
let mut ctx2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx2.request_frame_paced();
ctx2.with_hero_registry(®istry, |child| {
child.request_frame();
});
assert_eq!(ctx2.frame_class(), Some(TickClass::Transition));
assert!(!ctx2.needs_frame_paced_only());
let mut ctx3 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
ctx3.with_hero_registry(®istry, |child| {
child.request_frame_paced_at(SLOW_PACE);
});
assert_eq!(ctx3.paced_interval(), Some(SLOW_PACE));
ctx3.request_frame_paced();
assert_eq!(
ctx3.paced_interval(),
Some(Duration::ZERO),
"the outer theme-cap request tightens the folded aggregate"
);
}
#[test]
fn child_pod_paint_seeds_has_focus_composed_with_ancestor() {
use std::cell::Cell;
use std::rc::Rc;
struct FocusProbe(Rc<Cell<Option<bool>>>);
impl Widget for FocusProbe {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(10.0, 10.0))
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
self.0.set(Some(ctx.has_focus()));
}
}
let seen = Rc::new(Cell::new(None));
let mut pod = ChildPod::new(Box::new(FocusProbe(seen.clone())));
let mut lctx = LayoutCtx::new();
pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
pod.set_focused(true);
let mut focused_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
focused_parent.set_has_focus(true);
pod.paint_child(&mut focused_parent, &mut scene);
assert_eq!(seen.get(), Some(true));
let mut blurred_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
blurred_parent.set_has_focus(false);
pod.paint_child(&mut blurred_parent, &mut scene);
assert_eq!(seen.get(), Some(false));
pod.set_focused(false);
let mut focused_parent2 = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
focused_parent2.set_has_focus(true);
pod.paint_child(&mut focused_parent2, &mut scene);
assert_eq!(seen.get(), Some(false));
}
#[test]
fn child_pod_stamps_a_hover_claim_and_seeds_it_by_epoch() {
use std::cell::Cell;
use std::rc::Rc;
struct HoverProbe {
event_seen: Rc<Cell<Option<bool>>>,
paint_seen: Rc<Cell<Option<bool>>>,
}
impl Widget for HoverProbe {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(10.0, 10.0))
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
self.paint_seen.set(Some(ctx.is_hovered()));
}
fn event(&mut self, ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
self.event_seen.set(Some(ctx.is_hovered()));
ctx.claim_hover();
EventResult::Ignored
}
}
let event_seen = Rc::new(Cell::new(None));
let paint_seen = Rc::new(Cell::new(None));
let mut pod = ChildPod::new(Box::new(HoverProbe {
event_seen: event_seen.clone(),
paint_seen: paint_seen.clone(),
}));
let mut lctx = LayoutCtx::new();
pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let mut state = ();
let event = InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Move,
position: Point::new(5.0, 5.0),
button: PointerButton::Primary,
});
assert_eq!(pod.hover_epoch(), 0);
{
let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
ctx.set_hover_epoch(4);
pod.event_child(&mut ctx, &event);
assert!(!ctx.is_hover_claimed(), "nothing bubbled up");
}
assert_eq!(pod.hover_epoch(), 0, "no stamp from an ineligible pass");
{
let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
ctx.set_hover_epoch(4);
ctx.set_hovered(true);
ctx.set_hover_eligible(true);
pod.event_child(&mut ctx, &event);
assert!(ctx.is_hover_claimed());
assert_eq!(
event_seen.get(),
Some(false),
"the pod held no link going into this pass"
);
}
assert_eq!(pod.hover_epoch(), 5);
let mut live = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
live.set_hover_epoch(5);
live.set_hovered(true);
pod.paint_child(&mut live, &mut scene);
assert_eq!(paint_seen.get(), Some(true));
let mut unhovered_parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
unhovered_parent.set_hover_epoch(5);
unhovered_parent.set_hovered(false);
pod.paint_child(&mut unhovered_parent, &mut scene);
assert_eq!(paint_seen.get(), Some(false));
let mut later = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
later.set_hover_epoch(6);
later.set_hovered(true);
pod.paint_child(&mut later, &mut scene);
assert_eq!(paint_seen.get(), Some(false));
pod.set_active(true);
{
let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, pod.size());
ctx.set_hover_epoch(6);
ctx.set_hover_eligible(true);
pod.event_child(&mut ctx, &event);
assert!(!ctx.is_hover_claimed());
}
assert_eq!(pod.hover_epoch(), 5, "the captured pod kept its old stamp");
}
#[test]
fn only_the_first_hover_claim_in_a_pass_is_recorded() {
struct Claimer;
impl Widget for Claimer {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(10.0, 10.0))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
ctx.claim_hover();
EventResult::Ignored
}
}
let mut first = ChildPod::new(Box::new(Claimer));
let mut second = ChildPod::new(Box::new(Claimer));
let mut state = ();
let event = InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Move,
position: Point::new(5.0, 5.0),
button: PointerButton::Primary,
});
let mut ctx = EventCtx::new(&mut state as &mut dyn Any, Point::ZERO, Size::ZERO);
ctx.set_hover_epoch(2);
ctx.set_hover_eligible(true);
first.event_child(&mut ctx, &event);
second.event_child(&mut ctx, &event);
assert_eq!(first.hover_epoch(), 3, "the first claim is recorded");
assert_eq!(
second.hover_epoch(),
0,
"the pass was closed to further claims"
);
}
#[test]
fn child_pod_contains_uses_container_space_bounds() {
let mut pod = ChildPod::new(Box::new(FixedBox {
intrinsic: Size::ZERO,
}));
pod.set_origin(Point::new(10.0, 10.0));
let mut lctx = LayoutCtx::new();
pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(20.0, 20.0)));
assert!(pod.contains(Point::new(10.0, 10.0))); assert!(pod.contains(Point::new(29.9, 29.9)));
assert!(!pod.contains(Point::new(30.0, 30.0))); assert!(!pod.contains(Point::new(9.9, 15.0)));
}
#[derive(Default)]
struct ShadowLayerRecordingScene {
shadows: Vec<(Point, Size, f64, f64, Color)>,
layers: Vec<(Point, Size, f32)>,
pops: u32,
}
impl PaintScene for ShadowLayerRecordingScene {
fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
fn draw_text(&mut self, _origin: Point, _text: &str) {}
fn draw_shadow(
&mut self,
origin: Point,
size: Size,
radius: f64,
std_dev: f64,
color: Color,
) {
self.shadows.push((origin, size, radius, std_dev, color));
}
fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
self.layers.push((origin, size, alpha));
}
fn pop_layer(&mut self) {
self.pops += 1;
}
}
#[test]
fn draw_shadow_and_push_layer_are_no_ops_when_not_overridden() {
let mut scene = RecordingScene::default();
scene.draw_shadow(Point::ZERO, Size::new(10.0, 10.0), 4.0, 2.0, Color::BLACK);
scene.push_layer(Point::ZERO, Size::new(10.0, 10.0), 0.5);
scene.pop_layer();
assert!(scene.rects.is_empty());
assert!(scene.texts.is_empty());
}
#[test]
fn fill_path_and_stroke_path_are_no_ops_when_not_overridden() {
let mut scene = RecordingScene::default();
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((1.0, 0.0));
scene.fill_path(Point::ZERO, &path, &Brush::Solid(Color::BLACK));
scene.stroke_path(Point::ZERO, &path, 2.0, &Brush::Solid(Color::BLACK));
assert!(scene.rects.is_empty());
assert!(scene.texts.is_empty());
}
#[derive(Default)]
struct PathRecordingScene {
fills: Vec<(Point, BezPath)>,
strokes: Vec<(Point, BezPath, f64)>,
}
impl PaintScene for PathRecordingScene {
fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
fn draw_text(&mut self, _origin: Point, _text: &str) {}
fn fill_path(&mut self, origin: Point, path: &BezPath, _brush: &Brush) {
self.fills.push((origin, path.clone()));
}
fn stroke_path(&mut self, origin: Point, path: &BezPath, width: f64, _brush: &Brush) {
self.strokes.push((origin, path.clone(), width));
}
}
#[test]
fn fill_path_and_stroke_path_reach_an_overriding_implementor() {
let mut scene = PathRecordingScene::default();
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((5.0, 0.0));
scene.fill_path(Point::new(1.0, 2.0), &path, &Brush::Solid(Color::BLACK));
scene.stroke_path(
Point::new(3.0, 4.0),
&path,
1.5,
&Brush::Solid(Color::BLACK),
);
assert_eq!(scene.fills.len(), 1);
assert_eq!(scene.fills[0].0, Point::new(1.0, 2.0));
assert_eq!(scene.fills[0].1, path);
assert_eq!(scene.strokes.len(), 1);
assert_eq!(scene.strokes[0].0, Point::new(3.0, 4.0));
assert_eq!(scene.strokes[0].1, path);
assert_eq!(scene.strokes[0].2, 1.5);
}
#[test]
fn path_at_translates_path_points_by_origin() {
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((5.0, 0.0));
let translated = path_at(Point::new(10.0, 20.0), &path);
let mut expected = BezPath::new();
expected.move_to((10.0, 20.0));
expected.line_to((15.0, 20.0));
assert_eq!(translated, expected);
}
#[derive(Debug, PartialEq)]
struct TestTheme {
accent: u32,
}
#[test]
fn layout_ctx_theme_as_recovers_threaded_theme() {
let theme = TestTheme { accent: 7 };
let ctx = LayoutCtx::new().with_theme(&theme);
assert_eq!(ctx.theme_as::<TestTheme>(), Some(&TestTheme { accent: 7 }));
}
#[test]
fn layout_ctx_theme_as_is_none_without_a_theme() {
let ctx = LayoutCtx::new();
assert!(ctx.theme_as::<TestTheme>().is_none());
}
#[test]
fn layout_ctx_theme_as_is_none_on_type_mismatch() {
let theme = TestTheme { accent: 1 };
let ctx = LayoutCtx::new().with_theme(&theme);
assert!(ctx.theme_as::<u32>().is_none());
}
#[test]
fn paint_ctx_theme_as_recovers_threaded_theme() {
let theme = TestTheme { accent: 9 };
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(1.0, 1.0));
ctx.set_theme(Some(&theme));
assert_eq!(ctx.theme_as::<TestTheme>(), Some(&TestTheme { accent: 9 }));
}
#[test]
fn paint_ctx_theme_as_is_none_without_a_theme() {
let ctx = PaintCtx::new(Point::ZERO, Size::new(1.0, 1.0));
assert!(ctx.theme_as::<TestTheme>().is_none());
}
#[test]
fn child_pod_paint_threads_theme_into_child() {
use std::cell::Cell;
use std::rc::Rc;
struct ThemeProbe(Rc<Cell<Option<u32>>>);
impl Widget for ThemeProbe {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(10.0, 10.0))
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
self.0.set(ctx.theme_as::<TestTheme>().map(|t| t.accent));
}
}
let seen = Rc::new(Cell::new(None));
let mut pod = ChildPod::new(Box::new(ThemeProbe(seen.clone())));
let mut lctx = LayoutCtx::new();
pod.layout_child(&mut lctx, &BoxConstraints::tight(Size::new(10.0, 10.0)));
let mut scene = RecordingScene::default();
let theme = TestTheme { accent: 42 };
let mut parent = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
parent.set_theme(Some(&theme));
pod.paint_child(&mut parent, &mut scene);
assert_eq!(seen.get(), Some(42));
let mut bare = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
pod.paint_child(&mut bare, &mut scene);
assert_eq!(seen.get(), None);
}
struct BlinkBox;
impl Widget for BlinkBox {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(4.0, 4.0))
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
let millis = ctx.frame_time().as_nanos() / 1_000_000;
if (millis / 500).is_multiple_of(2) {
scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
}
}
}
#[test]
fn for_test_seeds_the_requested_frame_time() {
let ctx = PaintCtx::for_test(Point::ZERO, Size::new(1.0, 1.0), FrameTime::from_nanos(42));
assert_eq!(ctx.frame_time(), FrameTime::from_nanos(42));
}
#[test]
fn for_test_drives_a_clock_dependent_widget_to_two_appearances() {
let mut widget = BlinkBox;
let size = Size::new(4.0, 4.0);
let mut on_ctx = PaintCtx::for_test(Point::ZERO, size, FrameTime::from_nanos(0));
let mut on_scene = RecordingScene::default();
widget.paint(&mut on_ctx, &mut on_scene);
assert_eq!(on_scene.rects.len(), 1, "expected the caret painted on");
let mut off_ctx = PaintCtx::for_test(Point::ZERO, size, FrameTime::from_nanos(500_000_000));
let mut off_scene = RecordingScene::default();
widget.paint(&mut off_ctx, &mut off_scene);
assert!(
off_scene.rects.is_empty(),
"expected the caret painted off half a blink cycle later"
);
}
#[test]
fn draw_shadow_and_push_layer_reach_an_overriding_implementor() {
let mut scene = ShadowLayerRecordingScene::default();
let origin = Point::new(3.0, 4.0);
let size = Size::new(20.0, 12.0);
scene.draw_shadow(origin, size, 6.0, 3.0, Color::BLACK);
scene.push_layer(origin, size, 0.25);
scene.pop_layer();
assert_eq!(scene.shadows, vec![(origin, size, 6.0, 3.0, Color::BLACK)]);
assert_eq!(scene.layers, vec![(origin, size, 0.25)]);
assert_eq!(scene.pops, 1);
}
#[test]
fn a_pods_type_name_resolves_through_every_box() {
let single = ChildPod::new(Box::new(Animator));
assert!(
single.type_name().ends_with("Animator"),
"{}",
single.type_name()
);
let boxed: Box<dyn Widget> = Box::new(Animator);
let double = ChildPod::new(Box::new(boxed));
assert!(
double.type_name().ends_with("Animator"),
"{}",
double.type_name()
);
}
#[test]
fn a_pods_tooling_id_is_assigned_once_and_never_shared() {
let a = ChildPod::new(Box::new(Animator));
let b = ChildPod::new(Box::new(Animator));
let first = a.inspect_id();
assert_eq!(a.inspect_id(), first, "a pod keeps the id it was given");
assert_ne!(b.inspect_id(), first, "two pods never share an id");
assert!(
first.0 >= ChildPod::INSPECT_ID_BASE,
"pod ids stay in the range reserved against arena WidgetIds"
);
}
#[test]
fn a_leaf_visits_no_children_and_a_container_visits_all_of_its_own() {
struct Two {
children: Vec<ChildPod>,
}
impl Widget for Two {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
for child in &self.children {
visitor(child);
}
}
}
let mut seen = 0;
Animator.visit_children(&mut |_| seen += 1);
assert_eq!(seen, 0);
let container = Two {
children: vec![
ChildPod::new(Box::new(Animator)),
ChildPod::new(Box::new(Animator)),
],
};
let mut ids = Vec::new();
container.visit_children(&mut |child| ids.push(child.inspect_id()));
assert_eq!(ids.len(), 2);
assert_ne!(ids[0], ids[1]);
}
#[derive(Default)]
struct UniformOnlyScene {
fills: Vec<(Point, Size, f64)>,
clips: Vec<(Point, Size, f64)>,
strokes: Vec<(Point, f64)>,
pops: usize,
}
impl PaintScene for UniformOnlyScene {
fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
fn draw_text(&mut self, _origin: Point, _text: &str) {}
fn fill_rounded_rect(&mut self, origin: Point, size: Size, radius: f64, _color: Color) {
self.fills.push((origin, size, radius));
}
fn push_clip_rounded(&mut self, origin: Point, size: Size, radius: f64) {
self.clips.push((origin, size, radius));
}
fn pop_clip(&mut self) {
self.pops += 1;
}
fn stroke_path(&mut self, origin: Point, _path: &BezPath, width: f64, _brush: &Brush) {
self.strokes.push((origin, width));
}
}
fn corner_probe_path() -> BezPath {
let mut path = BezPath::new();
path.move_to((0.0, 0.0));
path.line_to((10.0, 0.0));
path
}
#[test]
fn per_corner_paint_defaults_fall_back_to_the_uniform_methods() {
let mut scene = UniformOnlyScene::default();
let origin = Point::new(2.0, 3.0);
let size = Size::new(20.0, 10.0);
let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
scene.fill_rounded_rect_radii(origin, size, radii, Color::BLACK);
scene.push_clip_rounded_radii(origin, size, radii);
scene.pop_clip();
assert_eq!(scene.fills, vec![(origin, size, 12.0)]);
assert_eq!(scene.clips, vec![(origin, size, 12.0)]);
assert_eq!(scene.pops, 1);
}
#[test]
fn dashed_stroke_default_falls_back_to_the_solid_stroke() {
let mut scene = UniformOnlyScene::default();
let origin = Point::new(5.0, 6.0);
scene.stroke_path_dashed(
origin,
&corner_probe_path(),
2.0,
DashPattern::new(4.0, 2.0),
&Brush::Solid(Color::BLACK),
);
assert_eq!(scene.strokes, vec![(origin, 2.0)]);
}
#[test]
fn scene_builder_records_per_corner_radii_translated_by_origin() {
let mut scene = frust_scene::Scene::new();
let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
{
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
let paint: &mut dyn PaintScene = &mut builder;
paint.fill_rounded_rect_radii(
Point::new(2.0, 3.0),
Size::new(20.0, 10.0),
radii,
Color::BLACK,
);
paint.push_clip_rounded_radii(Point::new(0.0, 0.0), Size::new(5.0, 5.0), radii);
paint.pop_clip();
}
match &scene.commands()[0] {
frust_scene::Command::RoundedRect {
rect, radii: got, ..
} => {
assert_eq!(*rect, Rect::new(2.0, 3.0, 22.0, 13.0));
assert_eq!(*got, radii);
}
other => panic!("expected RoundedRect, got {other:?}"),
}
match &scene.commands()[1] {
frust_scene::Command::PushClipRounded { radii: got, .. } => assert_eq!(*got, radii),
other => panic!("expected PushClipRounded, got {other:?}"),
}
assert!(matches!(scene.commands()[2], frust_scene::Command::PopClip));
}
#[test]
fn scene_builder_records_a_dashed_stroke_translated_by_origin() {
let mut scene = frust_scene::Scene::new();
let dash = DashPattern::new(4.0, 2.0).with_phase(1.0);
{
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
let paint: &mut dyn PaintScene = &mut builder;
paint.stroke_path_dashed(
Point::new(5.0, 0.0),
&corner_probe_path(),
2.0,
dash,
&Brush::Solid(Color::BLACK),
);
}
match &scene.commands()[0] {
frust_scene::Command::Path { path, style, .. } => {
assert_eq!(
*style,
frust_scene::PathStyle::Stroke {
width: 2.0,
dash: Some(dash)
}
);
assert_eq!(path.elements().len(), 2);
assert!(matches!(
path.elements()[0],
kurbo::PathEl::MoveTo(p) if p == Point::new(5.0, 0.0)
));
}
other => panic!("expected Path, got {other:?}"),
}
}
#[test]
fn uniform_paint_calls_still_encode_the_commands_they_always_did() {
let mut scene = frust_scene::Scene::new();
{
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
let paint: &mut dyn PaintScene = &mut builder;
paint.fill_rounded_rect(
Point::new(0.0, 0.0),
Size::new(10.0, 10.0),
4.0,
Color::BLACK,
);
paint.push_clip_rounded(Point::new(0.0, 0.0), Size::new(10.0, 10.0), 4.0);
paint.pop_clip();
paint.draw_shadow(
Point::new(0.0, 0.0),
Size::new(10.0, 10.0),
4.0,
2.0,
Color::BLACK,
);
paint.stroke_path(
Point::new(0.0, 0.0),
&corner_probe_path(),
1.0,
&Brush::Solid(Color::BLACK),
);
}
let uniform = CornerRadii::uniform(4.0);
match &scene.commands()[0] {
frust_scene::Command::RoundedRect { radii, .. } => assert_eq!(*radii, uniform),
other => panic!("expected RoundedRect, got {other:?}"),
}
match &scene.commands()[1] {
frust_scene::Command::PushClipRounded { radii, .. } => assert_eq!(*radii, uniform),
other => panic!("expected PushClipRounded, got {other:?}"),
}
match &scene.commands()[3] {
frust_scene::Command::BlurredRoundedRect { radii, .. } => assert_eq!(*radii, uniform),
other => panic!("expected BlurredRoundedRect, got {other:?}"),
}
match &scene.commands()[4] {
frust_scene::Command::Path { style, .. } => assert_eq!(
*style,
frust_scene::PathStyle::Stroke {
width: 1.0,
dash: None
}
),
other => panic!("expected Path, got {other:?}"),
}
}
#[test]
fn every_paint_scene_method_terminates_on_discard_scene() {
use frust_scene::{FontHandle, Glyph};
let mut scene = DiscardScene;
let origin = Point::new(2.0, 3.0);
let size = Size::new(20.0, 10.0);
let color = Color::BLACK;
let brush = Brush::Solid(color);
let radii = CornerRadii::new(4.0, 12.0, 0.0, 1.0);
let path = corner_probe_path();
scene.fill_rect(origin, size, color);
scene.fill_rounded_rect(origin, size, 4.0, color);
scene.fill_rounded_rect_radii(origin, size, radii, color);
scene.stroke_line(origin, Point::new(10.0, 10.0), 1.0, color);
scene.push_clip(origin, size);
scene.push_clip_rounded(origin, size, 4.0);
scene.push_clip_rounded_radii(origin, size, radii);
scene.pop_clip();
scene.draw_text(origin, "discarded");
let font = FontHandle::new(peniko::FontData::new(
peniko::Blob::from(Vec::<u8>::new()),
0,
));
let run = GlyphRun {
font,
font_size: 16.0,
brush: brush.clone(),
transform: Affine::IDENTITY,
glyphs: vec![Glyph {
id: 1,
x: 0.0,
y: 0.0,
}],
};
scene.draw_glyph_run(run);
let image = peniko::ImageData {
data: peniko::Blob::from(vec![0u8; 2 * 2 * 4]),
format: peniko::ImageFormat::Rgba8,
alpha_type: peniko::ImageAlphaType::Alpha,
width: 2,
height: 2,
};
scene.draw_image(&image, Rect::new(0.0, 0.0, 20.0, 20.0));
let program = ShaderProgram::new("fn main() {}");
scene.draw_shader(&program, Rect::new(0.0, 0.0, 10.0, 10.0), 1.5);
scene.draw_shadow(origin, size, 4.0, 2.0, color);
scene.fill_rect_brush(origin, size, &brush);
scene.fill_rounded_rect_brush(origin, size, 4.0, &brush);
scene.push_layer(origin, size, 0.5);
scene.pop_layer();
scene.clear_rect(origin, size);
scene.fill_path(origin, &path, &brush);
scene.stroke_path(origin, &path, 1.0, &brush);
scene.stroke_path_dashed(origin, &path, 1.0, DashPattern::new(4.0, 2.0), &brush);
scene.push_transform(Affine::translate((1.0, 2.0)));
scene.pop_transform();
}
#[derive(Default)]
struct MinimalSnapshotRecorder {
operations: Vec<String>,
}
impl PaintScene for MinimalSnapshotRecorder {
fn fill_rect(&mut self, _origin: Point, _size: Size, _color: Color) {}
fn draw_text(&mut self, _origin: Point, _text: &str) {}
fn push_transform(&mut self, _transform: Affine) {
self.operations.push("push_transform".to_string());
}
fn pop_transform(&mut self) {
self.operations.push("pop_transform".to_string());
}
fn push_layer(&mut self, _origin: Point, _size: Size, _alpha: f32) {
self.operations.push("push_layer".to_string());
}
fn pop_layer(&mut self) {
self.operations.push("pop_layer".to_string());
}
}
#[test]
fn default_push_snapshot_pop_snapshot_emulates_presentation_for_recorders() {
let mut recorder = MinimalSnapshotRecorder::default();
let origin = Point::new(10.0, 20.0);
let size = Size::new(100.0, 50.0);
let alpha = 0.8;
let scale = 0.9;
recorder.push_snapshot(1, origin, size, alpha, scale);
recorder.pop_snapshot();
assert_eq!(
recorder.operations,
vec![
"push_transform".to_string(),
"push_layer".to_string(),
"pop_layer".to_string(),
"pop_transform".to_string(),
]
);
}
#[test]
fn scene_builder_records_push_pop_snapshot_commands_directly_without_extra_transforms() {
let mut scene = frust_scene::Scene::new();
{
let mut builder = frust_scene::SceneBuilder::new(&mut scene);
let paint: &mut dyn PaintScene = &mut builder;
paint.push_snapshot(7, Point::new(5.0, 10.0), Size::new(80.0, 40.0), 0.5, 1.2);
paint.fill_rect(Point::new(10.0, 15.0), Size::new(20.0, 25.0), Color::BLACK);
paint.pop_snapshot();
}
let commands = scene.commands();
assert_eq!(commands.len(), 3);
match &commands[0] {
frust_scene::Command::PushSnapshot {
key,
rect,
alpha,
scale,
..
} => {
assert_eq!(*key, 7);
assert_eq!(*rect, Rect::new(5.0, 10.0, 85.0, 50.0));
assert_eq!(*alpha, 0.5);
assert_eq!(*scale, 1.2);
}
other => panic!("expected PushSnapshot, got {other:?}"),
}
match &commands[1] {
frust_scene::Command::FillRect { rect, .. } => {
assert_eq!(*rect, Rect::new(10.0, 15.0, 30.0, 40.0));
}
other => panic!("expected FillRect, got {other:?}"),
}
assert!(
matches!(commands[2], frust_scene::Command::PopSnapshot),
"expected PopSnapshot, got {:?}",
commands[2]
);
}
}
#[cfg(test)]
mod transform_tests {
use std::f64::consts::FRAC_PI_4;
use kurbo::Vec2;
use super::*;
use crate::event::{
PointerButton, PointerEvent, PointerId, PointerPhase, ScaleEvent, ScalePhase, ScrollDelta,
};
#[derive(Clone, Debug, PartialEq)]
enum Op {
Rect(Point, Size),
Push(Affine),
Pop,
}
#[derive(Default)]
struct OpLog(Vec<Op>);
impl PaintScene for OpLog {
fn fill_rect(&mut self, origin: Point, size: Size, _color: Color) {
self.0.push(Op::Rect(origin, size));
}
fn draw_text(&mut self, _origin: Point, _text: &str) {}
fn push_transform(&mut self, transform: Affine) {
self.0.push(Op::Push(transform));
}
fn pop_transform(&mut self) {
self.0.push(Op::Pop);
}
}
#[derive(Default)]
struct Recorder {
events: Vec<InputEvent>,
}
impl Widget for Recorder {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
self.events.push(event.clone());
if matches!(
event,
InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Down,
..
})
) {
ctx.capture_pointer();
}
EventResult::Handled
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
ctx.push_node(accesskit::Role::Label, |_| {});
}
}
struct VisibleProbe(std::rc::Rc<Cell<Option<Rect>>>);
impl Widget for VisibleProbe {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
self.0.set(ctx.visible_rect());
}
}
fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase,
position: Point::new(x, y),
button: PointerButton::Primary,
})
}
fn recorder_pod(origin: Point, size: Size) -> ChildPod {
let mut pod = ChildPod::new(Box::new(Recorder::default()));
pod.layout_child(&mut LayoutCtx::new(), &BoxConstraints::tight(size));
pod.set_origin(origin);
pod
}
fn recorded(pod: &mut ChildPod) -> Vec<InputEvent> {
pod.widget_mut()
.downcast_mut::<Recorder>()
.expect("recorder pod")
.events
.clone()
}
fn paint(pod: &mut ChildPod, visible: Option<Rect>) -> (Vec<Op>, Option<Rect>) {
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(400.0, 400.0));
ctx.set_visible_rect(visible);
let mut scene = OpLog::default();
pod.paint_child(&mut ctx, &mut scene);
(scene.0, ctx.visible_rect_ref())
}
fn dispatch(pod: &mut ChildPod, event: &InputEvent) -> EventResult {
let mut state = 0u32;
let mut ctx = EventCtx::new(&mut state, Point::ZERO, Size::new(400.0, 400.0));
pod.event_child(&mut ctx, event)
}
#[test]
fn a_child_under_scale_and_translate_receives_down_at_its_local_point() {
let mut pod = recorder_pod(Point::new(10.0, 20.0), Size::new(50.0, 50.0));
pod.set_transform(Some(
Affine::translate(Vec2::new(5.0, 5.0)) * Affine::scale(2.0),
));
let at = Point::new(29.0, 43.0);
assert!(pod.contains(at));
assert_eq!(
dispatch(&mut pod, &pointer(PointerPhase::Down, at.x, at.y)),
EventResult::Handled
);
assert!(pod.is_active(), "capture bookkeeping is unchanged");
dispatch(
&mut pod,
&InputEvent::Scroll {
position: at,
delta: ScrollDelta::Pixels(0.0, 12.0),
},
);
dispatch(
&mut pod,
&InputEvent::Scale(ScaleEvent {
phase: ScalePhase::Update,
scale_delta: 1.1,
focal: at,
velocity: 0.0,
}),
);
let local = Point::new(7.0, 9.0);
assert_eq!(
recorded(&mut pod),
vec![
pointer(PointerPhase::Down, local.x, local.y),
InputEvent::Scroll {
position: local,
delta: ScrollDelta::Pixels(0.0, 12.0),
},
InputEvent::Scale(ScaleEvent {
phase: ScalePhase::Update,
scale_delta: 1.1,
focal: local,
velocity: 0.0,
}),
]
);
assert!(!pod.contains(Point::new(12.0, 22.0)));
assert!(pod.contains(Point::new(100.0, 100.0)));
assert!(!pod.contains(Point::new(116.0, 60.0)));
let contact = InputEvent::PointerContact {
pointer_id: PointerId::touch(1),
event: PointerEvent {
phase: PointerPhase::Move,
position: at,
button: PointerButton::Primary,
},
};
dispatch(&mut pod, &contact);
assert_eq!(
recorded(&mut pod).last(),
Some(&InputEvent::PointerContact {
pointer_id: PointerId::touch(1),
event: PointerEvent {
phase: PointerPhase::Move,
position: local,
button: PointerButton::Primary,
},
})
);
}
#[test]
fn contains_agrees_with_paint_for_rotated_content_at_the_corners() {
let size = Size::new(100.0, 100.0);
let mut pod = recorder_pod(Point::new(40.0, 40.0), size);
pod.set_transform(Some(Affine::rotate_about(
FRAC_PI_4,
Point::new(50.0, 50.0),
)));
let (ops, _) = paint(&mut pod, None);
let [Op::Push(drawn), Op::Rect(rect_origin, rect_size), Op::Pop] = ops.as_slice() else {
panic!("expected push/rect/pop, got {ops:?}");
};
assert_eq!((*rect_origin, *rect_size), (Point::new(40.0, 40.0), size));
let rect = Rect::from_origin_size(*rect_origin, *rect_size);
let center = *drawn * rect.center();
for corner in [
Point::new(rect.x0, rect.y0),
Point::new(rect.x1, rect.y0),
Point::new(rect.x1, rect.y1),
Point::new(rect.x0, rect.y1),
] {
let painted = *drawn * corner;
let inward = (center - painted).normalize();
assert!(
pod.contains(painted + inward),
"just inside painted corner {painted:?}"
);
assert!(
!pod.contains(painted - inward),
"just outside painted corner {painted:?}"
);
let unrotated = corner + (rect.center() - corner) * 0.02;
assert!(!pod.contains(unrotated), "AABB corner {unrotated:?}");
}
}
#[test]
fn an_untransformed_pod_paints_and_routes_exactly_as_before() {
let origin = Point::new(12.0, 34.0);
let size = Size::new(60.0, 40.0);
let events = [
pointer(PointerPhase::Down, 20.0, 40.0),
pointer(PointerPhase::Move, 90.0, 10.0),
pointer(PointerPhase::Up, 30.0, 50.0),
InputEvent::Scroll {
position: Point::new(25.0, 45.0),
delta: ScrollDelta::Lines(0.0, 1.0),
},
InputEvent::Scale(ScaleEvent {
phase: ScalePhase::Begin,
scale_delta: 1.0,
focal: Point::new(15.0, 35.0),
velocity: 0.0,
}),
InputEvent::Housekeeping,
];
let visible = Some(Rect::new(0.0, 0.0, 50.0, 50.0));
let mut baseline = recorder_pod(origin, size);
let baseline_paint = paint(&mut baseline, visible);
for event in &events {
dispatch(&mut baseline, event);
}
let baseline_events = recorded(&mut baseline);
assert_eq!(baseline_paint.0, vec![Op::Rect(origin, size)]);
assert_eq!(baseline_paint.1, visible);
let translated: Vec<_> = events
.iter()
.map(|e| e.translated(-origin.to_vec2()))
.collect();
assert_eq!(baseline_events, translated);
let mut cleared = recorder_pod(origin, size);
cleared.set_transform(Some(Affine::rotate(1.0) * Affine::scale(3.0)));
cleared.set_transform(None);
assert_eq!(cleared.transform(), None);
assert_eq!(paint(&mut cleared, visible), baseline_paint);
for event in &events {
dispatch(&mut cleared, event);
}
assert_eq!(recorded(&mut cleared), baseline_events);
for point in [
Point::new(12.0, 34.0),
Point::new(71.9, 73.9),
Point::new(72.0, 50.0),
Point::new(11.9, 50.0),
] {
assert_eq!(cleared.contains(point), baseline.contains(point));
}
}
#[test]
fn a_singular_transform_hits_nothing_and_never_panics() {
let origin = Point::new(10.0, 10.0);
for singular in [
Affine::scale(0.0),
Affine::scale_non_uniform(2.0, 0.0),
Affine::new([1.0, 2.0, 2.0, 4.0, 0.0, 0.0]),
Affine::new([f64::NAN, 0.0, 0.0, 1.0, 0.0, 0.0]),
] {
let mut pod = recorder_pod(origin, Size::new(50.0, 50.0));
pod.set_transform(Some(singular));
for point in [origin, Point::new(20.0, 20.0), Point::new(10.0, 30.0)] {
assert!(!pod.contains(point), "{singular:?} hit {point:?}");
}
assert_eq!(
paint(&mut pod, Some(Rect::new(0.0, 0.0, 99.0, 99.0))).0,
vec![]
);
dispatch(&mut pod, &pointer(PointerPhase::Up, 20.0, 25.0));
assert_eq!(
recorded(&mut pod),
vec![pointer(PointerPhase::Up, 10.0, 15.0)]
);
let mut sem = SemanticsCtx::new(Size::new(200.0, 200.0), 2);
pod.semantics_child(&mut sem);
let update = sem.finish(accesskit::NodeId(1));
let bounds = update.nodes[0].1.bounds().expect("bounds");
assert!(
[bounds.x0, bounds.y0, bounds.x1, bounds.y1]
.iter()
.all(|v| v.is_finite()),
"{singular:?} gave {bounds:?}"
);
}
}
#[test]
fn a_transformed_pod_maps_the_visible_rect_and_reports_its_bounding_box() {
let mut pod = recorder_pod(Point::new(10.0, 20.0), Size::new(50.0, 50.0));
pod.set_transform(Some(Affine::scale(2.0)));
let visible = Some(Rect::new(10.0, 20.0, 110.0, 120.0));
let (ops, parent_visible) = paint(&mut pod, visible);
assert_eq!(parent_visible, visible);
assert_eq!(
ops,
vec![
Op::Push(
Affine::translate(Vec2::new(10.0, 20.0))
* Affine::scale(2.0)
* Affine::translate(Vec2::new(-10.0, -20.0))
),
Op::Rect(Point::new(10.0, 20.0), Size::new(50.0, 50.0)),
Op::Pop,
]
);
let seen = std::rc::Rc::new(Cell::new(None));
let mut probe = ChildPod::new(Box::new(VisibleProbe(seen.clone())));
probe.layout_child(
&mut LayoutCtx::new(),
&BoxConstraints::tight(Size::new(50.0, 50.0)),
);
probe.set_origin(Point::new(10.0, 20.0));
probe.set_transform(Some(Affine::scale(2.0)));
paint(&mut probe, visible);
assert_eq!(seen.get(), Some(Rect::new(10.0, 20.0, 60.0, 70.0)));
let mut sem = SemanticsCtx::new(Size::new(400.0, 400.0), 2);
pod.semantics_child(&mut sem);
let update = sem.finish(accesskit::NodeId(1));
let bounds = update.nodes[0].1.bounds().expect("bounds");
assert_eq!(
(bounds.x0, bounds.y0, bounds.x1, bounds.y1),
(10.0, 20.0, 110.0, 120.0)
);
}
}
#[cfg(test)]
mod contact_release_tests {
use super::*;
use crate::event::{ContactPass, PointerButton, PointerEvent, PointerId, PointerPhase};
struct Grab {
opt_in: bool,
}
impl Widget for Grab {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(20.0, 20.0))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if let InputEvent::Pointer(p) = event
&& p.phase == PointerPhase::Down
{
ctx.capture_pointer();
if self.opt_in {
ctx.capture_contacts();
}
}
EventResult::Handled
}
}
struct Taker {
inner: ChildPod,
}
impl Widget for Taker {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.inner.layout_child(ctx, bc);
bc.constrain(Size::new(20.0, 20.0))
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.inner.paint_child(ctx, scene);
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
match event {
InputEvent::Pointer(p)
if p.phase == PointerPhase::Move && self.inner.is_active() =>
{
ctx.release_captured_child(&mut self.inner);
EventResult::Handled
}
_ => self.inner.event_child(ctx, event),
}
}
}
fn nested(opt_in: bool) -> ChildPod {
let mut pod = ChildPod::new(Box::new(Taker {
inner: ChildPod::new(Box::new(Grab { opt_in })),
}));
pod.layout_child(
&mut LayoutCtx::new(),
&BoxConstraints::tight(Size::new(20.0, 20.0)),
);
pod
}
fn pointer(phase: PointerPhase) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase,
position: Point::new(5.0, 5.0),
button: PointerButton::Primary,
})
}
fn released(pod: &mut ChildPod, phase: PointerPhase) -> bool {
let mut unit = ();
let mut ctx = EventCtx::new(&mut unit, Point::ZERO, Size::new(20.0, 20.0));
pod.event_child(&mut ctx, &pointer(phase));
ctx.is_capture_released()
}
fn inner_active(pod: &mut ChildPod) -> bool {
pod.widget_mut()
.downcast_mut::<Taker>()
.expect("a Taker")
.inner
.is_active()
}
#[test]
fn releasing_the_opted_in_child_signals_and_bubbles() {
let mut pod = nested(true);
assert!(!released(&mut pod, PointerPhase::Down));
assert!(pod.holds_contact_opt_in(), "the opt-in below is recorded");
assert!(
released(&mut pod, PointerPhase::Move),
"and its release bubbles"
);
assert!(!inner_active(&mut pod), "the child left the active path");
assert!(pod.is_active(), "the container that took over keeps it");
}
#[test]
fn releasing_a_child_without_the_opt_in_clears_the_link_silently() {
let mut pod = nested(false);
released(&mut pod, PointerPhase::Down);
assert!(!pod.holds_contact_opt_in());
assert!(!released(&mut pod, PointerPhase::Move));
assert!(!inner_active(&mut pod));
}
#[test]
fn a_non_claimant_contact_cannot_release_anything() {
let mut pod = nested(true);
released(&mut pod, PointerPhase::Down);
let _pass = ContactPass::enter(PointerId::touch(1), true);
assert!(!released(&mut pod, PointerPhase::Move));
assert!(inner_active(&mut pod), "the claimant's link stands");
}
}