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