Skip to main content

cranpose_ui/
lib.rs

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