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