use std::any::Any;
use kurbo::{Point, Size};
use reactive_graph::owner::Owner;
use crate::event::{EventCtx, EventResult, InputEvent, PointerPhase};
use crate::layout::BoxConstraints;
use crate::view::{AnyView, BuildCtx, ChangeFlags, View};
use crate::widget::{ChildPod, LayoutCtx, PaintCtx, PaintScene, Widget};
pub trait Component: 'static {
type State: 'static;
fn init(&self) -> Self::State;
fn build(&self, state: &mut Self::State) -> AnyView<Self::State>;
}
pub struct ComponentView<C: Component> {
component: C,
}
pub fn component<C: Component>(c: C) -> ComponentView<C> {
ComponentView { component: c }
}
pub struct ComponentWidget<C: Component> {
state: C::State,
prev: AnyView<C::State>,
child: ChildPod,
owner: Owner,
next_id: u64,
disposed: bool,
}
impl<C: Component> ComponentWidget<C> {
fn dispose(&mut self) {
if !self.disposed {
self.owner.cleanup();
self.disposed = true;
}
}
}
impl<C: Component> Drop for ComponentWidget<C> {
fn drop(&mut self) {
self.dispose();
}
}
impl<Outer: 'static, C: Component> View<Outer> for ComponentView<C> {
type Element = ComponentWidget<C>;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> Self::Element {
let owner = Owner::new();
let (state, prev, child, next_id) = owner.with(|| {
let mut state = self.component.init();
let view = self.component.build(&mut state);
let mut next_id = 0u64;
let mut inner = BuildCtx::new(&mut next_id);
inner.set_has_focus(false);
let element: Box<dyn Widget> = view.build(&mut inner);
(state, view, ChildPod::new(Box::new(element)), next_id)
});
ComponentWidget {
state,
prev,
child,
owner,
next_id,
disposed: false,
}
}
fn rebuild(
&self,
_prev: &Self,
element: &mut Self::Element,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let outer_has_focus = ctx.has_focus();
let owner = element.owner.clone();
owner.with(|| {
let new_view = self.component.build(&mut element.state);
let child_focused = element.child.is_focused();
let (flags, swapped) = {
let boxed = element
.child
.widget_mut()
.downcast_mut::<Box<dyn Widget>>()
.expect("component child element is a boxed AnyView widget");
let before = {
let any: &dyn Any = &**boxed;
any.type_id()
};
let mut inner = BuildCtx::new(&mut element.next_id);
inner.set_has_focus(outer_has_focus && child_focused);
let flags = new_view.rebuild(&element.prev, boxed, &mut inner);
let after = {
let any: &dyn Any = &**boxed;
any.type_id()
};
(flags, before != after)
};
if swapped {
element.child.set_active(false);
element.child.set_focused(false);
if outer_has_focus && child_focused {
crate::event::mark_focus_orphaned();
}
}
element.prev = new_view;
flags
})
}
fn teardown(&self, element: &mut Self::Element, ctx: &mut BuildCtx<'_>) {
let inner_has_focus = ctx.has_focus() && element.child.is_focused();
element.owner.clone().with(|| {
let mut next_id = element.next_id;
let mut inner = BuildCtx::new(&mut next_id);
inner.set_has_focus(inner_has_focus);
if let Some(boxed) = element.child.widget_mut().downcast_mut::<Box<dyn Widget>>() {
element.prev.teardown(boxed, &mut inner);
}
});
element.dispose();
}
}
impl<C: Component> Widget for ComponentWidget<C> {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.child.set_origin(Point::ZERO);
self.child.layout_child(ctx, bc)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.child.paint_child(ctx, scene);
}
fn semantics(&self, ctx: &mut crate::semantics::SemanticsCtx) {
self.child.semantics_child(ctx);
}
fn visit_children(&self, visitor: &mut dyn FnMut(&ChildPod)) {
visitor(&self.child);
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
let size = ctx.size();
let has_focus = ctx.has_focus();
let hovered = ctx.is_hovered();
let hover_epoch = ctx.hover_epoch();
let hover_eligible = ctx.is_hover_eligible();
let (result, needs_redraw, captured, hover_claimed, focus_req, focus_rel, ime) = {
let state_any: &mut dyn Any = &mut self.state;
let mut inner = EventCtx::new(state_any, Point::ZERO, size);
inner.set_has_focus(has_focus);
inner.set_hovered(hovered);
inner.set_hover_epoch(hover_epoch);
inner.set_hover_eligible(hover_eligible);
let result = route_child(&mut self.child, &mut inner, event);
(
result,
inner.needs_redraw(),
inner.is_pointer_captured(),
inner.is_hover_claimed(),
inner.is_focus_requested(),
inner.is_focus_released(),
inner.take_ime_state(),
)
};
if needs_redraw {
ctx.request_redraw();
}
if captured {
ctx.capture_pointer();
}
if hover_claimed {
ctx.claim_hover();
}
if focus_req {
ctx.request_focus();
}
if focus_rel {
ctx.release_focus();
}
if let Some(ime) = ime {
ctx.publish_ime_state(ime);
}
result
}
}
fn route_child(pod: &mut ChildPod, ctx: &mut EventCtx<'_>, event: &InputEvent) -> EventResult {
if event.is_broadcast() {
pod.event_child(ctx, event);
return EventResult::Ignored;
}
if event.is_focus_routed() {
if pod.is_focused() {
return pod.event_child(ctx, event);
}
return EventResult::Ignored;
}
if pod.is_active() {
let result = pod.event_child(ctx, event);
if releases_capture(event) {
pod.set_active(false);
}
return result;
}
let inside = pod.contains(event.position());
let result = if inside {
pod.event_child(ctx, event)
} else {
EventResult::Ignored
};
if is_pointer_down(event) && !inside && pod.is_focused() {
pod.set_focused(false);
}
result
}
fn releases_capture(event: &InputEvent) -> bool {
matches!(
event,
InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Up | PointerPhase::Cancel)
)
}
fn is_pointer_down(event: &InputEvent) -> bool {
matches!(
event,
InputEvent::Pointer(p) if matches!(p.phase, PointerPhase::Down)
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{
CursorIcon, EditingState, ImeState, Key, KeyEvent, Modifiers, NamedKey, PointerButton,
PointerEvent, clear_cursor_request, take_cursor_request,
};
use crate::view::any;
use kurbo::Rect;
use reactive_graph::owner::{on_cleanup, provide_context, use_context};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::{Arc, Mutex};
fn pointer(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase,
position: Point::new(x, y),
button: PointerButton::Primary,
})
}
fn key_enter() -> InputEvent {
InputEvent::Key(KeyEvent {
key: Key::Named(NamedKey::Enter),
modifiers: Modifiers::default(),
repeat: false,
})
}
struct Empty;
struct EmptyWidget;
impl Widget for EmptyWidget {
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) {}
}
impl View<()> for Empty {
type Element = EmptyWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> EmptyWidget {
EmptyWidget
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut EmptyWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
struct OtherLeaf;
struct OtherWidget;
impl Widget for OtherWidget {
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) {}
}
impl View<()> for OtherLeaf {
type Element = OtherWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> OtherWidget {
OtherWidget
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut OtherWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
struct CounterState {
count: u32,
}
struct ClickCounter {
label: &'static str,
}
impl Component for ClickCounter {
type State = CounterState;
fn init(&self) -> CounterState {
CounterState { count: 0 }
}
fn build(&self, state: &mut CounterState) -> AnyView<CounterState> {
any(CounterButtonView {
count: state.count,
label: self.label,
})
}
}
struct CounterButtonView {
count: u32,
label: &'static str,
}
struct CounterButtonWidget {
count: u32,
label: &'static str,
}
impl View<CounterState> for CounterButtonView {
type Element = CounterButtonWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> CounterButtonWidget {
CounterButtonWidget {
count: self.count,
label: self.label,
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut CounterButtonWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
if prev.count != self.count || prev.label != self.label {
element.count = self.count;
element.label = self.label;
flags = ChangeFlags::PAINT;
}
flags
}
}
impl Widget for CounterButtonWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(40.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.state_mut::<CounterState>().count += 1;
ctx.request_redraw();
return EventResult::Handled;
}
EventResult::Ignored
}
}
fn ambient() -> Owner {
let owner = Owner::new();
owner.set();
owner
}
fn build_widget<Outer: 'static, C: Component>(view: &ComponentView<C>) -> ComponentWidget<C> {
let mut next_id = 0u64;
let mut ctx = BuildCtx::new(&mut next_id);
View::<Outer>::build(view, &mut ctx)
}
#[test]
fn local_state_survives_parent_driven_rebuild() {
let _owner = ambient();
let v1 = component(ClickCounter { label: "a" });
let mut widget = build_widget::<(), _>(&v1);
let mut lctx = LayoutCtx::new();
widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 20.0)));
let mut outer = ();
for _ in 0..3 {
let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 20.0));
widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
}
assert_eq!(widget.state.count, 3);
let v2 = component(ClickCounter { label: "changed" });
let mut next_id = 0u64;
let mut ctx = BuildCtx::new(&mut next_id);
View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
assert_eq!(widget.state.count, 3, "local state retained across rebuild");
}
#[derive(Clone)]
struct Theme(i32);
struct Provider {
sink: Arc<Mutex<Option<i32>>>,
}
impl Component for Provider {
type State = ();
fn init(&self) {}
fn build(&self, _state: &mut ()) -> AnyView<()> {
provide_context(Theme(7));
any(component(Reader {
sink: self.sink.clone(),
}))
}
}
struct Reader {
sink: Arc<Mutex<Option<i32>>>,
}
impl Component for Reader {
type State = ();
fn init(&self) {}
fn build(&self, _state: &mut ()) -> AnyView<()> {
*self.sink.lock().unwrap() = use_context::<Theme>().map(|t| t.0);
any(Empty)
}
}
struct SecretProvider;
impl Component for SecretProvider {
type State = ();
fn init(&self) {}
fn build(&self, _state: &mut ()) -> AnyView<()> {
provide_context(Theme(99));
any(Empty)
}
}
#[test]
fn use_context_resolves_ancestor_provided_value() {
let _owner = ambient();
let sink = Arc::new(Mutex::new(None));
let v = component(Provider { sink: sink.clone() });
let _widget = build_widget::<(), _>(&v);
assert_eq!(*sink.lock().unwrap(), Some(7));
}
#[test]
fn sibling_does_not_see_a_cousins_context() {
let parent = ambient();
let reader_sink = Arc::new(Mutex::new(Some(-1)));
parent.with(|| {
let secret = component(SecretProvider);
let _s = build_widget::<(), _>(&secret);
let reader = component(Reader {
sink: reader_sink.clone(),
});
let _r = build_widget::<(), _>(&reader);
});
assert_eq!(*reader_sink.lock().unwrap(), None);
}
struct Disposable {
probe: Arc<AtomicU32>,
}
impl Component for Disposable {
type State = ();
fn init(&self) {
let probe = self.probe.clone();
on_cleanup(move || {
probe.fetch_add(1, Ordering::SeqCst);
});
}
fn build(&self, _state: &mut ()) -> AnyView<()> {
any(Empty)
}
}
#[test]
fn anyview_type_swap_disposes_component_owner_once() {
let _owner = ambient();
let probe = Arc::new(AtomicU32::new(0));
let mut next_id = 0u64;
let mut ctx = BuildCtx::new(&mut next_id);
let prev = any::<(), _>(component(Disposable {
probe: probe.clone(),
}));
let mut element: Box<dyn Widget> = prev.build(&mut ctx);
assert_eq!(probe.load(Ordering::SeqCst), 0);
let next = any::<(), _>(OtherLeaf);
next.rebuild(&prev, &mut element, &mut ctx);
assert_eq!(probe.load(Ordering::SeqCst), 1);
drop(element);
assert_eq!(probe.load(Ordering::SeqCst), 1);
}
struct SwapComp {
swapped: Rc<Cell<bool>>,
}
impl Component for SwapComp {
type State = ();
fn init(&self) {}
fn build(&self, _state: &mut ()) -> AnyView<()> {
if self.swapped.get() {
any(OtherLeaf)
} else {
any(Empty)
}
}
}
#[test]
fn component_swap_arm_marks_the_orphan_only_on_a_live_chain() {
fn swap_child(outer_live: bool) -> (bool, bool) {
let _ = crate::event::take_focus_orphaned();
let _owner = ambient();
let swapped = Rc::new(Cell::new(false));
let v1 = component(SwapComp {
swapped: swapped.clone(),
});
let mut widget = build_widget::<(), _>(&v1);
widget.child.set_focused(true);
swapped.set(true);
let v2 = component(SwapComp {
swapped: swapped.clone(),
});
let mut next_id = 0u64;
let mut ctx = BuildCtx::new(&mut next_id);
ctx.set_has_focus(outer_live);
View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
(
widget.child.is_focused(),
crate::event::take_focus_orphaned(),
)
}
let (still_focused, marked) = swap_child(true);
assert!(
!still_focused,
"a swapped-in widget must not inherit the old focus link"
);
assert!(
marked,
"a live focus path dying inside the component's swap releases the session"
);
let (still_focused, marked) = swap_child(false);
assert!(!still_focused, "the flag is dropped either way");
assert!(
!marked,
"the same swap under an already-blurred ancestor owns no session to release"
);
}
#[test]
fn component_content_only_rebuild_keeps_focus_and_marks_nothing() {
let _ = crate::event::take_focus_orphaned();
let _owner = ambient();
let v1 = component(Labeled { text: "a" });
let mut widget = build_widget::<(), _>(&v1);
widget.child.set_focused(true);
let v2 = component(Labeled { text: "b" });
let mut next_id = 0u64;
let mut ctx = BuildCtx::new(&mut next_id);
View::<()>::rebuild(&v2, &v1, &mut widget, &mut ctx);
assert!(
widget.child.is_focused(),
"a content-only rebuild keeps the recorded focus path"
);
assert!(
!crate::event::take_focus_orphaned(),
"an in-place child rebuild must not orphan the focus session"
);
}
#[test]
fn explicit_teardown_disposes_owner_once_and_drop_is_idempotent() {
let _owner = ambient();
let probe = Arc::new(AtomicU32::new(0));
let view = component(Disposable {
probe: probe.clone(),
});
let mut widget = build_widget::<(), _>(&view);
let mut next_id = 0u64;
let mut ctx = BuildCtx::new(&mut next_id);
View::<()>::teardown(&view, &mut widget, &mut ctx);
assert_eq!(probe.load(Ordering::SeqCst), 1);
drop(widget);
assert_eq!(probe.load(Ordering::SeqCst), 1);
}
struct CaptureWidget {
pressed: bool,
}
impl Widget for CaptureWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(40.0, 40.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 {
match p.phase {
PointerPhase::Down => {
self.pressed = true;
ctx.capture_pointer();
ctx.request_redraw();
return EventResult::Handled;
}
PointerPhase::Move => return EventResult::Handled,
PointerPhase::Up | PointerPhase::Cancel => {
self.pressed = false;
return EventResult::Handled;
}
}
}
EventResult::Ignored
}
}
struct CaptureView;
impl View<()> for CaptureView {
type Element = CaptureWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> CaptureWidget {
CaptureWidget { pressed: false }
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut CaptureWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
struct CaptureComp;
impl Component for CaptureComp {
type State = ();
fn init(&self) {}
fn build(&self, _state: &mut ()) -> AnyView<()> {
any(CaptureView)
}
}
fn inner_widget<C: Component, W: Widget>(w: &mut ComponentWidget<C>) -> &mut W {
w.child
.widget_mut()
.downcast_mut::<Box<dyn Widget>>()
.expect("double-boxed AnyView element")
.downcast_mut::<W>()
.expect("inner concrete widget")
}
#[test]
fn capture_survives_drag_outside_bounds_and_mirrors_outward() {
let _owner = ambient();
let view = component(CaptureComp);
let mut widget = build_widget::<(), _>(&view);
let mut lctx = LayoutCtx::new();
widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
let mut outer = ();
let captured = {
let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
ectx.is_pointer_captured()
};
assert!(captured, "inner capture bubbles to the outer context");
assert!(
widget.child.is_active(),
"component pod records the capture"
);
{
let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
let r = widget.event(&mut ectx, &pointer(PointerPhase::Move, 500.0, 500.0));
assert_eq!(r, EventResult::Handled, "drag outside bounds still routed");
}
assert!(widget.child.is_active(), "capture survives the drag");
{
let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
widget.event(&mut ectx, &pointer(PointerPhase::Up, 500.0, 500.0));
}
assert!(!widget.child.is_active(), "Up auto-releases the capture");
assert!(!inner_widget::<_, CaptureWidget>(&mut widget).pressed);
}
#[test]
fn synthesized_cancel_over_unit_state_is_safe_and_clears_pressed() {
let _owner = ambient();
let view = component(CaptureComp);
let mut widget = build_widget::<(), _>(&view);
let mut lctx = LayoutCtx::new();
widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
let mut outer = ();
{
let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
widget.event(&mut ectx, &pointer(PointerPhase::Down, 5.0, 5.0));
}
assert!(inner_widget::<_, CaptureWidget>(&mut widget).pressed);
let mut dummy = ();
{
let mut ectx = EventCtx::new(&mut dummy, Point::ZERO, Size::new(40.0, 40.0));
widget.event(&mut ectx, &pointer(PointerPhase::Cancel, 0.0, 0.0));
}
assert!(
!inner_widget::<_, CaptureWidget>(&mut widget).pressed,
"Cancel cleared pressed across the boundary"
);
assert!(!widget.child.is_active(), "Cancel released the capture");
}
struct CursorLeafWidget;
impl Widget for CursorLeafWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(40.0, 40.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::Move
{
ctx.set_cursor(CursorIcon::Text);
}
EventResult::Ignored
}
}
struct CursorLeafView;
impl View<()> for CursorLeafView {
type Element = CursorLeafWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> CursorLeafWidget {
CursorLeafWidget
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut CursorLeafWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
struct CursorComp;
impl Component for CursorComp {
type State = ();
fn init(&self) {}
fn build(&self, _state: &mut ()) -> AnyView<()> {
any(CursorLeafView)
}
}
#[test]
fn a_cursor_request_crosses_the_component_boundary() {
let _owner = ambient();
let view = component(CursorComp);
let mut widget = build_widget::<(), _>(&view);
let mut lctx = LayoutCtx::new();
widget.layout(&mut lctx, &BoxConstraints::tight(Size::new(40.0, 40.0)));
clear_cursor_request();
let mut outer = ();
{
let mut ectx = EventCtx::new(&mut outer, Point::ZERO, Size::new(40.0, 40.0));
widget.event(&mut ectx, &pointer(PointerPhase::Move, 5.0, 5.0));
}
assert_eq!(
take_cursor_request(),
Some(CursorIcon::Text),
"a request from below the component boundary reaches the root"
);
}
struct ImeLeafWidget;
impl Widget for ImeLeafWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(100.0, 100.0))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
match event {
InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
if p.position.x < 50.0 {
ctx.request_focus();
ctx.publish_ime_state(ImeState {
active: true,
editing: EditingState {
text: "abc".to_string(),
selection_base: 3,
selection_extent: 3,
composing_base: -1,
composing_extent: -1,
},
caret: Some(Rect::new(0.0, 0.0, 1.0, 12.0)),
content_type: Default::default(),
suppress_soft_keyboard: false,
});
}
EventResult::Handled
}
InputEvent::Key(_) | InputEvent::Ime(_) => EventResult::Handled,
_ => EventResult::Ignored,
}
}
}
struct ImeLeafView;
impl View<()> for ImeLeafView {
type Element = ImeLeafWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeLeafWidget {
ImeLeafWidget
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut ImeLeafWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
struct ImeComp;
impl Component for ImeComp {
type State = ();
fn init(&self) {}
fn build(&self, _state: &mut ()) -> AnyView<()> {
any(ImeLeafView)
}
}
#[test]
fn focus_ime_and_blur_surface_through_render_root() {
use crate::app::RenderRoot;
let _owner = ambient();
let mut root: RenderRoot<(), ComponentView<ImeComp>> = RenderRoot::new();
let mut state = ();
root.rebuild(&mut |_s: &mut ()| component(ImeComp), &mut state);
root.layout(Size::new(100.0, 100.0));
assert!(!root.is_focus_active());
assert!(root.ime_state().is_none());
root.event(&mut state, &pointer(PointerPhase::Down, 10.0, 10.0));
assert!(root.is_focus_active(), "focus request crossed the boundary");
let ime = root.ime_state().expect("IME surface surfaced at the root");
assert!(ime.active);
assert_eq!(ime.editing.text, "abc");
let outcome = root.event(&mut state, &key_enter());
assert!(outcome.handled, "Key routed to the focused inner leaf");
root.event(&mut state, &pointer(PointerPhase::Down, 80.0, 10.0));
assert!(!root.is_focus_active(), "blur crossed the boundary");
assert!(root.ime_state().is_none());
}
struct TextLeafView {
text: &'static str,
}
struct TextLeafWidget {
text: &'static str,
}
impl View<()> for TextLeafView {
type Element = TextLeafWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> TextLeafWidget {
TextLeafWidget { text: self.text }
}
fn rebuild(
&self,
prev: &Self,
element: &mut TextLeafWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
if prev.text != self.text {
element.text = self.text;
ChangeFlags::PAINT
} else {
ChangeFlags::NONE
}
}
}
impl Widget for TextLeafWidget {
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) {}
}
struct Labeled {
text: &'static str,
}
impl Component for Labeled {
type State = ();
fn init(&self) {}
fn build(&self, _state: &mut ()) -> AnyView<()> {
any(TextLeafView { text: self.text })
}
}
#[test]
fn child_paint_flags_bubble_out_of_component_rebuild() {
let _owner = ambient();
let v1 = component(Labeled { text: "a" });
let mut widget = build_widget::<(), _>(&v1);
let v_same = component(Labeled { text: "a" });
let mut next_id = 0u64;
let mut ctx = BuildCtx::new(&mut next_id);
let flags = View::<()>::rebuild(&v_same, &v1, &mut widget, &mut ctx);
assert_eq!(flags, ChangeFlags::NONE);
let v2 = component(Labeled { text: "b" });
let flags = View::<()>::rebuild(&v2, &v_same, &mut widget, &mut ctx);
assert_eq!(flags, ChangeFlags::PAINT);
}
}