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