pub mod app;
pub mod drawer;
pub mod dropdown;
pub mod accordion;
mod hero;
pub mod hero_tag;
pub mod segmented;
pub mod tabs;
pub mod radio;
pub mod skeleton;
pub mod circular_progress;
pub mod wrap;
pub mod positioned;
pub mod grid;
pub mod aspect_ratio;
pub mod app_bar;
pub mod avatar;
pub mod badge;
pub mod button;
pub mod card;
pub mod checkbox;
pub mod chip;
pub mod column;
pub mod container;
pub mod custom_paint;
pub mod dialog;
pub mod divider;
pub mod focus_api;
pub mod icon;
pub mod image;
pub mod list_tile;
pub mod list_view;
pub mod menu;
pub mod nav_rail;
pub mod overlay;
pub mod overlay_api;
pub mod padding;
pub mod autocomplete;
pub mod dismissible;
pub mod pointer;
pub mod pressable;
pub mod progress_bar;
pub mod pull_to_refresh;
pub mod rect_reader;
pub mod bottom_nav;
pub mod search_bar;
pub mod snackbar;
pub mod fab;
pub mod table;
pub mod carousel;
pub mod stepper;
pub mod rating_bar;
pub mod interactive_viewer;
pub mod material;
pub mod selection;
pub mod shader_paint;
pub mod date_picker;
pub mod time_picker;
pub mod data_table;
pub mod render_tree;
pub mod repaint_boundary;
pub mod row;
pub mod scaffold;
pub mod screen_transition_view;
pub mod scroll_view;
pub mod sheet;
pub mod slider;
pub mod spacer;
pub mod stack;
pub mod switch;
pub mod tab;
pub mod text;
pub mod text_area;
pub mod text_edit;
pub mod text_input;
pub mod toast;
pub mod tooltip;
pub mod transform_layer;
pub use app::WidgetApp;
pub use app_bar::AppBar;
pub use avatar::Avatar;
pub use badge::Badge;
pub use button::{Button, ButtonVariant};
pub use card::Card;
pub use checkbox::Checkbox;
pub use chip::Chip;
pub use column::Column;
pub use container::{BoxShape, Container};
pub use aspect_ratio::AspectRatio;
pub use grid::Grid;
pub use circular_progress::CircularProgress;
pub use skeleton::Skeleton;
pub use radio::Radio;
pub use segmented::SegmentedControl;
pub use tabs::{TabView, Tabs};
pub use accordion::Accordion;
pub use hero_tag::{Hero, HeroApi};
pub use dropdown::Dropdown;
pub use drawer::Drawer;
pub use positioned::Positioned;
pub use wrap::Wrap;
pub use custom_paint::CustomPaint;
pub use dialog::{Dialog, DialogPresentation};
pub use menu::Menu;
pub use sheet::Sheet;
pub use toast::{Toast, ToastKind};
pub use divider::Divider;
pub use focus_api::{FocusApi, WithFocus};
pub use icon::{register_icon, resolve_icon, Icon, IconKind};
pub use image::Image;
pub use list_tile::ListTile;
pub use list_view::ListView;
pub use nav_rail::{NavItem, NavRail};
pub use overlay::{
LayerId, LayerPosition, InputBehavior, FocusBehavior, ScrimConfig,
OverlayEntry, push_overlay, drain_overlays, clear_overlays,
};
pub use overlay_api::{OverlayApi, OverlayKind, WithOverlay};
pub use padding::EdgeInsets;
pub use pointer::{AbsorbPointer, IgnorePointer};
pub use autocomplete::Autocomplete;
pub use dismissible::{Dismissible, DismissDirection};
pub use pressable::{LongPressable, PressApi, Pressable};
pub use progress_bar::ProgressBar;
pub use pull_to_refresh::PullToRefresh;
pub use rect_reader::RectReader;
pub use bottom_nav::{BottomNavItem, BottomNavigationBar};
pub use search_bar::SearchBar;
pub use snackbar::Snackbar;
pub use fab::FloatingActionButton;
pub use table::{Table, TableColumn};
pub use carousel::{Carousel, PageView};
pub use stepper::Stepper;
pub use rating_bar::RatingBar;
pub use interactive_viewer::InteractiveViewer;
pub use material::{
MaterialKey, resolve_material, ContainerMaterial, CardMaterial,
DialogMaterial, SheetMaterial, DrawerMaterial, AppBarMaterial, BottomNavMaterial,
};
pub use selection::{GlassLens, SelectionKind, SelectionStyle};
pub use shader_paint::ShaderPaint;
pub use date_picker::{DatePicker, SimpleDate, SelectionMode, PageAxis};
pub use time_picker::{TimePicker, SimpleTime, TimeUnit};
pub use data_table::{DataTable, DataTableColumn, SortDirection};
pub use render_tree::{HitHandler, InspectNode, NodeId, RenderTree, ScrollAxes, ScrollHandler, TreeNode};
pub use repaint_boundary::RepaintBoundary;
pub use row::Row;
pub use scaffold::Scaffold;
pub use screen_transition_view::ScreenTransitionView;
pub use scroll_view::{ScrollView, ScrollAxis, MAX_TL_DIM};
pub use slider::Slider;
pub use spacer::{Expanded, Spacer};
pub use stack::Stack;
pub use switch::Switch;
pub use tab::{Tab, TabBar};
pub use text::{Text, TextAlign, FontWeight};
pub use text_area::TextArea;
pub use text_edit::{
CursorShape, CursorStyle, EditController, EditableDecl, InputFilter, Span, SpanFn,
TextEditState, TextLayoutSnapshot,
};
pub use text_input::TextInput;
pub use tooltip::{Tooltip, TooltipStyle, WidgetExt};
pub use transform_layer::TransformLayer;
use std::rc::Rc;
use std::cell::RefCell;
use std::sync::Arc;
use rosace_core::types::{Point, Rect, Size};
use rosace_core::{Element, NativeElement, WidgetPayload};
use rosace_layout::{AxisBound, Constraints};
pub(crate) fn shrink_axis(b: AxisBound, by: f32) -> AxisBound {
match b {
AxisBound::Bounded(v) => AxisBound::Bounded((v - by).max(0.0)),
other => other,
}
}
use rosace_render::{Color, DrawCommand, FontCache, Picture, PictureRecorder};
use rosace_theme::ThemeData;
use std::cell::Cell;
thread_local! {
static ANIM_REQUEST: Cell<bool> = const { Cell::new(false) };
}
pub fn request_animation() { ANIM_REQUEST.with(|a| a.set(true)); }
thread_local! {
static CURRENT_POINTER: Cell<(f32, f32)> = const { Cell::new((0.0, 0.0)) };
}
pub fn set_pointer(x: f32, y: f32) { CURRENT_POINTER.with(|p| p.set((x, y))); }
pub fn current_pointer() -> (f32, f32) { CURRENT_POINTER.with(|p| p.get()) }
thread_local! {
static BOTTOM_OVERLAY_INSET: Cell<f32> = const { Cell::new(0.0) };
}
pub fn set_bottom_overlay_inset(px: f32) { BOTTOM_OVERLAY_INSET.with(|v| v.set(px)); }
pub fn take_bottom_overlay_inset() -> f32 { BOTTOM_OVERLAY_INSET.with(|v| v.replace(0.0)) }
pub fn take_animation_request() -> bool { ANIM_REQUEST.with(|a| a.replace(false)) }
pub fn anim_clock() -> f32 {
use std::sync::OnceLock;
use web_time::Instant;
static START: OnceLock<Instant> = OnceLock::new();
START.get_or_init(Instant::now).elapsed().as_secs_f32()
}
pub(crate) fn lerp_color(a: Color, b: Color, t: f32) -> Color {
let l = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t).round() as u8;
Color::rgba(l(a.r, b.r), l(a.g, b.g), l(a.b, b.b), l(a.a, b.a))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Alignment {
TopLeft, TopCenter, TopRight,
CenterLeft, #[default] Center, CenterRight,
BottomLeft, BottomCenter, BottomRight,
}
impl Alignment {
pub fn offset(&self, container: Size, child: Size) -> Point {
let fx = match self {
Alignment::TopLeft | Alignment::CenterLeft | Alignment::BottomLeft => 0.0,
Alignment::TopCenter | Alignment::Center | Alignment::BottomCenter => 0.5,
Alignment::TopRight | Alignment::CenterRight | Alignment::BottomRight => 1.0,
};
let fy = match self {
Alignment::TopLeft | Alignment::TopCenter | Alignment::TopRight => 0.0,
Alignment::CenterLeft | Alignment::Center | Alignment::CenterRight => 0.5,
Alignment::BottomLeft | Alignment::BottomCenter | Alignment::BottomRight => 1.0,
};
Point {
x: ((container.width - child.width) * fx).max(0.0),
y: ((container.height - child.height) * fy).max(0.0),
}
}
}
#[derive(Clone, Debug)]
pub struct Semantics {
pub role: rosace_core::Role,
pub label: Option<String>,
pub value: Option<String>,
pub heading_level: Option<u8>,
pub href: Option<String>,
}
impl Semantics {
pub fn new(role: rosace_core::Role) -> Self {
Self { role, label: None, value: None, heading_level: None, href: None }
}
pub fn label(mut self, l: impl Into<String>) -> Self { self.label = Some(l.into()); self }
pub fn value(mut self, v: impl Into<String>) -> Self { self.value = Some(v.into()); self }
pub fn heading_level(mut self, level: u8) -> Self { self.heading_level = Some(level); self }
pub fn href(mut self, href: impl Into<String>) -> Self { self.href = Some(href.into()); self }
}
pub struct HitTarget {
pub rect: Rect,
pub callback: Arc<dyn Fn() + Send + Sync>,
}
pub struct ScrollTarget {
pub rect: Rect,
pub callback: Arc<dyn Fn(f32, f32) + Send + Sync>,
}
#[derive(Clone)]
pub struct TransformLayerEntry {
pub picture: Picture,
pub child_size: Size,
pub viewport_rect: Rect,
pub zoom: f32,
pub scroll_x: f32,
pub scroll_y: f32,
}
pub struct PaintCtx<'a> {
pub recorder: &'a mut PictureRecorder,
pub rect: Rect,
pub font: &'a FontCache,
pub theme: ThemeData,
pub tree: Rc<RefCell<RenderTree>>,
pub node: NodeId,
pub owner: rosace_core::types::ComponentId,
pub clip_rect: Option<Rect>,
}
impl<'a> PaintCtx<'a> {
pub fn root(
recorder: &'a mut PictureRecorder,
rect: Rect,
font: &'a FontCache,
theme: ThemeData,
tree: Rc<RefCell<RenderTree>>,
) -> PaintCtx<'a> {
tree.borrow_mut().start_frame();
PaintCtx {
recorder,
rect,
font,
theme,
tree,
node: RenderTree::ROOT,
owner: rosace_core::types::ComponentId(0),
clip_rect: None,
}
}
pub fn child(&mut self, rect: Rect) -> PaintCtx<'_> {
let node = self.tree.borrow_mut().slot(self.node, true);
self.tree.borrow_mut().node_mut(node).cached_rect = Some(rect);
PaintCtx {
recorder: self.recorder,
rect,
font: self.font,
theme: self.theme.clone(),
tree: Rc::clone(&self.tree),
node,
owner: self.owner,
clip_rect: self.clip_rect,
}
}
pub fn child_keyed(&mut self, rect: Rect, key: u64) -> PaintCtx<'_> {
let node = self.tree.borrow_mut().keyed_slot(self.node, key);
self.tree.borrow_mut().node_mut(node).cached_rect = Some(rect);
PaintCtx {
recorder: self.recorder,
rect,
font: self.font,
theme: self.theme.clone(),
tree: Rc::clone(&self.tree),
node,
owner: self.owner,
clip_rect: self.clip_rect,
}
}
pub fn register_scroll_target(
&self,
rect: Rect,
axes: render_tree::ScrollAxes,
callback: Arc<dyn Fn(f32, f32) + Send + Sync>,
) {
self.tree.borrow_mut().node_mut(self.node).scrolls.push((rect, axes, callback));
}
pub fn register_zoom_target(&self, rect: Rect, callback: Arc<dyn Fn(f32) + Send + Sync>) {
self.tree.borrow_mut().node_mut(self.node).zooms.push((rect, callback));
}
pub fn register_focus(&self, node: rosace_a11y::FocusNode) {
self.tree.borrow_mut().node_mut(self.node).focus.push(node);
}
pub fn register_hit(&self, callback: Arc<dyn Fn() + Send + Sync>) {
let hit_rect = if let Some(clip) = self.clip_rect {
match intersect_rect(self.rect, clip) {
Some(r) => r,
None => return, }
} else {
self.rect
};
self.tree.borrow_mut().node_mut(self.node).hits.push((hit_rect, callback));
}
pub fn on_press(&self, f: impl Fn() + Send + Sync + 'static) {
self.register_hit(Arc::new(f));
}
pub fn scroll_controller(&self) -> rosace_scroll::ScrollController {
let mut tree = self.tree.borrow_mut();
let node = tree.node_mut(self.node);
if let Some(c) = &node.scroll_ctrl {
return c.clone();
}
let c = rosace_scroll::ScrollController::new();
c.offset.subscribe(self.owner);
c.content_size.subscribe(self.owner);
c.viewport_size.subscribe(self.owner);
node.scroll_ctrl = Some(c.clone());
c
}
pub fn hovered(&self) -> bool {
self.tree.borrow().node(self.node).hovered
}
pub fn pressed(&self) -> bool {
self.tree.borrow().node(self.node).pressed
}
pub fn hoverable(&self) {
let r = self.rect;
self.tree.borrow_mut().node_mut(self.node).hover_regions.push(r);
}
pub fn on_long_press(&self, f: impl Fn() + Send + Sync + 'static) {
let r = self.rect;
self.tree.borrow_mut().node_mut(self.node).long_hits.push((r, Arc::new(f)));
}
pub fn set_pointer_mode(&self, mode: u8) {
self.tree.borrow_mut().node_mut(self.node).pointer_mode = mode;
}
pub fn on_press_at(&self, f: impl Fn(f32, f32) + Send + Sync + 'static) {
let hit_rect = if let Some(clip) = self.clip_rect {
match intersect_rect(self.rect, clip) {
Some(r) => r,
None => return,
}
} else {
self.rect
};
self.tree.borrow_mut().node_mut(self.node).hits_at.push((hit_rect, Arc::new(f)));
}
pub fn register_nested_scroll(&mut self, f: impl Fn(f32, f32) -> bool + Send + Sync + 'static) {
let hit_rect = if let Some(clip) = self.clip_rect {
match intersect_rect(self.rect, clip) {
Some(r) => r,
None => return,
}
} else {
self.rect
};
self.tree.borrow_mut().node_mut(self.node).nested_scrolls.push((hit_rect, Arc::new(f)));
}
pub fn on_scroll(&self, f: impl Fn(f32, f32) + Send + Sync + 'static) {
self.register_scroll_target(self.rect, render_tree::ScrollAxes::BOTH, Arc::new(f));
}
pub fn semantics(&self, s: Semantics) {
self.tree.borrow_mut().node_mut(self.node).semantics.push(s);
}
pub fn focus_node(&self) -> rosace_a11y::FocusNode {
self.focus_node_seeded(false)
}
pub fn focus_node_seeded(&self, seed: bool) -> rosace_a11y::FocusNode {
let mut tree = self.tree.borrow_mut();
let node = tree.node_mut(self.node);
if let Some(f) = &node.focus_node {
return f.clone();
}
let f = rosace_a11y::FocusNode::new();
if seed {
f.request();
}
node.focus_node = Some(f.clone());
f
}
pub fn register_editable(&self, decl: text_edit::EditableDecl) {
self.tree.borrow_mut().node_mut(self.node).editable = Some(decl);
}
pub fn text_edit(&self) -> text_edit::TextEditState {
self.tree.borrow().node(self.node).text_edit.clone()
}
pub fn set_scrolled_cursor(&self, cursor: Option<usize>) {
self.tree.borrow_mut().node_mut(self.node).text_edit.scrolled_cursor = cursor;
}
pub fn set_scroll_x(&self, scroll_x: f32) {
self.tree.borrow_mut().node_mut(self.node).text_edit.scroll_x = scroll_x;
}
pub fn capture(&mut self, rect: Rect, paint: impl FnOnce(&mut PaintCtx)) -> rosace_render::Picture {
let node = self.tree.borrow_mut().slot(self.node, true);
self.capture_into(node, rect, paint)
}
pub fn keep_child_slot(&mut self) {
self.tree.borrow_mut().slot(self.node, false);
}
fn capture_into(&mut self, node: NodeId, rect: Rect, paint: impl FnOnce(&mut PaintCtx)) -> rosace_render::Picture {
let mut rec = rosace_render::PictureRecorder::new();
{
let mut cctx = PaintCtx {
recorder: &mut rec,
rect,
font: self.font,
theme: self.theme.clone(),
tree: Rc::clone(&self.tree),
node,
owner: self.owner,
clip_rect: self.clip_rect,
};
paint(&mut cctx);
}
rec.finish()
}
pub fn replay_offset(&mut self, picture: &rosace_render::Picture, dx: f32, dy: f32) {
for cmd in &picture.commands {
self.recorder.push(cmd.offset(dx, dy));
}
}
pub fn replay_morphed(&mut self, picture: &rosace_render::Picture, src: Rect, dst: Rect) {
let sx = if src.size.width.abs() > f32::EPSILON { dst.size.width / src.size.width } else { 1.0 };
let sy = if src.size.height.abs() > f32::EPSILON { dst.size.height / src.size.height } else { 1.0 };
for cmd in &picture.commands {
self.recorder.push(cmd.morph(src.origin, dst.origin, sx, sy));
}
}
pub fn attach_overlay(&self, entry: OverlayEntry) {
self.tree.borrow_mut().node_mut(self.node).overlays.push(entry);
}
pub fn attach_transform(&self, entry: TransformLayerEntry) {
self.tree.borrow_mut().node_mut(self.node).transforms.push(entry);
}
pub fn tc(&self, c: rosace_theme::Color) -> Color {
Color::rgba(
(c.r * 255.0) as u8,
(c.g * 255.0) as u8,
(c.b * 255.0) as u8,
(c.a * 255.0) as u8,
)
}
pub fn fill(&mut self, color: Color) {
let rect = self.rect;
self.recorder.push(DrawCommand::FillRect { rect, color });
}
pub fn stroke(&mut self, color: Color, width: f32) {
let rect = self.rect;
self.recorder.push(DrawCommand::StrokeRect { rect, color, width });
}
pub fn fill_rect(&mut self, rect: Rect, color: Color) {
self.recorder.push(DrawCommand::FillRect { rect, color });
}
pub fn stroke_rect(&mut self, rect: Rect, color: Color, width: f32) {
self.recorder.push(DrawCommand::StrokeRect { rect, color, width });
}
pub fn fill_rrect(&mut self, rect: Rect, radius: f32, color: Color) {
self.recorder.push(DrawCommand::FillRRect { rect, radius, color });
}
pub fn fill_circle(&mut self, center: Point, radius: f32, color: Color) {
self.recorder.push(DrawCommand::FillCircle { center, radius, color });
}
pub fn backdrop_blur(&mut self, rect: Rect, radius: f32, blur: f32, tint: Color) {
self.recorder.push(DrawCommand::BackdropBlur { rect, radius, blur, tint });
}
pub fn shader_fill(&mut self, rect: Rect, pipeline: rosace_shader::PipelineId, uniforms: Vec<u8>) {
self.recorder.push(DrawCommand::ShaderFill {
pipeline_id: pipeline.raw(),
rect,
uniforms,
animate_time: false,
});
}
pub fn shader_fill_animated(&mut self, rect: Rect, pipeline: rosace_shader::PipelineId, uniforms: Vec<u8>) {
self.recorder.push(DrawCommand::ShaderFill {
pipeline_id: pipeline.raw(),
rect,
uniforms,
animate_time: true,
});
}
fn bold_text_weight(mq: rosace_core::MediaQuery, weight: rosace_render::FontWeight) -> rosace_render::FontWeight {
use rosace_render::FontWeight;
if mq.bold_text && matches!(weight, FontWeight::Light | FontWeight::Regular | FontWeight::Medium) {
FontWeight::SemiBold
} else {
weight
}
}
pub fn draw_text_at(&mut self, text: &str, origin: Point, color: Color, px: f32) {
let mq = rosace_core::media_query::use_media_query();
let px = px * mq.text_scale;
self.recorder.push(DrawCommand::DrawText {
text: text.to_string(),
origin,
color,
px,
weight: Self::bold_text_weight(mq, rosace_render::FontWeight::Regular),
});
}
pub fn text(&mut self, s: &str, dx: f32, dy: f32, color: Color, px: f32) {
let mq = rosace_core::media_query::use_media_query();
let px = px * mq.text_scale;
let origin = Point { x: self.rect.origin.x + dx, y: self.rect.origin.y + dy };
self.recorder.push(DrawCommand::DrawText {
text: s.to_string(), origin, color, px,
weight: Self::bold_text_weight(mq, rosace_render::FontWeight::Regular),
});
}
pub fn text_styled(&mut self, s: &str, dx: f32, dy: f32, color: Color, px: f32, weight: rosace_render::FontWeight) {
let mq = rosace_core::media_query::use_media_query();
let px = px * mq.text_scale;
let weight = Self::bold_text_weight(mq, weight);
let origin = Point { x: self.rect.origin.x + dx, y: self.rect.origin.y + dy };
self.recorder.push(DrawCommand::DrawText { text: s.to_string(), origin, color, px, weight });
}
pub fn fill_shadow(&mut self, rect: Rect, color: Color, blur: f32) {
self.recorder.push(DrawCommand::DrawShadow { rect, radius: 0.0, color, blur });
}
pub fn fill_shadow_rrect(&mut self, rect: Rect, radius: f32, color: Color, blur: f32) {
self.recorder.push(DrawCommand::DrawShadow { rect, radius, color, blur });
}
pub fn stroke_rrect(&mut self, rect: Rect, radius: f32, color: Color, width: f32) {
self.recorder.push(DrawCommand::StrokeRRect { rect, radius, color, width });
}
pub fn fill_gradient(&mut self, rect: Rect, radius: f32, from: Color, to: Color, vertical: bool) {
self.recorder.push(DrawCommand::FillGradient { rect, radius, from, to, vertical });
}
pub fn fill_arc(&mut self, center: Point, radius: f32, thickness: f32, start_deg: f32, sweep_deg: f32, color: Color) {
self.recorder.push(DrawCommand::FillArc { center, radius, thickness, start_deg, sweep_deg, color });
}
pub fn request_animation(&self) { crate::tree::request_animation(); }
fn reduce_motion_animation_cfg(&self) -> rosace_theme::AnimationConfig {
let cfg = self.theme.animation;
if rosace_core::media_query::use_media_query().reduce_motion {
rosace_theme::AnimationConfig { enabled: false, ..cfg }
} else {
cfg
}
}
pub fn animate_to(&self, target: f32, duration_ms: f32) -> f32 {
let cfg = self.reduce_motion_animation_cfg();
if !cfg.enabled {
self.tree.borrow_mut().node_mut(self.node).anim = Some(target);
return target;
}
let dur = (if duration_ms > 0.0 { duration_ms } else { cfg.duration_ms }).max(1.0);
let (val, settled) = {
let mut tree = self.tree.borrow_mut();
let node = tree.node_mut(self.node);
match node.anim {
None => { node.anim = Some(target); (target, true) }
Some(cur) => {
let dt = rosace_animate::frame_dt();
let alpha = 1.0 - (-dt * (1000.0 / dur)).exp();
let next = cur + (target - cur) * alpha;
let settled = (next - target).abs() < 0.001;
let v = if settled { target } else { next };
node.anim = Some(v);
(v, settled)
}
}
};
if !settled { crate::tree::request_animation(); }
val
}
pub fn seed_anim_if_unset(&self, value: f32) {
let mut tree = self.tree.borrow_mut();
let node = tree.node_mut(self.node);
if node.anim.is_none() {
node.anim = Some(value);
}
}
pub fn set_anim(&self, value: f32) {
self.tree.borrow_mut().node_mut(self.node).anim = Some(value);
}
pub fn animate_channel(&self, channel: usize, target: f32, duration_ms: f32) -> f32 {
let cfg = self.reduce_motion_animation_cfg();
let mut tree = self.tree.borrow_mut();
let node = tree.node_mut(self.node);
if node.anim_channels.len() <= channel {
node.anim_channels.resize(channel + 1, None);
}
if !cfg.enabled {
node.anim_channels[channel] = Some(target);
return target;
}
let dur = (if duration_ms > 0.0 { duration_ms } else { cfg.duration_ms }).max(1.0);
let (val, settled) = match node.anim_channels[channel] {
None => (target, true),
Some(cur) => {
let dt = rosace_animate::frame_dt();
let alpha = 1.0 - (-dt * (1000.0 / dur)).exp();
let next = cur + (target - cur) * alpha;
let settled = (next - target).abs() < 0.001;
(if settled { target } else { next }, settled)
}
};
node.anim_channels[channel] = Some(val);
drop(tree);
if !settled { crate::tree::request_animation(); }
val
}
pub fn seed_channel_if_unset(&self, channel: usize, value: f32) {
let mut tree = self.tree.borrow_mut();
let node = tree.node_mut(self.node);
if node.anim_channels.len() <= channel {
node.anim_channels.resize(channel + 1, None);
}
if node.anim_channels[channel].is_none() {
node.anim_channels[channel] = Some(value);
}
}
pub fn anim_channel(&self, channel: usize) -> Option<f32> {
self.tree.borrow().node(self.node).anim_channels.get(channel).copied().flatten()
}
pub fn pointer(&self) -> Point {
let (x, y) = crate::tree::current_pointer();
Point { x, y }
}
pub fn set_anim_channel(&self, channel: usize, value: f32) {
let mut tree = self.tree.borrow_mut();
let node = tree.node_mut(self.node);
if node.anim_channels.len() <= channel { node.anim_channels.resize(channel + 1, None); }
node.anim_channels[channel] = Some(value);
}
pub fn record(&mut self, cmd: DrawCommand) {
self.recorder.push(cmd);
}
pub fn layout_ctx(&self, constraints: Constraints) -> LayoutCtx<'_> {
LayoutCtx::new(constraints, self.font, &self.theme)
}
}
pub struct LayoutCtx<'a> {
pub constraints: Constraints,
pub font: &'a FontCache,
pub theme: &'a ThemeData,
}
impl<'a> LayoutCtx<'a> {
pub fn new(constraints: Constraints, font: &'a FontCache, theme: &'a ThemeData) -> Self {
Self { constraints, font, theme }
}
pub fn with_constraints(&self, constraints: Constraints) -> LayoutCtx<'_> {
LayoutCtx { constraints, font: self.font, theme: self.theme }
}
}
pub enum Children<'a> {
None,
One(&'a dyn Widget),
Many(&'a [BoxedWidget]),
}
pub trait Widget: Send + Sync {
fn children(&self) -> Children<'_> { Children::None }
fn layout(&self, ctx: &LayoutCtx) -> Size {
match self.children() {
Children::None => ctx.constraints.constrain(Size { width: 0.0, height: 0.0 }),
Children::One(c) => c.layout(ctx),
Children::Many(cs) => {
let mut s = Size { width: 0.0, height: 0.0 };
for c in cs {
let cz = c.layout(ctx);
s.width = s.width.max(cz.width);
s.height = s.height.max(cz.height);
}
ctx.constraints.constrain(s)
}
}
}
fn paint(&self, ctx: &mut PaintCtx) {
match self.children() {
Children::None => {}
Children::One(c) => {
let r = ctx.rect;
c.paint(&mut ctx.child(r));
}
Children::Many(cs) => {
let r = ctx.rect;
for c in cs {
c.paint(&mut ctx.child(r));
}
}
}
}
fn flex_factor(&self) -> f32 {
match self.children() {
Children::One(c) => c.flex_factor(),
_ => 0.0,
}
}
fn into_element(self) -> Element
where
Self: Sized + 'static,
{
Element::Native(NativeElement {
tag: std::any::type_name::<Self>(),
payload: Some(Arc::new(WidgetBox(Box::new(self)))),
children: vec![],
key: None,
})
}
}
pub type BoxedWidget = Box<dyn Widget>;
impl Widget for Box<dyn Widget> {
fn children(&self) -> Children<'_> { (**self).children() }
fn layout(&self, ctx: &LayoutCtx) -> Size { (**self).layout(ctx) }
fn paint(&self, ctx: &mut PaintCtx) { (**self).paint(ctx) }
fn flex_factor(&self) -> f32 { (**self).flex_factor() }
}
pub struct WidgetBox(pub Box<dyn Widget>);
impl WidgetPayload for WidgetBox {
fn as_any(&self) -> &dyn std::any::Any { self }
}
pub(crate) fn avail_w(c: Constraints) -> f32 { c.max_width_f32() }
pub(crate) fn avail_h(c: Constraints) -> f32 { c.max_height_f32() }
pub(crate) fn vcenter_text_y(box_top: f32, box_h: f32, font: &rosace_render::FontCache, px: f32) -> f32 {
box_top + (box_h - font.line_height(px)) / 2.0
}
pub(crate) fn rect_at(origin: Point, size: Size) -> Rect {
Rect { origin, size }
}
pub(crate) fn offset(base: Point, dx: f32, dy: f32) -> Point {
Point { x: base.x + dx, y: base.y + dy }
}
pub(crate) fn intersect_rect(a: Rect, b: Rect) -> Option<Rect> {
let x0 = a.origin.x.max(b.origin.x);
let y0 = a.origin.y.max(b.origin.y);
let x1 = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
let y1 = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
if x1 > x0 && y1 > y0 {
Some(Rect { origin: Point { x: x0, y: y0 }, size: Size { width: x1 - x0, height: y1 - y0 } })
} else {
None
}
}