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