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