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    prune_draw_observations_to_nodes, request_focus_invalidation, request_layout_invalidation,
146    request_pointer_invalidation, request_render_invalidation, scale_sp, schedule_draw_repass,
147    schedule_layout_repass, schedule_measure_repass, set_density, set_font_scale,
148    set_font_scale_curve, take_draw_repass_nodes, take_focus_invalidation,
149    take_layout_invalidation, take_layout_repass_nodes, take_measure_repass_nodes,
150    take_pointer_invalidation, take_render_invalidation, AppContext, AppContextScope,
151    MAX_FONT_SCALE, MIN_FONT_SCALE,
152};
153pub use renderer::{HeadlessRenderer, PaintLayer, RecordedRenderScene, RenderOp};
154pub use scroll::{ScrollElement, ScrollNode, ScrollSettlePolicy, ScrollState};
155pub use semantics_dispatch::{
156    clear_semantics_invalidations, has_pending_semantics_invalidations,
157    process_semantics_invalidations, schedule_semantics_invalidation,
158};
159pub use zoom::ZoomState;
160// Test utilities for fling velocity verification (only with test-helpers feature)
161#[cfg(feature = "test-helpers")]
162pub use modifier::{last_fling_velocity, reset_last_fling_velocity};
163pub use subcompose_layout::{
164    Constraints, MeasureResult, Placement, SubcomposeLayoutNode, SubcomposeLayoutScope,
165    SubcomposeMeasureScope, SubcomposeMeasureScopeImpl,
166};
167pub use text::{
168    get_cursor_x_for_offset, get_offset_for_position, layout_text, measure_text,
169    measure_text_for_node, measure_text_with_options, measure_text_with_options_for_node,
170    prepare_text_layout, prepare_text_layout_for_node, set_text_measurer, LinkAnnotation,
171    ParagraphStyle, PlatformParagraphStyle, PlatformSpanStyle, PlatformTextStyle,
172    PreparedTextLayout, SpanStyle, StringAnnotation, TextDrawStyle, TextLayoutOptions,
173    TextLayoutResult, TextLinePrefixWidths, TextMeasurer, TextMetrics, TextOptions, TextOverflow,
174    TextShaping, TextStyle,
175};
176pub use text_field_modifier_node::{TextFieldElement, TextFieldModifierNode, TextPanResolver};
177pub use text_modifier_node::{TextModifierElement, TextModifierNode};
178pub use widgets::clickable_text::ClickableText;
179pub use widgets::lazy_list::{LazyColumn, LazyColumnSpec, LazyRow, LazyRowSpec};
180pub use widgets::linked_text::LinkedText;
181pub use widgets::swipe_to_dismiss::{SwipeDismissSide, SwipeToDismiss, SwipeToDismissSpec};
182pub use widgets::text_selection_menu::local_on_light_surface;
183
184// Debug utilities
185pub use debug::{
186    format_layout_tree, format_modifier_chain, format_render_scene, format_screen_summary,
187    install_modifier_chain_trace, log_layout_tree, log_modifier_chain, log_render_scene,
188    log_screen_summary, ModifierChainTraceGuard,
189};
190
191/// In-memory composition helper used by tests.
192pub struct TestComposition {
193    _scope: render_state::AppContextScope,
194    app_context: Rc<AppContext>,
195    composition: Composition<MemoryApplier>,
196}
197
198impl TestComposition {
199    pub fn root(&self) -> Option<NodeId> {
200        self.app_context.enter(|| self.composition.root())
201    }
202
203    pub fn runtime_handle(&self) -> RuntimeHandle {
204        self.app_context.enter(|| self.composition.runtime_handle())
205    }
206
207    pub fn should_render(&self) -> bool {
208        self.app_context.enter(|| self.composition.should_render())
209    }
210
211    pub fn take_root_render_request(&mut self) -> bool {
212        let app_context = Rc::clone(&self.app_context);
213        app_context.enter(|| self.composition.take_root_render_request())
214    }
215
216    pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
217        let app_context = Rc::clone(&self.app_context);
218        app_context.enter(|| self.composition.flush_pending_node_updates())
219    }
220
221    pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
222        let app_context = Rc::clone(&self.app_context);
223        app_context.enter(|| self.composition.process_invalid_scopes())
224    }
225
226    pub fn render(&mut self, root_key: Key, content: impl FnMut()) -> Result<(), NodeError> {
227        let app_context = Rc::clone(&self.app_context);
228        app_context.enter(|| self.composition.render(root_key, content))
229    }
230
231    pub fn applier_mut(&mut self) -> TestApplierGuard<'_> {
232        let scope = self.app_context.enter_scope();
233        let applier = self.composition.applier_mut();
234        TestApplierGuard {
235            _scope: scope,
236            applier,
237        }
238    }
239
240    pub fn with_app_context<R>(&self, block: impl FnOnce() -> R) -> R {
241        self.app_context.enter(block)
242    }
243}
244
245pub struct TestApplierGuard<'a> {
246    _scope: render_state::AppContextScope,
247    applier: ApplierGuard<'a, MemoryApplier>,
248}
249
250impl Deref for TestApplierGuard<'_> {
251    type Target = MemoryApplier;
252
253    fn deref(&self) -> &Self::Target {
254        &self.applier
255    }
256}
257
258impl DerefMut for TestApplierGuard<'_> {
259    fn deref_mut(&mut self) -> &mut Self::Target {
260        &mut self.applier
261    }
262}
263
264/// Build a composition with a simple in-memory applier and run the provided closure once.
265pub fn run_test_composition(build: impl FnMut()) -> TestComposition {
266    let app_context = AppContext::new();
267    app_context.enter(|| {
268        #[cfg(test)]
269        reset_render_state_for_tests();
270    });
271    let mut test_composition = TestComposition {
272        _scope: app_context.enter_scope(),
273        app_context,
274        composition: Composition::new(MemoryApplier::new()),
275    };
276    test_composition
277        .render(location_key(file!(), line!(), column!()), build)
278        .expect("initial render succeeds");
279    test_composition
280}
281
282pub use cranpose_core::MutableState as SnapshotState;
283
284#[cfg(test)]
285#[path = "tests/anchor_async_tests.rs"]
286mod anchor_async_tests;
287
288#[cfg(test)]
289#[path = "tests/animated_visibility_tests.rs"]
290mod animated_visibility_tests;
291
292#[cfg(test)]
293#[path = "tests/lazy_recycle_effect_tests.rs"]
294mod lazy_recycle_effect_tests;
295
296#[cfg(test)]
297#[path = "tests/animation_frame_pump_tests.rs"]
298mod animation_frame_pump_tests;
299
300#[cfg(test)]
301#[path = "tests/crossfade_tests.rs"]
302mod crossfade_tests;
303
304#[cfg(test)]
305#[path = "tests/async_runtime_full_layout_test.rs"]
306mod async_runtime_full_layout_test;
307
308#[cfg(test)]
309#[path = "tests/cursor_position_tests.rs"]
310mod cursor_position_tests;
311
312#[cfg(test)]
313#[path = "tests/popup_tests.rs"]
314mod popup_tests;
315
316#[cfg(test)]
317#[path = "tests/selection_handle_tests.rs"]
318mod selection_handle_tests;
319
320#[cfg(test)]
321#[path = "tests/tab_switching_tests.rs"]
322mod tab_switching_tests;
323
324#[cfg(test)]
325#[path = "tests/lazy_list_viewport_tests.rs"]
326mod lazy_list_viewport_tests;
327
328#[cfg(test)]
329#[path = "tests/swipe_to_dismiss_lazy_tests.rs"]
330mod swipe_to_dismiss_lazy_tests;
331
332#[cfg(test)]
333#[path = "tests/swipe_to_dismiss_render_tests.rs"]
334mod swipe_to_dismiss_render_tests;
335
336#[cfg(test)]
337#[path = "tests/lazy_list_recompose_tests.rs"]
338mod lazy_list_recompose_tests;
339
340#[cfg(test)]
341#[path = "tests/wear_widget_tests.rs"]
342mod wear_widget_tests;