This commit is contained in:
Olivier FAURE 2024-10-22 15:38:34 +02:00
parent 9c397da91f
commit 54f83dbb6c
6 changed files with 187 additions and 39 deletions

View File

@ -182,7 +182,7 @@ impl From<PointerButton> for PointerButtons {
// TODO - Touchpad, Touch, AxisMotion // TODO - Touchpad, Touch, AxisMotion
// TODO - How to handle CursorEntered? // TODO - How to handle CursorEntered?
// Note to self: Events like "pointerenter", "pointerleave" are handled differently at the Widget level. But that's weird because WidgetPod can distribute them. Need to think about this again. // Note to self: Events like "pointerenter", "pointerleave" are handled differently at the Widget level. But that's weird because WidgetPod can distribute them. Need to think about this again.
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub enum PointerEvent { pub enum PointerEvent {
PointerDown(PointerButton, PointerState), PointerDown(PointerButton, PointerState),
PointerUp(PointerButton, PointerState), PointerUp(PointerButton, PointerState),
@ -198,7 +198,7 @@ pub enum PointerEvent {
// TODO - Clipboard Paste? // TODO - Clipboard Paste?
// TODO skip is_synthetic=true events // TODO skip is_synthetic=true events
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub enum TextEvent { pub enum TextEvent {
KeyboardKey(KeyEvent, ModifiersState), KeyboardKey(KeyEvent, ModifiersState),
Ime(Ime), Ime(Ime),
@ -207,13 +207,13 @@ pub enum TextEvent {
FocusChange(bool), FocusChange(bool),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct AccessEvent { pub struct AccessEvent {
pub action: accesskit::Action, pub action: accesskit::Action,
pub data: Option<accesskit::ActionData>, pub data: Option<accesskit::ActionData>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct PointerState { pub struct PointerState {
// TODO // TODO
// pub device_id: DeviceId, // pub device_id: DeviceId,
@ -242,7 +242,7 @@ pub enum WindowTheme {
/// may occur during an [`on_event`](crate::Widget::on_event) pass, if some /// may occur during an [`on_event`](crate::Widget::on_event) pass, if some
/// widget has been added then. /// widget has been added then.
#[non_exhaustive] #[non_exhaustive]
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
#[allow(variant_size_differences)] #[allow(variant_size_differences)]
pub enum Update { pub enum Update {
/// Sent to a `Widget` when it is added to the widget tree. This should be /// Sent to a `Widget` when it is added to the widget tree. This should be

View File

@ -64,3 +64,30 @@ pub(crate) fn run_update_anim_pass(root: &mut RenderRoot, elapsed_ns: u64) {
elapsed_ns, elapsed_ns,
); );
} }
// --- MARK: TESTS ---
#[cfg(test)]
mod tests {
use crate::testing::{Record, Recording, TestHarness, TestWidgetExt as _};
use crate::widget::SizedBox;
use crate::WidgetId;
#[test]
fn test_update_anim_pass() {
let record = Recording::default();
let id = WidgetId::next();
let widget = SizedBox::new_with_id(SizedBox::empty().record(&record), id);
let mut harness = TestHarness::create(widget);
record.clear();
harness.edit_widget(id, |mut widget| {
widget.ctx.request_anim_frame();
});
harness.animate_ms(20);
assert_eq!(record.next(), Some(Record::AnimFrame(20_000_000)));
}
}

View File

@ -93,3 +93,120 @@ pub(crate) fn run_compose_pass(root: &mut RenderRoot) {
Vec2::ZERO, Vec2::ZERO,
); );
} }
// --- MARK: TESTS ---
#[cfg(test)]
mod tests {
use smallvec::smallvec;
use vello::kurbo::{Point, Size};
use crate::testing::{
widget_ids, ModularWidget, Record, Recording, TestHarness, TestWidgetExt as _,
};
use crate::widget::SizedBox;
use crate::WidgetPod;
use super::*;
#[test]
fn test_compose_pass() {
let record = Recording::default();
let [parent_id, recorder_id] = widget_ids();
let inner = SizedBox::new_with_id(SizedBox::empty().record(&record), recorder_id);
let parent = ModularWidget::new((WidgetPod::new(inner), Point::ZERO, Vec2::ZERO))
.layout_fn(|state, ctx, bc| {
let (child, pos, _) = state;
ctx.run_layout(child, bc);
ctx.place_child(child, *pos);
Size::ZERO
})
.compose_fn(|state, ctx| {
let (child, _, translation) = state;
ctx.set_child_translation(child, *translation);
})
.register_children_fn(move |state, ctx| {
let (child, _, _) = state;
ctx.register_child(child);
})
.children_fn(|(child, _, _)| smallvec![child.id()]);
let root = SizedBox::new_with_id(parent, parent_id);
let mut harness = TestHarness::create(root);
record.clear();
harness.edit_widget(parent_id, |mut widget| {
// TODO - Find better way to express this
let mut widget = widget.downcast::<ModularWidget<(WidgetPod<SizedBox>, Point, Vec2)>>();
widget.widget.state.1 = Point::new(30., 30.);
widget.ctx.request_layout();
});
assert_eq!(
record.drain(),
vec![
Record::Layout(Size::new(400., 400.)),
Record::Compose(Point::new(30., 30.)),
]
);
harness.edit_widget(parent_id, |mut widget| {
// TODO - Find better way to express this
let mut widget = widget.downcast::<ModularWidget<(WidgetPod<SizedBox>, Point, Vec2)>>();
widget.widget.state.2 = Vec2::new(8., 8.);
widget.ctx.request_compose();
});
// TODO - Should changing a parent transform call the child's compose method?
assert_eq!(record.drain(), vec![]);
}
#[test]
fn test_move_text_input() {
let record = Recording::default();
let [parent_id, recorder_id] = widget_ids();
let inner = SizedBox::new_with_id(SizedBox::empty().record(&record), recorder_id);
let parent = ModularWidget::new((WidgetPod::new(inner), Point::ZERO, Vec2::ZERO))
.layout_fn(|state, ctx, bc| {
let (child, pos, _) = state;
ctx.run_layout(child, bc);
ctx.place_child(child, *pos);
Size::ZERO
})
.compose_fn(|state, ctx| {
let (child, _, translation) = state;
ctx.set_child_translation(child, *translation);
})
.register_children_fn(move |state, ctx| {
let (child, _, _) = state;
ctx.register_child(child);
})
.children_fn(|(child, _, _)| smallvec![child.id()]);
let root = SizedBox::new_with_id(parent, parent_id);
let mut harness = TestHarness::create(root);
record.clear();
harness.edit_widget(parent_id, |mut widget| {
// TODO - Find better way to express this
let mut widget = widget.downcast::<ModularWidget<(WidgetPod<SizedBox>, Point, Vec2)>>();
widget.widget.state.1 = Point::new(30., 30.);
widget.ctx.request_layout();
});
assert_eq!(
record.drain(),
vec![
Record::Layout(Size::new(400., 400.)),
Record::Compose(Point::new(30., 30.)),
]
);
harness.edit_widget(parent_id, |mut widget| {
// TODO - Find better way to express this
let mut widget = widget.downcast::<ModularWidget<(WidgetPod<SizedBox>, Point, Vec2)>>();
widget.widget.state.2 = Vec2::new(8., 8.);
widget.ctx.request_compose();
});
// TODO - Should changing a parent transform call the child's compose method?
assert_eq!(record.drain(), vec![]);
}
}

View File

@ -13,7 +13,7 @@ use std::collections::VecDeque;
use std::rc::Rc; use std::rc::Rc;
use accesskit::{NodeBuilder, Role}; use accesskit::{NodeBuilder, Role};
use smallvec::SmallVec; use smallvec::{smallvec, SmallVec};
use tracing::trace_span; use tracing::trace_span;
use vello::Scene; use vello::Scene;
use widget::widget::get_child_at_pos; use widget::widget::get_child_at_pos;
@ -43,7 +43,7 @@ pub const REPLACE_CHILD: Selector = Selector::new("masonry-test.replace-child");
/// ///
/// This widget is generic over its state, which is passed in at construction time. /// This widget is generic over its state, which is passed in at construction time.
pub struct ModularWidget<S> { pub struct ModularWidget<S> {
state: S, pub state: S,
accepts_pointer_interaction: bool, accepts_pointer_interaction: bool,
accepts_focus: bool, accepts_focus: bool,
accepts_text_input: bool, accepts_text_input: bool,
@ -102,18 +102,18 @@ pub struct Recording(Rc<RefCell<VecDeque<Record>>>);
/// A recording of a method call on a widget. /// A recording of a method call on a widget.
/// ///
/// Each member of the enum corresponds to one of the methods on `Widget`. /// Each member of the enum corresponds to one of the methods on `Widget`.
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub enum Record { pub enum Record {
PE(PointerEvent), PointerEvent(PointerEvent),
TE(TextEvent), TextEvent(TextEvent),
AE(AccessEvent), AccessEvent(AccessEvent),
AF(u64), AnimFrame(u64),
RC, RegisterChildren,
U(Update), Update(Update),
Layout(Size), Layout(Size),
Compose, Compose(Point),
Paint, Paint,
Access, Accessibilty,
} }
/// External trait implemented for all widgets. /// External trait implemented for all widgets.
@ -161,6 +161,22 @@ impl<S> ModularWidget<S> {
} }
} }
impl<W: Widget> ModularWidget<WidgetPod<W>> {
pub fn new_parent(child: W) -> ModularWidget<WidgetPod<W>> {
let child = WidgetPod::new(child);
ModularWidget::new(child)
.register_children_fn(move |child, ctx| {
ctx.register_child(child);
})
.layout_fn(move |child, ctx, bc| {
let size = ctx.run_layout(child, bc);
ctx.place_child(child, Point::ZERO);
size
})
.children_fn(|child| smallvec![child.id()])
}
}
/// Builder methods. /// Builder methods.
/// ///
/// Each method takes a flag which is then returned by the matching Widget method. /// Each method takes a flag which is then returned by the matching Widget method.
@ -508,32 +524,32 @@ impl Recording {
#[warn(clippy::missing_trait_methods)] #[warn(clippy::missing_trait_methods)]
impl<W: Widget> Widget for Recorder<W> { impl<W: Widget> Widget for Recorder<W> {
fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &event::PointerEvent) { fn on_pointer_event(&mut self, ctx: &mut EventCtx, event: &event::PointerEvent) {
self.recording.push(Record::PE(event.clone())); self.recording.push(Record::PointerEvent(event.clone()));
self.child.on_pointer_event(ctx, event); self.child.on_pointer_event(ctx, event);
} }
fn on_text_event(&mut self, ctx: &mut EventCtx, event: &event::TextEvent) { fn on_text_event(&mut self, ctx: &mut EventCtx, event: &event::TextEvent) {
self.recording.push(Record::TE(event.clone())); self.recording.push(Record::TextEvent(event.clone()));
self.child.on_text_event(ctx, event); self.child.on_text_event(ctx, event);
} }
fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) { fn on_access_event(&mut self, ctx: &mut EventCtx, event: &AccessEvent) {
self.recording.push(Record::AE(event.clone())); self.recording.push(Record::AccessEvent(event.clone()));
self.child.on_access_event(ctx, event); self.child.on_access_event(ctx, event);
} }
fn on_anim_frame(&mut self, ctx: &mut UpdateCtx, interval: u64) { fn on_anim_frame(&mut self, ctx: &mut UpdateCtx, interval: u64) {
self.recording.push(Record::AF(interval)); self.recording.push(Record::AnimFrame(interval));
self.child.on_anim_frame(ctx, interval); self.child.on_anim_frame(ctx, interval);
} }
fn register_children(&mut self, ctx: &mut RegisterCtx) { fn register_children(&mut self, ctx: &mut RegisterCtx) {
self.recording.push(Record::RC); self.recording.push(Record::RegisterChildren);
self.child.register_children(ctx); self.child.register_children(ctx);
} }
fn update(&mut self, ctx: &mut UpdateCtx, event: &Update) { fn update(&mut self, ctx: &mut UpdateCtx, event: &Update) {
self.recording.push(Record::U(event.clone())); self.recording.push(Record::Update(event.clone()));
self.child.update(ctx, event); self.child.update(ctx, event);
} }
@ -544,7 +560,7 @@ impl<W: Widget> Widget for Recorder<W> {
} }
fn compose(&mut self, ctx: &mut ComposeCtx) { fn compose(&mut self, ctx: &mut ComposeCtx) {
self.recording.push(Record::Compose); self.recording.push(Record::Compose(ctx.window_origin()));
self.child.compose(ctx); self.child.compose(ctx);
} }
@ -558,7 +574,7 @@ impl<W: Widget> Widget for Recorder<W> {
} }
fn accessibility(&mut self, ctx: &mut AccessCtx, node: &mut NodeBuilder) { fn accessibility(&mut self, ctx: &mut AccessCtx, node: &mut NodeBuilder) {
self.recording.push(Record::Access); self.recording.push(Record::Accessibilty);
self.child.accessibility(ctx, node); self.child.accessibility(ctx, node);
} }

View File

@ -1,24 +1,12 @@
// Copyright 2022 the Xilem Authors // Copyright 2022 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use smallvec::smallvec;
use crate::testing::{ModularWidget, TestHarness, TestWidgetExt}; use crate::testing::{ModularWidget, TestHarness, TestWidgetExt};
use crate::widget::Flex; use crate::widget::Flex;
use crate::{Point, PointerButton, Size, Update, Widget, WidgetId, WidgetPod}; use crate::{Point, PointerButton, Size, Update, Widget, WidgetId, WidgetPod};
fn make_parent_widget<W: Widget>(child: W) -> ModularWidget<WidgetPod<W>> { fn make_parent_widget<W: Widget>(child: W) -> ModularWidget<WidgetPod<W>> {
let child = WidgetPod::new(child); ModularWidget::new_parent(child)
ModularWidget::new(child)
.register_children_fn(move |child, ctx| {
ctx.register_child(child);
})
.layout_fn(move |child, ctx, bc| {
let size = ctx.run_layout(child, bc);
ctx.place_child(child, Point::ZERO);
size
})
.children_fn(|child| smallvec![child.id()])
} }
#[cfg(FALSE)] #[cfg(FALSE)]

View File

@ -11,7 +11,7 @@ use crate::*;
fn next_pointer_event(recording: &Recording) -> Option<PointerEvent> { fn next_pointer_event(recording: &Recording) -> Option<PointerEvent> {
while let Some(event) = recording.next() { while let Some(event) = recording.next() {
match event { match event {
Record::PE(event) => { Record::PointerEvent(event) => {
return Some(event); return Some(event);
} }
_ => {} _ => {}
@ -27,7 +27,7 @@ fn is_hovered(harness: &TestHarness, id: WidgetId) -> bool {
fn next_hovered_changed(recording: &Recording) -> Option<bool> { fn next_hovered_changed(recording: &Recording) -> Option<bool> {
while let Some(event) = recording.next() { while let Some(event) = recording.next() {
match event { match event {
Record::U(Update::HoveredChanged(hovered)) => return Some(hovered), Record::Update(Update::HoveredChanged(hovered)) => return Some(hovered),
_ => {} _ => {}
} }
} }