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