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