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