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, BasicTextFieldWithOptions,
111    BasicTextWithOptions, BitmapPainter, Box, BoxScope, BoxSpec, BoxWithConstraints,
112    BoxWithConstraintsScope, BoxWithConstraintsScopeImpl, Button, ButtonSpec, Canvas, Column,
113    ColumnSpec, ContentScale, Crossfade, EnterTransition, ExitTransition, ForEach, Image, Layout,
114    LayoutNode, Painter, Row, RowSpec, Spacer, SubcomposeLayout, SvgPainter, SvgPainterError, Text,
115    TextWithOptions, DEFAULT_ALPHA,
116};
117// Lazy list exports - single source from cranpose-foundation
118pub use cranpose_foundation::lazy::{LazyListItemInfo, LazyListLayoutInfo, LazyListState};
119pub use key_event::{KeyCode, KeyEvent, KeyEventType, Modifiers};
120#[cfg(any(test, feature = "test-helpers"))]
121#[doc(hidden)]
122pub use render_state::reset_render_state_for_tests;
123pub use render_state::{
124    clear_transient_scroll_motion_contexts, current_density, debug_last_fling_velocity,
125    debug_reset_last_fling_velocity, has_current_app_context, has_pending_draw_repasses,
126    has_pending_layout_repasses, has_pending_measure_repasses, peek_focus_invalidation,
127    peek_layout_invalidation, peek_pointer_invalidation, peek_render_invalidation,
128    pending_layout_repass_nodes_snapshot, request_focus_invalidation, request_layout_invalidation,
129    request_pointer_invalidation, request_render_invalidation, schedule_draw_repass,
130    schedule_layout_repass, schedule_measure_repass, set_density, take_draw_repass_nodes,
131    take_focus_invalidation, take_layout_invalidation, take_layout_repass_nodes,
132    take_measure_repass_nodes, take_pointer_invalidation, take_render_invalidation, AppContext,
133    AppContextScope,
134};
135pub use renderer::{HeadlessRenderer, PaintLayer, RecordedRenderScene, RenderOp};
136pub use scroll::{ScrollElement, ScrollNode, ScrollSettlePolicy, ScrollState};
137pub use zoom::ZoomState;
138// Test utilities for fling velocity verification (only with test-helpers feature)
139#[cfg(feature = "test-helpers")]
140pub use modifier::{last_fling_velocity, reset_last_fling_velocity};
141pub use subcompose_layout::{
142    Constraints, MeasureResult, Placement, SubcomposeLayoutNode, SubcomposeLayoutScope,
143    SubcomposeMeasureScope, SubcomposeMeasureScopeImpl,
144};
145pub use text::{
146    get_cursor_x_for_offset, get_offset_for_position, layout_text, measure_text,
147    measure_text_for_node, measure_text_with_options, measure_text_with_options_for_node,
148    prepare_text_layout, prepare_text_layout_for_node, set_text_measurer, LinkAnnotation,
149    ParagraphStyle, PlatformParagraphStyle, PlatformSpanStyle, PlatformTextStyle,
150    PreparedTextLayout, SpanStyle, StringAnnotation, TextDrawStyle, TextLayoutOptions,
151    TextLayoutResult, TextLinePrefixWidths, TextMeasurer, TextMetrics, TextOptions, TextOverflow,
152    TextShaping, TextStyle,
153};
154pub use text_field_modifier_node::{TextFieldElement, TextFieldModifierNode, TextPanResolver};
155pub use text_modifier_node::{TextModifierElement, TextModifierNode};
156pub use widgets::clickable_text::ClickableText;
157pub use widgets::lazy_list::{LazyColumn, LazyColumnSpec, LazyRow, LazyRowSpec};
158pub use widgets::linked_text::LinkedText;
159pub use widgets::swipe_to_dismiss::{SwipeDismissSide, SwipeToDismiss, SwipeToDismissSpec};
160pub use widgets::text_selection_menu::local_on_light_surface;
161
162// Debug utilities
163pub use debug::{
164    format_layout_tree, format_modifier_chain, format_render_scene, format_screen_summary,
165    install_modifier_chain_trace, log_layout_tree, log_modifier_chain, log_render_scene,
166    log_screen_summary, ModifierChainTraceGuard,
167};
168
169/// In-memory composition helper used by tests.
170pub struct TestComposition {
171    _scope: render_state::AppContextScope,
172    app_context: Rc<AppContext>,
173    composition: Composition<MemoryApplier>,
174}
175
176impl TestComposition {
177    pub fn root(&self) -> Option<NodeId> {
178        self.app_context.enter(|| self.composition.root())
179    }
180
181    pub fn runtime_handle(&self) -> RuntimeHandle {
182        self.app_context.enter(|| self.composition.runtime_handle())
183    }
184
185    pub fn should_render(&self) -> bool {
186        self.app_context.enter(|| self.composition.should_render())
187    }
188
189    pub fn take_root_render_request(&mut self) -> bool {
190        let app_context = Rc::clone(&self.app_context);
191        app_context.enter(|| self.composition.take_root_render_request())
192    }
193
194    pub fn flush_pending_node_updates(&mut self) -> Result<(), NodeError> {
195        let app_context = Rc::clone(&self.app_context);
196        app_context.enter(|| self.composition.flush_pending_node_updates())
197    }
198
199    pub fn process_invalid_scopes(&mut self) -> Result<bool, NodeError> {
200        let app_context = Rc::clone(&self.app_context);
201        app_context.enter(|| self.composition.process_invalid_scopes())
202    }
203
204    pub fn render(&mut self, root_key: Key, content: impl FnMut()) -> Result<(), NodeError> {
205        let app_context = Rc::clone(&self.app_context);
206        app_context.enter(|| self.composition.render(root_key, content))
207    }
208
209    pub fn applier_mut(&mut self) -> TestApplierGuard<'_> {
210        let scope = self.app_context.enter_scope();
211        let applier = self.composition.applier_mut();
212        TestApplierGuard {
213            _scope: scope,
214            applier,
215        }
216    }
217
218    pub fn with_app_context<R>(&self, block: impl FnOnce() -> R) -> R {
219        self.app_context.enter(block)
220    }
221}
222
223pub struct TestApplierGuard<'a> {
224    _scope: render_state::AppContextScope,
225    applier: ApplierGuard<'a, MemoryApplier>,
226}
227
228impl Deref for TestApplierGuard<'_> {
229    type Target = MemoryApplier;
230
231    fn deref(&self) -> &Self::Target {
232        &self.applier
233    }
234}
235
236impl DerefMut for TestApplierGuard<'_> {
237    fn deref_mut(&mut self) -> &mut Self::Target {
238        &mut self.applier
239    }
240}
241
242/// Build a composition with a simple in-memory applier and run the provided closure once.
243pub fn run_test_composition(build: impl FnMut()) -> TestComposition {
244    let app_context = AppContext::new();
245    app_context.enter(|| {
246        #[cfg(test)]
247        reset_render_state_for_tests();
248    });
249    let mut test_composition = TestComposition {
250        _scope: app_context.enter_scope(),
251        app_context,
252        composition: Composition::new(MemoryApplier::new()),
253    };
254    test_composition
255        .render(location_key(file!(), line!(), column!()), build)
256        .expect("initial render succeeds");
257    test_composition
258}
259
260pub use cranpose_core::MutableState as SnapshotState;
261
262#[cfg(test)]
263#[path = "tests/anchor_async_tests.rs"]
264mod anchor_async_tests;
265
266#[cfg(test)]
267#[path = "tests/animated_visibility_tests.rs"]
268mod animated_visibility_tests;
269
270#[cfg(test)]
271#[path = "tests/animation_frame_pump_tests.rs"]
272mod animation_frame_pump_tests;
273
274#[cfg(test)]
275#[path = "tests/crossfade_tests.rs"]
276mod crossfade_tests;
277
278#[cfg(test)]
279#[path = "tests/async_runtime_full_layout_test.rs"]
280mod async_runtime_full_layout_test;
281
282#[cfg(test)]
283#[path = "tests/cursor_position_tests.rs"]
284mod cursor_position_tests;
285
286#[cfg(test)]
287#[path = "tests/popup_tests.rs"]
288mod popup_tests;
289
290#[cfg(test)]
291#[path = "tests/selection_handle_tests.rs"]
292mod selection_handle_tests;
293
294#[cfg(test)]
295#[path = "tests/tab_switching_tests.rs"]
296mod tab_switching_tests;
297
298#[cfg(test)]
299#[path = "tests/lazy_list_viewport_tests.rs"]
300mod lazy_list_viewport_tests;
301
302#[cfg(test)]
303#[path = "tests/swipe_to_dismiss_lazy_tests.rs"]
304mod swipe_to_dismiss_lazy_tests;
305
306#[cfg(test)]
307#[path = "tests/swipe_to_dismiss_render_tests.rs"]
308mod swipe_to_dismiss_render_tests;
309
310#[cfg(test)]
311#[path = "tests/lazy_list_recompose_tests.rs"]
312mod lazy_list_recompose_tests;