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