use smol_str::SmolStr;
use crate::{
AnimationManager,
Background,
Border,
BoxShadow,
BoxShadowCommand,
Color,
Constraints,
EventCtx,
EventStatus,
InputEvent,
Interaction,
LayoutBox,
Length,
MeasureContext,
MeasureResult,
Outline,
PaintContext,
RectCommand,
Style,
WidgetId,
properties::StyleValue,
};
use std::any::Any;
#[derive(Clone, Debug)]
pub struct NativeTextInputSnapshot {
pub value: String,
pub placeholder: String,
pub max_length: Option<usize>,
pub read_only: bool,
}
pub trait Widget: Any {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn debug_name(&self) -> &'static str {
"Widget"
}
fn get_key(&self) -> Option<&SmolStr> {
None
}
fn is_dirty(&self) -> bool;
fn set_dirty(&mut self, dirty: bool);
fn style(&self) -> &Style;
fn style_mut(&mut self) -> &mut Style;
fn computed_style(&self) -> &Style {
self.style()
}
fn on_mount(&mut self) {}
fn on_unmount(&mut self) {}
fn children(&self) -> &[Box<dyn Widget>] {
&[]
}
fn children_mut(&mut self) -> Option<&mut Vec<Box<dyn Widget>>> {
None
}
fn scroll_offset(&self) -> (f32, f32) {
(0.0, 0.0)
}
fn set_content_size(&mut self, _size: (f32, f32)) {}
fn clip_children(&self) -> Option<(f32, f32, f32, f32)> {
None
}
fn measure(&self, ctx: &mut MeasureContext, constraints: Constraints) -> MeasureResult;
fn on_layout_pass(&self, _ctx: &mut MeasureContext) {}
fn layout(&mut self, rect: LayoutBox);
fn layout_box(&self) -> &LayoutBox;
fn paint(&self, ctx: &mut PaintContext);
fn paint_overlay(&self, _ctx: &mut PaintContext) {}
fn paint_box(&self, ctx: &mut PaintContext) {
let style = self.computed_style();
let sf = ctx.scale_factor;
let layout = *self.layout_box();
let radius = style.border
.as_ref()
.and_then(|b| b.radius)
.map(|r| r.to_physical(sf))
.unwrap_or(0.0);
if let Some(shadows) = &style.box_shadow {
for shadow in shadows
.iter()
.rev()
.filter(|s| !s.inset) {
self.paint_shadow_layer(ctx, layout, radius, shadow, sf);
}
}
if style.background.is_some() || style.border.is_some() {
let border = style.border.as_ref();
if border.is_some_and(|b| !b.is_uniform()) {
if style.background.is_some() {
ctx.draw_rect(RectCommand {
position: (layout.x, layout.y),
size: (layout.width, layout.height),
background: style.background.clone(),
border_radius: None,
border_color: None,
border_width: None,
clip_rect: None,
});
}
self.paint_edge_borders(ctx, layout, border.unwrap(), sf);
} else {
ctx.draw_rect(RectCommand {
position: (layout.x, layout.y),
size: (layout.width, layout.height),
background: style.background.clone(),
border_radius: border
.and_then(|b| b.radius)
.map(|r| Length::px(r.to_physical(sf))),
border_color: border.map(|b| b.color),
border_width: border.map(|b| Length::px(b.top.to_physical(sf))),
clip_rect: None,
});
}
}
if let Some(shadows) = &style.box_shadow {
for shadow in shadows
.iter()
.rev()
.filter(|s| s.inset) {
self.paint_shadow_layer(ctx, layout, radius, shadow, sf);
}
}
}
fn paint_edge_borders(&self, ctx: &mut PaintContext, layout: LayoutBox, b: &Border, sf: f32) {
let top = b.top.to_physical(sf);
let right = b.right.to_physical(sf);
let bottom = b.bottom.to_physical(sf);
let left = b.left.to_physical(sf);
let mut edge = |x: f32, y: f32, w: f32, h: f32| {
ctx.draw_rect(RectCommand {
position: (x, y),
size: (w, h),
background: Some(Background::Color(b.color)),
border_radius: None,
border_width: None,
border_color: None,
clip_rect: None,
});
};
if top > 0.0 {
edge(layout.x, layout.y, layout.width, top);
}
if bottom > 0.0 {
edge(layout.x, layout.y + layout.height - bottom, layout.width, bottom);
}
if left > 0.0 {
edge(layout.x, layout.y, left, layout.height);
}
if right > 0.0 {
edge(layout.x + layout.width - right, layout.y, right, layout.height);
}
}
fn paint_shadow_layer(
&self,
ctx: &mut PaintContext,
layout: LayoutBox,
radius: f32,
shadow: &BoxShadow,
sf: f32
) {
let ox = shadow.offset_x.to_physical(sf);
let oy = shadow.offset_y.to_physical(sf);
let blur = shadow.blur_radius.to_physical(sf).max(0.0);
let spread = shadow.spread_radius.to_physical(sf);
let cx = layout.x + layout.width * 0.5;
let cy = layout.y + layout.height * 0.5;
let (shadow_position, shadow_size, shadow_radius) = if shadow.inset {
let half_w = (layout.width * 0.5 - spread).max(0.0);
let half_h = (layout.height * 0.5 - spread).max(0.0);
(
(cx + ox - half_w, cy + oy - half_h),
(half_w * 2.0, half_h * 2.0),
(radius - spread).max(0.0),
)
} else {
let half_w = layout.width * 0.5 + spread;
let half_h = layout.height * 0.5 + spread;
(
(cx + ox - half_w, cy + oy - half_h),
(half_w * 2.0, half_h * 2.0),
(radius + spread).max(0.0),
)
};
ctx.draw_box_shadow(BoxShadowCommand {
shadow_position,
shadow_size,
shadow_radius,
blur,
color: shadow.color,
inset: shadow.inset,
box_position: (layout.x, layout.y),
box_size: (layout.width, layout.height),
box_radius: radius,
clip_rect: None,
});
}
fn paint_outline(&self, ctx: &mut PaintContext) {
if self.interaction().is_some_and(|i| i.focused && i.focus_visible) {
return;
}
let style = self.computed_style();
let outline = match &style.outline {
StyleValue::None => {
return;
}
StyleValue::Value(outline) => outline,
StyleValue::Default => {
return;
}
};
let sf = ctx.scale_factor;
let layout = self.layout_box();
let offset = outline.offset.to_physical(sf);
let radius = outline.radius
.or_else(|| style.border.as_ref().and_then(|b| b.radius))
.map(|r| Length::px(r.to_physical(sf)));
ctx.draw_rect(RectCommand {
position: (layout.x - offset, layout.y - offset),
size: (layout.width + offset * 2.0, layout.height + offset * 2.0),
background: None,
border_radius: radius,
border_color: Some(outline.color),
border_width: Some(Length::px(outline.width.to_physical(sf))),
clip_rect: None,
});
}
fn paint_focus(&self, ctx: &mut PaintContext) {
let Some(interaction) = self.interaction() else {
return;
};
if !interaction.focused || !interaction.focus_visible {
return;
}
let style = self.computed_style();
let layout = self.layout_box();
let outline = match &style.outline {
StyleValue::None => {
return;
}
StyleValue::Value(outline) => *outline,
StyleValue::Default =>
Outline {
width: Length::px(2.5),
color: Color::BLUE_500,
radius: style.border
.as_ref()
.and_then(|b| b.radius)
.map(|r| r.add_px(4.0)),
offset: Length::px(4.0),
},
};
let sf = ctx.scale_factor;
let offset = outline.offset.to_physical(sf);
let radius = outline.radius
.or_else(|| style.border.as_ref().and_then(|b| b.radius))
.map(|r| Length::px(r.to_physical(sf)));
ctx.draw_rect(RectCommand {
position: (layout.x - offset, layout.y - offset),
size: (layout.width + offset * 2.0, layout.height + offset * 2.0),
background: None,
border_radius: radius,
border_width: Some(Length::px(outline.width.to_physical(sf))),
border_color: Some(outline.color),
clip_rect: None,
});
}
fn paint_top(&self, _ctx: &mut PaintContext) {}
fn hit_test(&self, point: (f32, f32)) -> bool {
let b = self.layout_box();
if point.0 < b.x || point.0 > b.x + b.width || point.1 < b.y || point.1 > b.y + b.height {
return false;
}
let Some(border) = &self.style().border else {
return true;
};
let radius: f32 = border.radius.unwrap_or(Length::Px(0.0)).value();
if radius <= 0.0 {
return true;
}
let r = radius.min(b.width * 0.5).min(b.height * 0.5);
let local_x = point.0 - b.x;
let local_y = point.1 - b.y;
if local_x >= r && local_x <= b.width - r {
return true;
}
if local_y >= r && local_y <= b.height - r {
return true;
}
let cx = if local_x < r { r } else { b.width - r };
let cy = if local_y < r { r } else { b.height - r };
let dx = local_x - cx;
let dy = local_y - cy;
dx * dx + dy * dy <= r * r
}
fn blocks_children_hit_test(&self, _point: (f32, f32)) -> bool {
false
}
fn interaction(&self) -> Option<&Interaction> {
None
}
fn interaction_mut(&mut self) -> Option<&mut Interaction> {
None
}
fn transfer_interaction_state(&mut self, old: &dyn Widget) {
if let (Some(new), Some(old)) = (self.interaction_mut(), old.interaction()) {
new.transfer_from(old);
}
}
fn event(&mut self, event: &InputEvent, ctx: &mut EventCtx) -> EventStatus {
let status = match self.interaction_mut() {
Some(interaction) if interaction.is_active() => interaction.handle(event, ctx),
_ => EventStatus::Ignored,
};
if matches!(status, EventStatus::Handled) {
self.set_dirty(true);
}
status
}
fn content_eq(&self, _other: &dyn Widget) -> bool {
false
}
fn cascade_style(&mut self, parent: &Style, anim: &mut AnimationManager) {
if let Some(children) = self.children_mut() {
for child in children.iter_mut() {
child.cascade_style(parent, anim);
}
}
}
fn after_interaction_transfer(&mut self) {}
fn transfer_measured_state(&mut self, _old: &dyn Widget) {}
fn blink_interval(&self) -> Option<web_time::Duration> {
None
}
fn wants_animation_frame(&self) -> bool {
false
}
fn selectable_text(&self) -> Option<&str> {
None
}
fn text_selection(&self) -> Option<(usize, usize)> {
None
}
fn set_text_selection(&mut self, _range: Option<(usize, usize)>) {}
fn cancel_text_selection(&mut self) {
self.set_text_selection(None);
}
fn text_index_at(&self, _point: (f32, f32)) -> usize {
0
}
fn select_all_text(&mut self) {}
fn anim_id(&self) -> WidgetId {
WidgetId::default()
}
fn native_text_input(&self) -> Option<NativeTextInputSnapshot> {
None
}
fn set_native_text_value(&mut self, _value: &str, _ctx: &mut EventCtx) {}
#[cfg(target_arch = "wasm32")]
fn sync_native_input(&self, _input: &web_sys::HtmlInputElement) {}
}
pub fn scaled_layout_box(rect: LayoutBox, scale: f32) -> LayoutBox {
let cx = rect.x + rect.width * 0.5;
let cy = rect.y + rect.height * 0.5;
let w = rect.width * scale;
let h = rect.height * scale;
LayoutBox {
x: cx - w * 0.5,
y: cy - h * 0.5,
width: w,
height: h,
}
}