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