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