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