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