Skip to main content

cranpose_ui/
lib.rs

1//! High level UI primitives built on top of the Compose core runtime.
2
3#![deny(unsafe_code)]
4
5use cranpose_core::{location_key, ApplierGuard, MemoryApplier, NodeError, NodeId, RuntimeHandle};
6pub use cranpose_core::{Composition, Key};
7pub use cranpose_macros::composable;
8use std::ops::{Deref, DerefMut};
9use std::rc::Rc;
10
11pub mod bring_into_view;
12pub mod clipboard_session;
13mod cursor_animation;
14mod debug;
15mod draw;
16pub mod fling_animation;
17mod focus_dispatch;
18pub mod font_scale;
19mod interaction;
20mod key_event;
21pub mod layout;
22mod modifier;
23mod modifier_nodes;
24mod pointer_dispatch;
25mod primitives;
26mod render_state;
27mod renderer;
28pub mod round_scaling_list;
29pub mod round_scroll_indicator;
30pub mod safe_area;
31pub mod scroll;
32mod semantics_dispatch;
33mod subcompose_layout;
34pub mod text;
35pub mod text_field_focus;
36mod text_field_handler;
37mod text_field_input;
38mod text_field_modifier_node;
39pub mod text_input_session;
40pub mod text_layout_result;
41mod text_modifier_node;
42pub mod text_selection;
43pub mod widgets;
44mod word_boundaries;
45pub mod zoom;
46
47// Export for cursor blink animation - AppShell checks this to continuously redraw
48pub use text_field_focus::has_focused_field;
49// Editable-state snapshot for platform IMEs (Android InputConnection, web
50// composition) - platform runtimes read it through the shell
51pub use text_field_focus::ImeEditorState;
52// Platform soft-keyboard bridge - platform runtimes install a handler so text
53// field focus changes can show/hide the on-screen keyboard
54pub use text_input_session::PlatformTextInputHandler;
55// Export cursor blink timing for WaitUntil scheduling
56pub use cursor_animation::{
57    is_cursor_visible, next_cursor_blink_time, reset_cursor_blink, start_cursor_blink,
58    stop_cursor_blink, tick_cursor_blink,
59};
60
61pub use bring_into_view::{
62    local_bring_into_view_responder, scroll_delta_to_reveal, BringIntoViewResponder,
63};
64pub use cranpose_ui_graphics::{BlurredEdgeTreatment, ColorFilter, Dp, ImageBitmap, ImageSampling};
65pub use cranpose_ui_layout::IntrinsicSize;
66pub use draw::{
67    command_draw_scope, command_draw_scope_retained, command_draw_scope_reusing,
68    execute_draw_commands, DrawCacheBuilder, DrawCommand, DrawCommandFn,
69};
70pub use focus_dispatch::{
71    active_focus_target, clear_focus_invalidations, has_pending_focus_invalidations,
72    process_focus_invalidations, schedule_focus_invalidation, set_active_focus_target,
73};
74pub use interaction::{
75    collect_is_pressed_as_state, rememberMutableInteractionSource, Interaction,
76    MutableInteractionSource, PressInteraction, PressInteractionCancel, PressInteractionPress,
77    PressInteractionRelease,
78};
79pub use safe_area::{local_ime_insets, local_safe_area_insets};
80// Re-export FocusManager from cranpose-foundation to avoid duplication
81pub use cranpose_foundation::nodes::input::focus::FocusManager;
82pub use cranpose_foundation::{
83    DelegatableNode, ModifierNode, ModifierNodeElement, NodeCapabilities, NodeState,
84};
85pub use layout::{
86    build_layout_tree_from_applier, build_semantics_tree_from_applier,
87    build_semantics_tree_from_layout_tree,
88    core::{
89        Alignment, Arrangement, HorizontalAlignment, LinearArrangement, Measurable, Placeable,
90        VerticalAlignment,
91    },
92    measure_layout, measure_layout_with_options, tree_needs_layout, tree_needs_semantics,
93    LayoutAllocationDebugStats, LayoutBox, LayoutEngine, LayoutMeasurements, LayoutNodeData,
94    LayoutNodeKind, LayoutTree, MeasureLayoutOptions, SemanticsAction, SemanticsCallback,
95    SemanticsNode, SemanticsRole, SemanticsTree,
96};
97// The accessibility vocabulary an app writes against. It is declared in
98// cranpose-foundation, next to `SemanticsConfiguration`, but an app composes
99// against cranpose-ui and should not have to reach past it to describe a
100// button.
101pub use cranpose_foundation::{
102    CanvasSemanticsNode, SemanticsConfiguration, SemanticsCustomAction, SemanticsWidgetRole,
103};
104pub use modifier::{
105    collect_modifier_slices, collect_semantics_from_modifier, collect_slices_from_modifier,
106    BlendMode, Brush, Color, CompositingStrategy, CornerRadii, DpOffset, EdgeInsets,
107    FocusDirection, FocusRequester, GlassMaterial, GraphicsLayer, LayerShape, Modifier,
108    ModifierNodeSlices, ModifierNodeSlicesDebugStats, Point, PointerEvent, PointerEventKind,
109    PointerInputScope, PointerSource, Rect, RenderEffect, ResolvedBackground, ResolvedModifiers,
110    RotaryInputModifierNode, RotaryScrollEvent, RoundedCornerShape, RuntimeShader,
111    SemanticsRequester, Shadow, ShadowScope, Size, TransformOrigin,
112};
113pub use modifier_nodes::{
114    AlphaElement, AlphaNode, BackgroundElement, BackgroundNode, ClickableElement, ClickableNode,
115    CornerShapeElement, CornerShapeNode, FillDirection, FillElement, FillNode,
116    FractionalOffsetElement, FractionalOffsetNode, OffsetElement, OffsetNode, PaddingElement,
117    PaddingNode, SizeElement, SizeNode,
118};
119pub use pointer_dispatch::{
120    clear_pointer_repasses, has_pending_pointer_repasses, process_pointer_repasses,
121    schedule_pointer_repass,
122};
123pub use primitives::{
124    fade_in, fade_out, remember_svg, slide_in_vertically, slide_out_vertically, AnimatedVisibility,
125    BasicText, BasicTextField, BasicTextFieldOptions, BasicTextFieldWithOptions,
126    BasicTextWithOptions, BitmapPainter, Box, BoxScope, BoxSpec, BoxWithConstraints,
127    BoxWithConstraintsScope, BoxWithConstraintsScopeImpl, Button, ButtonSpec, Canvas, Column,
128    ColumnSpec, ContentScale, Crossfade, EnterTransition, ExitTransition, ForEach, Image, Layout,
129    LayoutNode, Painter, Row, RowSpec, Spacer, SubcomposeLayout, SvgPainter, SvgPainterError, Text,
130    TextWithOptions, DEFAULT_ALPHA,
131};
132// Lazy list exports - single source from cranpose-foundation
133pub use cranpose_foundation::lazy::{LazyListItemInfo, LazyListLayoutInfo, LazyListState};
134pub use font_scale::{FontScaleCurve, MAX_FONT_SCALE_KNOTS};
135pub use key_event::{KeyCode, KeyEvent, KeyEventType, Modifiers};
136#[cfg(any(test, feature = "test-helpers"))]
137#[doc(hidden)]
138pub use render_state::reset_render_state_for_tests;
139pub use render_state::{
140    clear_transient_scroll_motion_contexts, current_density, current_font_scale,
141    current_font_scale_curve, debug_last_fling_velocity, debug_reset_last_fling_velocity,
142    has_current_app_context, has_pending_draw_repasses, has_pending_layout_repasses,
143    has_pending_measure_repasses, peek_focus_invalidation, peek_layout_invalidation,
144    peek_pointer_invalidation, peek_render_invalidation, pending_layout_repass_nodes_snapshot,
145    request_focus_invalidation, request_layout_invalidation, request_pointer_invalidation,
146    request_render_invalidation, scale_sp, schedule_draw_repass, schedule_layout_repass,
147    schedule_measure_repass, set_density, set_font_scale, set_font_scale_curve,
148    take_draw_repass_nodes, take_focus_invalidation, take_layout_invalidation,
149    take_layout_repass_nodes, take_measure_repass_nodes, take_pointer_invalidation,
150    take_render_invalidation, AppContext, AppContextScope, MAX_FONT_SCALE, MIN_FONT_SCALE,
151};
152pub use renderer::{HeadlessRenderer, PaintLayer, RecordedRenderScene, RenderOp};
153pub use scroll::{ScrollElement, ScrollNode, ScrollSettlePolicy, ScrollState};
154pub use semantics_dispatch::{
155    clear_semantics_invalidations, has_pending_semantics_invalidations,
156    process_semantics_invalidations, schedule_semantics_invalidation,
157};
158pub use zoom::ZoomState;
159// Test utilities for fling velocity verification (only with test-helpers feature)
160#[cfg(feature = "test-helpers")]
161pub use modifier::{last_fling_velocity, reset_last_fling_velocity};
162pub use subcompose_layout::{
163    Constraints, MeasureResult, Placement, SubcomposeLayoutNode, SubcomposeLayoutScope,
164    SubcomposeMeasureScope, SubcomposeMeasureScopeImpl,
165};
166pub use text::{
167    get_cursor_x_for_offset, get_offset_for_position, layout_text, measure_text,
168    measure_text_for_node, measure_text_with_options, measure_text_with_options_for_node,
169    prepare_text_layout, prepare_text_layout_for_node, set_text_measurer, LinkAnnotation,
170    ParagraphStyle, PlatformParagraphStyle, PlatformSpanStyle, PlatformTextStyle,
171    PreparedTextLayout, SpanStyle, StringAnnotation, TextDrawStyle, TextLayoutOptions,
172    TextLayoutResult, TextLinePrefixWidths, TextMeasurer, TextMetrics, TextOptions, TextOverflow,
173    TextShaping, TextStyle,
174};
175pub use text_field_modifier_node::{TextFieldElement, TextFieldModifierNode, TextPanResolver};
176pub use text_modifier_node::{TextModifierElement, TextModifierNode};
177pub use widgets::clickable_text::ClickableText;
178pub use widgets::lazy_list::{LazyColumn, LazyColumnSpec, LazyRow, LazyRowSpec};
179pub use widgets::linked_text::LinkedText;
180pub use widgets::swipe_to_dismiss::{SwipeDismissSide, SwipeToDismiss, SwipeToDismissSpec};
181pub use widgets::text_selection_menu::local_on_light_surface;
182
183// Debug utilities
184pub use debug::{
185    format_layout_tree, format_modifier_chain, format_render_scene, format_screen_summary,
186    install_modifier_chain_trace, log_layout_tree, log_modifier_chain, log_render_scene,
187    log_screen_summary, ModifierChainTraceGuard,
188};
189
190/// In-memory composition helper used by tests.
191pub struct TestComposition {
192    _scope: render_state::AppContextScope,
193    app_context: Rc<AppContext>,
194    composition: Composition<MemoryApplier>,
195}
196
197impl TestComposition {
198    pub fn root(&self) -> Option<NodeId> {
199        self.app_context.enter(|| self.composition.root())
200    }
201
202    pub fn runtime_handle(&self) -> RuntimeHandle {
203        self.app_context.enter(|| self.composition.runtime_handle())
204    }
205
206    pub fn should_render(&self) -> bool {
207        self.app_context.enter(|| self.composition.should_render())
208    }
209
210    pub fn take_root_render_request(&mut self) -> bool {
211        let app_context = Rc::clone(&self.app_context);
212        app_context.enter(|| self.composition.take_root_render_request())
213    }
214
215    pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
216        let app_context = Rc::clone(&self.app_context);
217        app_context.enter(|| self.composition.flush_pending_node_updates())
218    }
219
220    pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
221        let app_context = Rc::clone(&self.app_context);
222        app_context.enter(|| self.composition.process_invalid_scopes())
223    }
224
225    pub fn render(&mut self, root_key: Key, content: impl FnMut()) -> Result<(), NodeError> {
226        let app_context = Rc::clone(&self.app_context);
227        app_context.enter(|| self.composition.render(root_key, content))
228    }
229
230    pub fn applier_mut(&mut self) -> TestApplierGuard<'_> {
231        let scope = self.app_context.enter_scope();
232        let applier = self.composition.applier_mut();
233        TestApplierGuard {
234            _scope: scope,
235            applier,
236        }
237    }
238
239    pub fn with_app_context<R>(&self, block: impl FnOnce() -> R) -> R {
240        self.app_context.enter(block)
241    }
242}
243
244pub struct TestApplierGuard<'a> {
245    _scope: render_state::AppContextScope,
246    applier: ApplierGuard<'a, MemoryApplier>,
247}
248
249impl Deref for TestApplierGuard<'_> {
250    type Target = MemoryApplier;
251
252    fn deref(&self) -> &Self::Target {
253        &self.applier
254    }
255}
256
257impl DerefMut for TestApplierGuard<'_> {
258    fn deref_mut(&mut self) -> &mut Self::Target {
259        &mut self.applier
260    }
261}
262
263/// Build a composition with a simple in-memory applier and run the provided closure once.
264pub fn run_test_composition(build: impl FnMut()) -> TestComposition {
265    let app_context = AppContext::new();
266    app_context.enter(|| {
267        #[cfg(test)]
268        reset_render_state_for_tests();
269    });
270    let mut test_composition = TestComposition {
271        _scope: app_context.enter_scope(),
272        app_context,
273        composition: Composition::new(MemoryApplier::new()),
274    };
275    test_composition
276        .render(location_key(file!(), line!(), column!()), build)
277        .expect("initial render succeeds");
278    test_composition
279}
280
281pub use cranpose_core::MutableState as SnapshotState;
282
283#[cfg(test)]
284#[path = "tests/anchor_async_tests.rs"]
285mod anchor_async_tests;
286
287#[cfg(test)]
288#[path = "tests/animated_visibility_tests.rs"]
289mod animated_visibility_tests;
290
291#[cfg(test)]
292#[path = "tests/lazy_recycle_effect_tests.rs"]
293mod lazy_recycle_effect_tests;
294
295#[cfg(test)]
296#[path = "tests/animation_frame_pump_tests.rs"]
297mod animation_frame_pump_tests;
298
299#[cfg(test)]
300#[path = "tests/crossfade_tests.rs"]
301mod crossfade_tests;
302
303#[cfg(test)]
304#[path = "tests/async_runtime_full_layout_test.rs"]
305mod async_runtime_full_layout_test;
306
307#[cfg(test)]
308#[path = "tests/cursor_position_tests.rs"]
309mod cursor_position_tests;
310
311#[cfg(test)]
312#[path = "tests/popup_tests.rs"]
313mod popup_tests;
314
315#[cfg(test)]
316#[path = "tests/selection_handle_tests.rs"]
317mod selection_handle_tests;
318
319#[cfg(test)]
320#[path = "tests/tab_switching_tests.rs"]
321mod tab_switching_tests;
322
323#[cfg(test)]
324#[path = "tests/lazy_list_viewport_tests.rs"]
325mod lazy_list_viewport_tests;
326
327#[cfg(test)]
328#[path = "tests/swipe_to_dismiss_lazy_tests.rs"]
329mod swipe_to_dismiss_lazy_tests;
330
331#[cfg(test)]
332#[path = "tests/swipe_to_dismiss_render_tests.rs"]
333mod swipe_to_dismiss_render_tests;
334
335#[cfg(test)]
336#[path = "tests/lazy_list_recompose_tests.rs"]
337mod lazy_list_recompose_tests;
338
339#[cfg(test)]
340#[path = "tests/wear_widget_tests.rs"]
341mod wear_widget_tests;