use crate::core::{Color, Font, HorizontalAlignment, ObjectId, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::layout::LayoutParams;
use crate::layout::{AxisHints, Hints};
use crate::render::RenderContext;
use crate::widget::capability::coercion::{expect_bool, expect_f32, expect_string, expect_u32};
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
#[derive(Debug, Clone, PartialEq)]
pub struct Content {
label: String,
size: Size,
child: Option<ObjectId>,
}
impl Content {
pub fn new() -> Self {
Self { label: String::new(), size: Size::new(0, 0), child: None }
}
pub fn label(&self) -> &str {
&self.label
}
pub fn size(&self) -> Size {
self.size
}
pub fn child(&self) -> Option<ObjectId> {
self.child
}
pub fn set_label(&mut self, label: String) -> bool {
if self.label == label {
return false;
}
self.label = label;
true
}
pub fn set_size(&mut self, size: Size) -> bool {
if self.size == size {
return false;
}
self.size = size;
true
}
fn get(&self, name: &str) -> Option<CapabilityValue> {
match name {
"content" => Some(CapabilityValue::String(self.label.clone())),
"content_width" => Some(CapabilityValue::UInt(u64::from(self.size.width))),
"content_height" => Some(CapabilityValue::UInt(u64::from(self.size.height))),
_ => None,
}
}
fn set(
&mut self,
name: &str,
value: CapabilityValue,
) -> Option<Result<(), CapabilityAccessError>> {
let result = match name {
"content" => expect_string(value).map(|label| {
self.set_label(label);
}),
"content_width" => expect_u32(value).map(|width| {
self.set_size(Size::new(width, self.size.height));
}),
"content_height" => expect_u32(value).map(|height| {
self.set_size(Size::new(self.size.width, height));
}),
_ => return None,
};
Some(result)
}
}
impl Default for Content {
fn default() -> Self {
Self::new()
}
}
fn mount_content_child(
base: &mut BaseWidget,
slot: &mut Option<ObjectId>,
widget: Option<ObjectId>,
) -> bool {
if *slot == widget {
return false;
}
if let Some(previous) = slot.take() {
base.remove_child_linked(previous);
crate::set_widget_visible(previous, false);
}
*slot = widget;
if let Some(widget_id) = widget {
base.add_child_linked(widget_id);
crate::set_widget_visible(widget_id, true);
}
true
}
fn place_mounted_child(slot: Option<ObjectId>, rect: Rect) {
let Some(widget_id) = slot else {
return;
};
if crate::widget_geometry(widget_id) == Some((rect.x, rect.y, rect.width, rect.height)) {
return;
}
crate::set_widget_geometry(widget_id, rect.x, rect.y, rect.width, rect.height);
}
fn center_factor_extent(parent: u32, factor: f32) -> u32 {
let factor = if factor.is_finite() { factor.clamp(0.0, 1.0) } else { 1.0 };
if factor == 0.0 || parent == 0 {
return 0;
}
((parent as f32 * factor).round() as u32).min(parent).max(1)
}
fn aspect_to_px(extent: f32) -> u32 {
if extent <= 0.0 {
return 0;
}
let rounded = extent.round();
if rounded >= 1.0 {
rounded as u32
} else {
1
}
}
fn expect_object_id(value: CapabilityValue) -> Result<Option<ObjectId>, CapabilityAccessError> {
match value {
CapabilityValue::Null => Ok(None),
CapabilityValue::UInt(raw) => Ok(Some(ObjectId::from(raw))),
CapabilityValue::Int(raw) if raw >= 0 => Ok(Some(ObjectId::from(raw as u64))),
_ => Err(CapabilityAccessError::TypeMismatch),
}
}
fn object_id_value(slot: Option<ObjectId>) -> CapabilityValue {
match slot {
Some(id) => CapabilityValue::UInt(id),
None => CapabilityValue::Null,
}
}
pub(crate) struct WrapperPalette {
pub(crate) surface: Color,
pub(crate) ink: Color,
pub(crate) accent: Color,
pub(crate) font: Font,
}
fn wrapper_palette(base: &BaseWidget, role: &str) -> WrapperPalette {
let style = base.style();
let themed = crate::style::resolved_theme_style_shared(role);
let (theme_background, theme_foreground, theme_accent) = {
let manager = crate::style::theme_manager();
match manager.current_theme() {
Some(theme) => (theme.colors.background, theme.colors.foreground, theme.colors.accent),
None => (Color::WHITE, Color::BLACK, Color::PRIMARY),
}
};
let surface = style
.background_color
.or_else(|| themed.as_ref().and_then(|resolved| resolved.background_color))
.unwrap_or(theme_background);
let ink = style
.text_color
.or_else(|| themed.as_ref().and_then(|resolved| resolved.text_color))
.unwrap_or(theme_foreground);
let accent = themed.as_ref().and_then(|resolved| resolved.border_color).unwrap_or(theme_accent);
let font = style
.font
.clone()
.or_else(|| themed.as_ref().and_then(|resolved| resolved.font.clone()))
.unwrap_or_else(|| Font::simple("Arial", 12.0));
WrapperPalette { surface, ink, accent, font }
}
fn paint_wrapper(
ctx: &mut RenderContext,
base: &BaseWidget,
role: &str,
outer: Rect,
content: Rect,
content_box: &Content,
) {
if outer.width == 0 || outer.height == 0 {
return;
}
let palette = wrapper_palette(base, role);
ctx.draw_rect(outer, palette.ink.blend(&palette.surface, 0.7));
if content.width == 0 || content.height == 0 {
return;
}
let tint = palette.accent.blend(&palette.surface, 0.82);
let radius = (content.height.min(content.width) / 8).clamp(2, 6);
ctx.fill_rounded_rect(content, radius, tint);
ctx.draw_rounded_rect_stroke(content, radius, palette.accent.with_alpha_f32(0.55), 1);
if content_box.label().is_empty() || content_box.child().is_some() {
return;
}
let label_ink = tint.contrast_color();
ctx.draw_text_line(
content,
content_box.label(),
&palette.font,
label_ink,
HorizontalAlignment::Center,
);
}
pub struct Spacer {
base: BaseWidget,
flex: u32,
base_size: u32,
paint_hint: bool,
content_child: Option<ObjectId>,
}
impl Spacer {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Spacer, geometry, "Spacer"),
flex: 1,
base_size: 0,
paint_hint: true,
content_child: None,
}
}
pub fn flex(&self) -> u32 {
self.flex
}
pub fn set_flex(&mut self, flex: u32) {
if self.flex == flex {
return;
}
self.flex = flex;
self.base.request_layout();
self.base.request_redraw();
}
pub fn base_size(&self) -> u32 {
self.base_size
}
pub fn set_base_size(&mut self, size: u32) {
if self.base_size == size {
return;
}
self.base_size = size;
self.base.request_layout();
}
pub fn paint_hint(&self) -> bool {
self.paint_hint
}
pub fn set_paint_hint(&mut self, paint_hint: bool) {
if self.paint_hint == paint_hint {
return;
}
self.paint_hint = paint_hint;
self.base.request_redraw();
}
pub fn widget(&self) -> Option<ObjectId> {
self.content_child
}
pub fn set_widget(&mut self, widget: Option<ObjectId>) {
if mount_content_child(&mut self.base, &mut self.content_child, widget) {
place_mounted_child(self.content_child, self.geometry());
self.base.request_layout();
self.base.request_redraw();
}
}
}
impl Widget for Spacer {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
Size::new(self.base_size, self.base_size)
}
fn hints(&self) -> Hints {
Hints {
width: AxisHints::new(0, self.base_size, u32::MAX),
height: AxisHints::new(0, self.base_size, u32::MAX),
}
}
fn default_layout_params(&self) -> LayoutParams {
LayoutParams::stretched(self.flex)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for Spacer {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"flex" => Ok(CapabilityValue::UInt(u64::from(self.flex))),
"base_size" => Ok(CapabilityValue::UInt(u64::from(self.base_size))),
"paint_hint" => Ok(CapabilityValue::Bool(self.paint_hint)),
"widget" => Ok(object_id_value(self.content_child)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
"flex" => {
self.set_flex(expect_u32(value)?);
Ok(())
}
"base_size" => {
self.set_base_size(expect_u32(value)?);
Ok(())
}
"paint_hint" => {
self.set_paint_hint(expect_bool(value)?);
Ok(())
}
"widget" => {
self.set_widget(expect_object_id(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["flex", "base_size", "paint_hint", "widget", BASE_PROPERTY_NAMES]
}
}
impl Draw for Spacer {
fn draw(&mut self, ctx: &mut RenderContext) {
place_mounted_child(self.content_child, self.geometry());
if !self.paint_hint {
return;
}
let rect = self.geometry();
if rect.width == 0 || rect.height == 0 {
return;
}
let palette = wrapper_palette(&self.base, "spacer");
let color = palette.ink.blend(&palette.surface, 0.55);
paint_dashed_frame(ctx, rect, color);
}
}
pub(crate) fn paint_dashed_frame(ctx: &mut RenderContext, rect: Rect, color: Color) {
const DASH: u32 = 4;
const GAP: u32 = 4;
let step = DASH + GAP;
if rect.width == 0 || rect.height == 0 {
return;
}
let bottom = rect.y.saturating_add(rect.height as i32).saturating_sub(1);
let mut x = 0;
while x < rect.width {
let dash = DASH.min(rect.width - x);
ctx.fill_rect(Rect::new(rect.x.saturating_add(x as i32), rect.y, dash, 1), color);
ctx.fill_rect(Rect::new(rect.x.saturating_add(x as i32), bottom, dash, 1), color);
x = x.saturating_add(step);
}
let right = rect.x.saturating_add(rect.width as i32).saturating_sub(1);
let mut y = 0;
while y < rect.height {
let dash = DASH.min(rect.height - y);
ctx.fill_rect(Rect::new(rect.x, rect.y.saturating_add(y as i32), 1, dash), color);
ctx.fill_rect(Rect::new(right, rect.y.saturating_add(y as i32), 1, dash), color);
y = y.saturating_add(step);
}
}
impl EventHandler for Spacer {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
pub struct Center {
base: BaseWidget,
content: Content,
width_factor: f32,
height_factor: f32,
}
impl Center {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Center, geometry, "Center"),
content: Content::new(),
width_factor: 1.0,
height_factor: 1.0,
}
}
pub fn content(&self) -> &Content {
&self.content
}
pub fn widget(&self) -> Option<ObjectId> {
self.content.child
}
pub fn set_widget(&mut self, widget: Option<ObjectId>) {
if mount_content_child(&mut self.base, &mut self.content.child, widget) {
place_mounted_child(self.content.child, self.content_rect());
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn set_content_label(&mut self, label: String) {
if self.content.set_label(label) {
self.base.request_redraw();
}
}
pub fn set_content_size(&mut self, size: Size) {
if self.content.set_size(size) {
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn width_factor(&self) -> f32 {
self.width_factor
}
pub fn set_width_factor(&mut self, factor: f32) {
if self.width_factor == factor {
return;
}
self.width_factor = factor;
self.base.request_redraw();
}
pub fn height_factor(&self) -> f32 {
self.height_factor
}
pub fn set_height_factor(&mut self, factor: f32) {
if self.height_factor == factor {
return;
}
self.height_factor = factor;
self.base.request_redraw();
}
pub fn content_rect(&self) -> Rect {
let rect = self.geometry();
let width = center_factor_extent(rect.width, self.width_factor);
let height = center_factor_extent(rect.height, self.height_factor);
Rect::new(
rect.x + (rect.width.saturating_sub(width) / 2) as i32,
rect.y + (rect.height.saturating_sub(height) / 2) as i32,
width,
height,
)
}
}
impl Widget for Center {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
self.content.size()
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for Center {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
if let Some(value) = self.content.get(name) {
return Ok(value);
}
match name {
"width_factor" => Ok(CapabilityValue::Float(f64::from(self.width_factor))),
"height_factor" => Ok(CapabilityValue::Float(f64::from(self.height_factor))),
"widget" => Ok(object_id_value(self.content.child)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
if let Some(result) = self.content.set(name, value.clone()) {
if result.is_ok() {
self.base.request_layout();
self.base.request_redraw();
}
return result;
}
match name {
"width_factor" => {
self.set_width_factor(expect_f32(value)?);
Ok(())
}
"height_factor" => {
self.set_height_factor(expect_f32(value)?);
Ok(())
}
"widget" => {
self.set_widget(expect_object_id(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"content",
"content_width",
"content_height",
"width_factor",
"height_factor",
"widget",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for Center {
fn draw(&mut self, ctx: &mut RenderContext) {
let outer = self.geometry();
let content = self.content_rect();
place_mounted_child(self.content.child, content);
paint_wrapper(ctx, &self.base, "center", outer, content, &self.content);
}
}
impl EventHandler for Center {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AlignPosition {
TopLeft,
TopCentre,
TopRight,
CentreLeft,
#[default]
Centre,
CentreRight,
BottomLeft,
BottomCentre,
BottomRight,
}
impl AlignPosition {
pub const ALL: [Self; 9] = [
Self::TopLeft,
Self::TopCentre,
Self::TopRight,
Self::CentreLeft,
Self::Centre,
Self::CentreRight,
Self::BottomLeft,
Self::BottomCentre,
Self::BottomRight,
];
pub const fn token(self) -> &'static str {
match self {
Self::TopLeft => "top_left",
Self::TopCentre => "top_centre",
Self::TopRight => "top_right",
Self::CentreLeft => "centre_left",
Self::Centre => "centre",
Self::CentreRight => "centre_right",
Self::BottomLeft => "bottom_left",
Self::BottomCentre => "bottom_centre",
Self::BottomRight => "bottom_right",
}
}
pub fn from_token(token: &str) -> Option<Self> {
Self::ALL.into_iter().find(|anchor| anchor.token() == token)
}
fn horizontal(self) -> Anchor {
match self {
Self::TopLeft | Self::CentreLeft | Self::BottomLeft => Anchor::Start,
Self::TopCentre | Self::Centre | Self::BottomCentre => Anchor::Middle,
Self::TopRight | Self::CentreRight | Self::BottomRight => Anchor::End,
}
}
fn vertical(self) -> Anchor {
match self {
Self::TopLeft | Self::TopCentre | Self::TopRight => Anchor::Start,
Self::CentreLeft | Self::Centre | Self::CentreRight => Anchor::Middle,
Self::BottomLeft | Self::BottomCentre | Self::BottomRight => Anchor::End,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Anchor {
Start,
Middle,
End,
}
fn anchored_offset(available: u32, content: u32, anchor: Anchor) -> i32 {
let free = available.saturating_sub(content);
match anchor {
Anchor::Start => 0,
Anchor::Middle => (free / 2) as i32,
Anchor::End => free as i32,
}
}
pub struct Align {
base: BaseWidget,
content: Content,
alignment: AlignPosition,
}
impl Align {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Align, geometry, "Align"),
content: Content::new(),
alignment: AlignPosition::default(),
}
}
pub fn content(&self) -> &Content {
&self.content
}
pub fn widget(&self) -> Option<ObjectId> {
self.content.child
}
pub fn set_widget(&mut self, widget: Option<ObjectId>) {
if mount_content_child(&mut self.base, &mut self.content.child, widget) {
place_mounted_child(self.content.child, self.content_rect());
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn set_content_label(&mut self, label: String) {
if self.content.set_label(label) {
self.base.request_redraw();
}
}
pub fn set_content_size(&mut self, size: Size) {
if self.content.set_size(size) {
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn alignment(&self) -> AlignPosition {
self.alignment
}
pub fn set_alignment(&mut self, alignment: AlignPosition) {
if self.alignment == alignment {
return;
}
self.alignment = alignment;
self.base.request_redraw();
}
pub fn content_rect(&self) -> Rect {
let outer = self.geometry();
let size = Size::new(
self.content.size().width.min(outer.width),
self.content.size().height.min(outer.height),
);
let dx = anchored_offset(outer.width, size.width, self.alignment.horizontal());
let dy = anchored_offset(outer.height, size.height, self.alignment.vertical());
Rect::new(outer.x.saturating_add(dx), outer.y.saturating_add(dy), size.width, size.height)
}
}
impl Widget for Align {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
self.content.size()
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for Align {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
if let Some(value) = self.content.get(name) {
return Ok(value);
}
match name {
"alignment" => Ok(CapabilityValue::String(self.alignment.token().to_string())),
"widget" => Ok(object_id_value(self.content.child)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
if let Some(result) = self.content.set(name, value.clone()) {
if result.is_ok() {
self.base.request_layout();
self.base.request_redraw();
}
return result;
}
match name {
"alignment" => {
let token = expect_string(value)?;
let alignment =
AlignPosition::from_token(&token).ok_or(CapabilityAccessError::OutOfRange)?;
self.set_alignment(alignment);
Ok(())
}
"widget" => {
self.set_widget(expect_object_id(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"content",
"content_width",
"content_height",
"alignment",
"widget",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for Align {
fn draw(&mut self, ctx: &mut RenderContext) {
let outer = self.geometry();
let content = self.content_rect();
place_mounted_child(self.content.child, content);
paint_wrapper(ctx, &self.base, "align", outer, content, &self.content);
}
}
impl EventHandler for Align {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
pub struct SizedBox {
base: BaseWidget,
content: Content,
fixed: Size,
}
impl SizedBox {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::SizedBox, geometry, "SizedBox"),
content: Content::new(),
fixed: Size::new(geometry.width, geometry.height),
}
}
pub fn content(&self) -> &Content {
&self.content
}
pub fn widget(&self) -> Option<ObjectId> {
self.content.child
}
pub fn set_widget(&mut self, widget: Option<ObjectId>) {
if mount_content_child(&mut self.base, &mut self.content.child, widget) {
place_mounted_child(self.content.child, self.content_rect());
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn set_content_label(&mut self, label: String) {
if self.content.set_label(label) {
self.base.request_redraw();
}
}
pub fn set_content_size(&mut self, size: Size) {
if self.content.set_size(size) {
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn box_size(&self) -> Size {
self.fixed
}
pub fn set_box_size(&mut self, size: Size) {
if self.fixed == size {
return;
}
self.fixed = size;
self.base.request_layout();
self.base.request_redraw();
}
pub fn content_rect(&self) -> Rect {
let outer = self.geometry();
let width = self.fixed.width.min(outer.width);
let height = self.fixed.height.min(outer.height);
Rect::new(
outer.x.saturating_add(((outer.width - width) / 2) as i32),
outer.y.saturating_add(((outer.height - height) / 2) as i32),
width,
height,
)
}
}
impl Widget for SizedBox {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
self.fixed
}
fn hints(&self) -> Hints {
Hints::fixed(self.fixed.width, self.fixed.height)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for SizedBox {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
if let Some(value) = self.content.get(name) {
return Ok(value);
}
match name {
"box_width" => Ok(CapabilityValue::UInt(u64::from(self.fixed.width))),
"box_height" => Ok(CapabilityValue::UInt(u64::from(self.fixed.height))),
"widget" => Ok(object_id_value(self.content.child)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
if let Some(result) = self.content.set(name, value.clone()) {
if result.is_ok() {
self.base.request_redraw();
}
return result;
}
match name {
"box_width" => {
let width = expect_u32(value)?;
self.set_box_size(Size::new(width, self.fixed.height));
Ok(())
}
"box_height" => {
let height = expect_u32(value)?;
self.set_box_size(Size::new(self.fixed.width, height));
Ok(())
}
"widget" => {
self.set_widget(expect_object_id(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"content",
"content_width",
"content_height",
"box_width",
"box_height",
"widget",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for SizedBox {
fn draw(&mut self, ctx: &mut RenderContext) {
let outer = self.geometry();
let content = self.content_rect();
place_mounted_child(self.content.child, content);
paint_wrapper(ctx, &self.base, "sized_box", outer, content, &self.content);
}
}
impl EventHandler for SizedBox {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
pub struct Padding {
base: BaseWidget,
content: Content,
top: u32,
right: u32,
bottom: u32,
left: u32,
}
impl Padding {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Padding, geometry, "Padding"),
content: Content::new(),
top: 0,
right: 0,
bottom: 0,
left: 0,
}
}
pub fn content(&self) -> &Content {
&self.content
}
pub fn widget(&self) -> Option<ObjectId> {
self.content.child
}
pub fn set_widget(&mut self, widget: Option<ObjectId>) {
if mount_content_child(&mut self.base, &mut self.content.child, widget) {
place_mounted_child(self.content.child, self.content_rect());
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn set_content_label(&mut self, label: String) {
if self.content.set_label(label) {
self.base.request_redraw();
}
}
pub fn set_content_size(&mut self, size: Size) {
if self.content.set_size(size) {
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn insets(&self) -> (u32, u32, u32, u32) {
(self.top, self.right, self.bottom, self.left)
}
pub fn set_insets(&mut self, top: u32, right: u32, bottom: u32, left: u32) {
if (self.top, self.right, self.bottom, self.left) == (top, right, bottom, left) {
return;
}
self.top = top;
self.right = right;
self.bottom = bottom;
self.left = left;
self.base.request_layout();
self.base.request_redraw();
}
pub fn content_rect(&self) -> Rect {
let outer = self.geometry();
Rect::new(
outer.x.saturating_add(self.left as i32),
outer.y.saturating_add(self.top as i32),
outer.width.saturating_sub(self.left.saturating_add(self.right)),
outer.height.saturating_sub(self.top.saturating_add(self.bottom)),
)
}
}
impl Widget for Padding {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
Size::new(
self.content.size().width.saturating_add(self.left.saturating_add(self.right)),
self.content.size().height.saturating_add(self.top.saturating_add(self.bottom)),
)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for Padding {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
if let Some(value) = self.content.get(name) {
return Ok(value);
}
match name {
"padding_top" => Ok(CapabilityValue::UInt(u64::from(self.top))),
"padding_right" => Ok(CapabilityValue::UInt(u64::from(self.right))),
"padding_bottom" => Ok(CapabilityValue::UInt(u64::from(self.bottom))),
"padding_left" => Ok(CapabilityValue::UInt(u64::from(self.left))),
"widget" => Ok(object_id_value(self.content.child)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
if let Some(result) = self.content.set(name, value.clone()) {
if result.is_ok() {
self.base.request_layout();
self.base.request_redraw();
}
return result;
}
let (top, right, bottom, left) = self.insets();
match name {
"padding_top" => {
self.set_insets(expect_u32(value)?, right, bottom, left);
Ok(())
}
"padding_right" => {
self.set_insets(top, expect_u32(value)?, bottom, left);
Ok(())
}
"padding_bottom" => {
self.set_insets(top, right, expect_u32(value)?, left);
Ok(())
}
"padding_left" => {
self.set_insets(top, right, bottom, expect_u32(value)?);
Ok(())
}
"widget" => {
self.set_widget(expect_object_id(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"content",
"content_width",
"content_height",
"padding_top",
"padding_right",
"padding_bottom",
"padding_left",
"widget",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for Padding {
fn draw(&mut self, ctx: &mut RenderContext) {
let outer = self.geometry();
let content = self.content_rect();
place_mounted_child(self.content.child, content);
paint_wrapper(ctx, &self.base, "padding", outer, content, &self.content);
}
}
impl EventHandler for Padding {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
pub struct Expanded {
base: BaseWidget,
content: Content,
flex: u32,
}
impl Expanded {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Expanded, geometry, "Expanded"),
content: Content::new(),
flex: 1,
}
}
pub fn content(&self) -> &Content {
&self.content
}
pub fn widget(&self) -> Option<ObjectId> {
self.content.child
}
pub fn set_widget(&mut self, widget: Option<ObjectId>) {
if mount_content_child(&mut self.base, &mut self.content.child, widget) {
place_mounted_child(self.content.child, self.content_rect());
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn set_content_label(&mut self, label: String) {
if self.content.set_label(label) {
self.base.request_redraw();
}
}
pub fn set_content_size(&mut self, size: Size) {
if self.content.set_size(size) {
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn flex(&self) -> u32 {
self.flex
}
pub fn set_flex(&mut self, flex: u32) {
if self.flex == flex {
return;
}
self.flex = flex;
self.base.request_layout();
self.base.request_redraw();
}
pub fn content_rect(&self) -> Rect {
self.geometry()
}
}
impl Widget for Expanded {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
self.content.size()
}
fn hints(&self) -> Hints {
Hints {
width: AxisHints::new(0, self.content.size().width, u32::MAX),
height: AxisHints::new(0, self.content.size().height, u32::MAX),
}
}
fn default_layout_params(&self) -> LayoutParams {
LayoutParams::stretched(self.flex)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for Expanded {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
if let Some(value) = self.content.get(name) {
return Ok(value);
}
match name {
"flex" => Ok(CapabilityValue::UInt(u64::from(self.flex))),
"widget" => Ok(object_id_value(self.content.child)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
if let Some(result) = self.content.set(name, value.clone()) {
if result.is_ok() {
self.base.request_layout();
self.base.request_redraw();
}
return result;
}
match name {
"flex" => {
self.set_flex(expect_u32(value)?);
Ok(())
}
"widget" => {
self.set_widget(expect_object_id(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"content",
"content_width",
"content_height",
"flex",
"widget",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for Expanded {
fn draw(&mut self, ctx: &mut RenderContext) {
let outer = self.geometry();
let content = self.content_rect();
place_mounted_child(self.content.child, content);
paint_wrapper(ctx, &self.base, "expanded", outer, content, &self.content);
}
}
impl EventHandler for Expanded {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FlexFit {
#[default]
Loose,
Tight,
}
impl FlexFit {
pub const fn token(self) -> &'static str {
match self {
Self::Loose => "loose",
Self::Tight => "tight",
}
}
pub fn from_token(token: &str) -> Option<Self> {
match token {
"loose" => Some(Self::Loose),
"tight" => Some(Self::Tight),
_ => None,
}
}
}
pub struct Flexible {
base: BaseWidget,
content: Content,
flex: u32,
fit: FlexFit,
}
impl Flexible {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::Flexible, geometry, "Flexible"),
content: Content::new(),
flex: 1,
fit: FlexFit::default(),
}
}
pub fn content(&self) -> &Content {
&self.content
}
pub fn set_content_label(&mut self, label: String) {
if self.content.set_label(label) {
self.base.request_redraw();
}
}
pub fn set_content_size(&mut self, size: Size) {
if self.content.set_size(size) {
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn flex(&self) -> u32 {
self.flex
}
pub fn set_flex(&mut self, flex: u32) {
if self.flex == flex {
return;
}
self.flex = flex;
self.base.request_layout();
self.base.request_redraw();
}
pub fn fit(&self) -> FlexFit {
self.fit
}
pub fn set_fit(&mut self, fit: FlexFit) {
if self.fit == fit {
return;
}
self.fit = fit;
self.base.request_layout();
}
pub fn content_rect(&self) -> Rect {
self.geometry()
}
pub fn widget(&self) -> Option<ObjectId> {
self.content.child
}
pub fn set_widget(&mut self, widget: Option<ObjectId>) {
if mount_content_child(&mut self.base, &mut self.content.child, widget) {
place_mounted_child(self.content.child, self.content_rect());
self.base.request_layout();
self.base.request_redraw();
}
}
}
impl Widget for Flexible {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
self.content.size()
}
fn hints(&self) -> Hints {
let size = self.content.size();
match self.fit {
FlexFit::Loose => Hints {
width: AxisHints::new(0, size.width, u32::MAX),
height: AxisHints::new(0, size.height, u32::MAX),
},
FlexFit::Tight => Hints::fixed(size.width, size.height),
}
}
fn default_layout_params(&self) -> LayoutParams {
let mut params = LayoutParams::stretched(self.flex);
params.fill = matches!(self.fit, FlexFit::Tight);
params
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for Flexible {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
if let Some(value) = self.content.get(name) {
return Ok(value);
}
match name {
"flex" => Ok(CapabilityValue::UInt(u64::from(self.flex))),
"fit" => Ok(CapabilityValue::String(self.fit.token().to_string())),
"widget" => Ok(object_id_value(self.content.child)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
if let Some(result) = self.content.set(name, value.clone()) {
if result.is_ok() {
self.base.request_layout();
self.base.request_redraw();
}
return result;
}
match name {
"flex" => {
self.set_flex(expect_u32(value)?);
Ok(())
}
"fit" => {
let token = expect_string(value)?;
let fit = FlexFit::from_token(&token).ok_or(CapabilityAccessError::OutOfRange)?;
self.set_fit(fit);
Ok(())
}
"widget" => {
self.set_widget(expect_object_id(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"content",
"content_width",
"content_height",
"flex",
"fit",
"widget",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for Flexible {
fn draw(&mut self, ctx: &mut RenderContext) {
let outer = self.geometry();
let content = self.content_rect();
place_mounted_child(self.content.child, content);
paint_wrapper(ctx, &self.base, "flexible", outer, content, &self.content);
}
}
impl EventHandler for Flexible {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
pub struct IntrinsicWidth {
base: BaseWidget,
content: Content,
}
impl IntrinsicWidth {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::IntrinsicWidth, geometry, "IntrinsicWidth"),
content: Content::new(),
}
}
pub fn content(&self) -> &Content {
&self.content
}
pub fn widget(&self) -> Option<ObjectId> {
self.content.child
}
pub fn set_widget(&mut self, widget: Option<ObjectId>) {
if mount_content_child(&mut self.base, &mut self.content.child, widget) {
place_mounted_child(self.content.child, self.content_rect());
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn set_content_label(&mut self, label: String) {
if self.content.set_label(label) {
self.base.request_redraw();
}
}
pub fn set_content_size(&mut self, size: Size) {
if self.content.set_size(size) {
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn content_rect(&self) -> Rect {
let outer = self.geometry();
Rect::new(outer.x, outer.y, self.content.size().width.min(outer.width), outer.height)
}
}
impl Widget for IntrinsicWidth {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
self.content.size()
}
fn hints(&self) -> Hints {
Hints {
width: AxisHints::fixed(self.content.size().width),
height: AxisHints::at_least(self.content.size().height),
}
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for IntrinsicWidth {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
if let Some(value) = self.content.get(name) {
return Ok(value);
}
match name {
"widget" => Ok(object_id_value(self.content.child)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
if let Some(result) = self.content.set(name, value.clone()) {
if result.is_ok() {
self.base.request_layout();
self.base.request_redraw();
}
return result;
}
match name {
"widget" => {
self.set_widget(expect_object_id(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"content",
"content_width",
"content_height",
"widget",
BASE_PROPERTY_NAMES
]
}
}
impl Draw for IntrinsicWidth {
fn draw(&mut self, ctx: &mut RenderContext) {
let outer = self.geometry();
let content = self.content_rect();
place_mounted_child(self.content.child, content);
paint_wrapper(ctx, &self.base, "intrinsic_width", outer, content, &self.content);
}
}
impl EventHandler for IntrinsicWidth {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
pub const DEFAULT_ASPECT_RATIO: f32 = 16.0 / 9.0;
pub struct AspectRatio {
base: BaseWidget,
content: Content,
ratio: f32,
respect_parent: bool,
}
impl AspectRatio {
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::AspectRatio, geometry, "AspectRatio"),
content: Content::new(),
ratio: DEFAULT_ASPECT_RATIO,
respect_parent: true,
}
}
pub fn content(&self) -> &Content {
&self.content
}
pub fn widget(&self) -> Option<ObjectId> {
self.content.child
}
pub fn set_widget(&mut self, widget: Option<ObjectId>) {
if mount_content_child(&mut self.base, &mut self.content.child, widget) {
place_mounted_child(self.content.child, self.content_rect());
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn set_content_label(&mut self, label: String) {
if self.content.set_label(label) {
self.base.request_redraw();
}
}
pub fn set_content_size(&mut self, size: Size) {
if self.content.set_size(size) {
self.base.request_layout();
self.base.request_redraw();
}
}
pub fn ratio(&self) -> f32 {
self.ratio
}
pub fn set_ratio(&mut self, ratio: f32) -> Result<(), CapabilityAccessError> {
if !ratio.is_finite() || ratio <= 0.0 {
return Err(CapabilityAccessError::OutOfRange);
}
if self.ratio == ratio {
return Ok(());
}
self.ratio = ratio;
self.base.request_layout();
self.base.request_redraw();
Ok(())
}
pub fn respect_parent(&self) -> bool {
self.respect_parent
}
pub fn set_respect_parent(&mut self, respect_parent: bool) {
if self.respect_parent == respect_parent {
return;
}
self.respect_parent = respect_parent;
self.base.request_layout();
self.base.request_redraw();
}
pub fn content_rect(&self) -> Rect {
let parent = self.geometry();
let parent_w = parent.width as f32;
let parent_h = parent.height as f32;
let ratio = self.ratio;
let (child_w, child_h) = if !self.respect_parent {
let width = parent_w;
(aspect_to_px(width), aspect_to_px(width / ratio))
} else if parent_w / ratio <= parent_h {
(aspect_to_px(parent_w), aspect_to_px(parent_w / ratio))
} else {
(aspect_to_px(parent_h * ratio), aspect_to_px(parent_h))
};
let x_offset = (((parent.width as i32) - (child_w as i32)) as f32 / 2.0).round() as i32;
let y_offset = (((parent.height as i32) - (child_h as i32)) as f32 / 2.0).round() as i32;
Rect::new(parent.x + x_offset, parent.y + y_offset, child_w, child_h)
}
fn requested_size(&self) -> Size {
let content = self.content.size();
let height = if content.height > 0 {
content.height
} else {
(content.width as f32 / self.ratio).round().max(0.0) as u32
};
let width = if content.width > 0 {
content.width
} else {
(height as f32 * self.ratio).round().max(0.0) as u32
};
Size::new(width, height)
}
}
impl Widget for AspectRatio {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
self.requested_size()
}
fn hints(&self) -> Hints {
let rect = self.content_rect();
Hints::fixed(rect.width, rect.height)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
}
impl WidgetProperties for AspectRatio {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
if let Some(value) = self.content.get(name) {
return Ok(value);
}
match name {
"ratio" => Ok(CapabilityValue::Float(f64::from(self.ratio))),
"respect_parent" => Ok(CapabilityValue::Bool(self.respect_parent)),
"widget" => Ok(object_id_value(self.content.child)),
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
if let Some(result) = self.content.set(name, value.clone()) {
if result.is_ok() {
self.base.request_layout();
self.base.request_redraw();
}
return result;
}
match name {
"ratio" => self.set_ratio(expect_f32(value)?),
"respect_parent" => {
self.set_respect_parent(expect_bool(value)?);
Ok(())
}
"widget" => {
self.set_widget(expect_object_id(value)?);
Ok(())
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of![
"content",
"content_width",
"content_height",
"ratio",
"respect_parent",
"widget",
BASE_PROPERTY_NAMES
]
}
}
fn ratio_label(ratio: f32) -> String {
if ratio.is_finite() && ratio > 0.0 {
for denominator in 1..=32u32 {
let numerator = (ratio * denominator as f32).round();
if numerator < 1.0 {
continue;
}
let candidate = numerator / denominator as f32;
if (candidate - ratio).abs() / ratio < 0.005 {
return format!("{}:{}", numerator as u32, denominator);
}
}
}
format!("{ratio:.2}")
}
fn paint_ratio_guides(
ctx: &mut RenderContext,
base: &BaseWidget,
role: &str,
content: Rect,
ratio: f32,
) {
if content.width == 0 || content.height == 0 {
return;
}
let palette = wrapper_palette(base, role);
let content = Rect::new(
content.x + 1,
content.y + 1,
content.width.saturating_sub(2),
content.height.saturating_sub(2),
);
if content.width == 0 || content.height == 0 {
return;
}
let right = content.x + content.width as i32;
let bottom = content.y + content.height as i32;
let guide = palette.accent.with_alpha_f32(0.35);
ctx.draw_line_aa(Point::new(content.x, content.y), Point::new(right, bottom), guide);
ctx.draw_line_aa(Point::new(right, content.y), Point::new(content.x, bottom), guide);
let tick = (content.height.min(content.width) / 6).clamp(4, 12);
let tick = tick.min(content.width / 2).min(content.height / 2).max(1) as i32;
for (corner_x, corner_y, inward_x, inward_y) in [
(content.x, content.y, 1, 1),
(right, content.y, -1, 1),
(content.x, bottom, 1, -1),
(right, bottom, -1, -1),
] {
ctx.draw_line_stroke_aa(
Point::new(corner_x, corner_y),
Point::new(corner_x + inward_x * tick, corner_y),
palette.accent,
2,
);
ctx.draw_line_stroke_aa(
Point::new(corner_x, corner_y),
Point::new(corner_x, corner_y + inward_y * tick),
palette.accent,
2,
);
}
let advance = palette.font.size().ceil().max(1.0) as u32;
let line_height = advance + 2;
let caption_rect = Rect::new(
content.x + 4,
content
.y
.saturating_add(content.height as i32)
.saturating_sub(line_height as i32)
.saturating_sub(4),
content.width.saturating_sub(8),
line_height,
);
ctx.draw_text_line(
caption_rect,
&ratio_label(ratio),
&palette.font,
palette.ink.with_alpha_f32(0.8),
HorizontalAlignment::Left,
);
}
impl Draw for AspectRatio {
fn draw(&mut self, ctx: &mut RenderContext) {
let outer = self.geometry();
let content = self.content_rect();
place_mounted_child(self.content.child, content);
paint_wrapper(ctx, &self.base, "aspect_ratio", outer, content, &self.content);
if self.content.child.is_none() {
paint_ratio_guides(ctx, &self.base, "aspect_ratio", content, self.ratio);
}
}
}
impl EventHandler for AspectRatio {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
}
}
pub(crate) use self::shared::paint_item_box;
mod shared {
use super::*;
pub(crate) fn paint_item_box(
ctx: &mut RenderContext,
rect: Rect,
label: &str,
palette: &WrapperPalette,
radius: u32,
) {
if rect.width == 0 || rect.height == 0 {
return;
}
let tint = palette.accent.blend(&palette.surface, 0.82);
ctx.fill_rounded_rect(rect, radius, tint);
ctx.draw_rounded_rect_stroke(rect, radius, palette.accent.with_alpha_f32(0.55), 1);
if label.is_empty() {
return;
}
let ink = tint.contrast_color();
ctx.draw_text_line(rect, label, &palette.font, ink, HorizontalAlignment::Center);
}
}
pub(crate) use self::palette_bridge::resolve_palette;
mod palette_bridge {
use super::*;
pub(crate) fn resolve_palette(base: &BaseWidget, role: &str) -> WrapperPalette {
wrapper_palette(base, role)
}
}
#[cfg(test)]
impl Center {
fn with_content_for_test(geometry: Rect, size: Size) -> Self {
let mut center = Self::new(geometry);
center.set_content_size(size);
center
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Point;
#[test]
fn every_wrapper_reports_a_real_size_hint() {
let mut center = Center::new(Rect::new(0, 0, 200, 200));
center.set_content_size(Size::new(80, 40));
assert_eq!(center.size_hint(), Size::new(80, 40));
let mut align = Align::new(Rect::new(0, 0, 200, 200));
align.set_content_size(Size::new(30, 30));
assert_eq!(align.size_hint(), Size::new(30, 30));
let mut padding = Padding::new(Rect::new(0, 0, 200, 200));
padding.set_content_size(Size::new(50, 20));
padding.set_insets(4, 8, 4, 8);
assert_eq!(
padding.size_hint(),
Size::new(66, 28),
"a padding wrapper asks for its content plus its insets"
);
}
#[test]
fn spacer_publishes_the_channels_a_layout_reads() {
let mut spacer = Spacer::new(Rect::new(0, 0, 10, 10));
spacer.set_flex(3);
spacer.set_base_size(12);
assert!(spacer.default_layout_params().fill, "a spacer absorbs leftover room");
assert_eq!(spacer.default_layout_params().stretch, 3);
assert_eq!(spacer.hints().width.pref, 12);
assert_eq!(spacer.hints().width.max, u32::MAX);
}
#[test]
fn expanded_fills_while_flexible_may_stay_loose() {
let mut expanded = Expanded::new(Rect::new(0, 0, 10, 10));
expanded.set_flex(2);
assert!(expanded.default_layout_params().fill);
assert_eq!(expanded.default_layout_params().stretch, 2);
let mut flexible = Flexible::new(Rect::new(0, 0, 10, 10));
assert!(!flexible.default_layout_params().fill, "the default fit is loose");
flexible.set_fit(FlexFit::Tight);
assert!(flexible.default_layout_params().fill, "a tight fit fills its share");
}
#[test]
fn center_rect_is_the_factor_box_centred() {
let mut center =
Center::with_content_for_test(Rect::new(100, 100, 200, 100), Size::new(40, 20));
center.set_width_factor(0.5);
center.set_height_factor(0.5);
let rect = center.content_rect();
assert_eq!(rect, Rect::new(150, 125, 100, 50));
}
#[test]
fn align_reaches_all_nine_anchors() {
let geometry = Rect::new(10, 20, 100, 60);
let expected = [
(AlignPosition::TopLeft, 10, 20),
(AlignPosition::TopCentre, 45, 20),
(AlignPosition::TopRight, 80, 20),
(AlignPosition::CentreLeft, 10, 40),
(AlignPosition::Centre, 45, 40),
(AlignPosition::CentreRight, 80, 40),
(AlignPosition::BottomLeft, 10, 60),
(AlignPosition::BottomCentre, 45, 60),
(AlignPosition::BottomRight, 80, 60),
];
for (anchor, x, y) in expected {
let mut align = Align::new(geometry);
align.set_content_size(Size::new(30, 20));
align.set_alignment(anchor);
let rect = align.content_rect();
assert_eq!((rect.x, rect.y), (x, y), "{anchor:?}");
assert_eq!((rect.width, rect.height), (30, 20), "{anchor:?} keeps its own size");
}
}
#[test]
fn align_tokens_round_trip() {
for anchor in AlignPosition::ALL {
assert_eq!(AlignPosition::from_token(anchor.token()), Some(anchor));
}
assert_eq!(AlignPosition::from_token("middle"), None);
}
#[test]
fn sized_box_pins_its_content_to_the_requested_box() {
let mut sized = SizedBox::new(Rect::new(0, 0, 200, 200));
sized.set_box_size(Size::new(100, 50));
assert_eq!(sized.content_rect(), Rect::new(50, 75, 100, 50));
assert_eq!(sized.hints().width, AxisHints::fixed(100), "a box is a hard constraint");
assert_eq!(sized.hints().height, AxisHints::fixed(50));
}
#[test]
fn padding_saturates_rather_than_underflowing() {
let mut padding = Padding::new(Rect::new(0, 0, 40, 40));
padding.set_insets(30, 30, 30, 30);
let rect = padding.content_rect();
assert_eq!(rect.width, 0);
assert_eq!(rect.height, 0);
}
#[test]
fn intrinsic_width_pins_the_width_and_keeps_the_height() {
let mut intrinsic = IntrinsicWidth::new(Rect::new(0, 0, 200, 80));
intrinsic.set_content_size(Size::new(60, 20));
assert_eq!(intrinsic.content_rect(), Rect::new(0, 0, 60, 80));
assert_eq!(intrinsic.hints().width, AxisHints::fixed(60));
assert_eq!(intrinsic.hints().height.min, 20);
}
#[test]
fn aspect_ratio_refuses_a_ratio_that_describes_no_rectangle() {
let mut ratio = AspectRatio::new(Rect::new(0, 0, 320, 240));
assert!(ratio.set_ratio(0.0).is_err());
assert!(ratio.set_ratio(f32::NAN).is_err());
assert!(ratio.set_ratio(f32::INFINITY).is_err());
assert_eq!(ratio.ratio(), DEFAULT_ASPECT_RATIO);
}
#[test]
fn aspect_ratio_derives_the_missing_extent() {
let mut ratio = AspectRatio::new(Rect::new(0, 0, 320, 240));
ratio.set_ratio(2.0).expect("2:1 is a valid ratio");
ratio.set_content_size(Size::new(100, 0));
assert_eq!(ratio.requested_size(), Size::new(100, 50));
ratio.set_content_size(Size::new(0, 40));
assert_eq!(ratio.requested_size(), Size::new(80, 40));
}
#[test]
fn aspect_ratio_content_rect_fits_inside_the_control() {
let ratio = AspectRatio::new(Rect::new(0, 0, 320, 240));
let rect = ratio.content_rect();
assert!(rect.width <= 320 && rect.height <= 240, "{rect:?} must fit inside");
assert_eq!(rect, Rect::new(0, 30, 320, 180));
}
#[test]
fn the_ratio_caption_reads_like_the_author_wrote_it() {
assert_eq!(ratio_label(16.0 / 9.0), "16:9");
assert_eq!(ratio_label(4.0 / 3.0), "4:3");
assert_eq!(ratio_label(1.0), "1:1");
assert_eq!(ratio_label(2.0), "2:1");
assert_eq!(ratio_label(1.25), "5:4");
assert_eq!(ratio_label(0.5), "1:2");
assert_eq!(ratio_label(1.618_034), "13:8");
assert_eq!(ratio_label(f32::INFINITY), "inf");
assert_eq!(ratio_label(0.0), "0.00");
assert_eq!(ratio_label(-2.0), "-2.00");
}
#[test]
fn an_aspect_ratio_paints_its_guides_and_caption() {
let mut ratio = AspectRatio::new(Rect::new(0, 0, 240, 120));
ratio.set_content_size(Size::new(80, 40));
ratio.set_content_label("Sample".to_string());
let svg = crate::widget::svg::render_to_svg(&mut ratio);
let lines = svg.lines().filter(|line| line.contains("<line")).count();
assert_eq!(lines, 10, "{svg}");
let boxes = crate::widget::svg::text_ink_boxes(&svg);
assert_eq!(boxes.len(), 2, "{svg}");
let lowest = boxes.iter().map(|b| b.1).max().expect("caption ink");
let highest = boxes.iter().map(|b| b.1).min().expect("label ink");
assert!(lowest > highest, "caption must sit below the label: {boxes:?}");
}
#[test]
fn a_wrapper_property_route_agrees_with_its_own_accessors() {
let mut align = Align::new(Rect::new(0, 0, 100, 100));
align
.set("alignment", CapabilityValue::String("bottom_right".to_string()))
.expect("writable");
assert_eq!(align.alignment(), AlignPosition::BottomRight);
assert_eq!(
align.get("alignment").expect("readable"),
CapabilityValue::String("bottom_right".to_string())
);
assert!(align.set("alignment", CapabilityValue::String("nowhere".to_string())).is_err());
}
#[test]
fn a_wrapper_paints_inside_its_own_rect() {
let rect = Rect::new(0, 0, 120, 80);
fn assert_paints<W: Draw + Widget>(widget: &mut W, name: &str) {
let svg = crate::widget::svg::render_to_svg(widget);
assert!(svg.starts_with("<svg"), "{name} produced no document: {svg}");
assert!(svg.contains("fill="), "{name} must paint something: {svg}");
}
assert_paints(&mut Spacer::new(rect), "spacer");
let mut center = Center::new(rect);
center.set_content_size(Size::new(60, 30));
center.set_content_label("C".to_string());
assert_paints(&mut center, "center");
let mut align = Align::new(rect);
align.set_content_size(Size::new(60, 30));
align.set_content_label("A".to_string());
assert_paints(&mut align, "align");
let mut sized = SizedBox::new(rect);
sized.set_box_size(Size::new(60, 40));
sized.set_content_label("S".to_string());
assert_paints(&mut sized, "sized_box");
let mut padding = Padding::new(rect);
padding.set_insets(6, 6, 6, 6);
padding.set_content_label("P".to_string());
assert_paints(&mut padding, "padding");
let mut expanded = Expanded::new(rect);
expanded.set_content_label("E".to_string());
assert_paints(&mut expanded, "expanded");
let mut flexible = Flexible::new(rect);
flexible.set_content_label("F".to_string());
assert_paints(&mut flexible, "flexible");
let mut intrinsic = IntrinsicWidth::new(rect);
intrinsic.set_content_size(Size::new(70, 20));
intrinsic.set_content_label("I".to_string());
assert_paints(&mut intrinsic, "intrinsic_width");
assert_paints(&mut AspectRatio::new(rect), "aspect_ratio");
}
#[test]
fn a_wrapper_event_delegation_does_not_panic() {
let mut center = Center::new(Rect::new(0, 0, 100, 100));
center.handle_event(&Event::MouseMove { pos: Point::new(5, 5) });
center.handle_event(&Event::MousePress { pos: Point::new(5, 5), button: 1, modifiers: 0 });
center.handle_event(&Event::MouseRelease { pos: Point::new(5, 5), button: 1 });
}
#[test]
fn the_spacer_design_hint_can_be_turned_off() {
let mut spacer = Spacer::new(Rect::new(0, 0, 20, 20));
assert!(spacer.paint_hint());
spacer.set("paint_hint", CapabilityValue::Bool(false)).expect("writable");
let svg = crate::widget::svg::render_to_svg(&mut spacer);
assert_eq!(
svg.matches("<rect").count(),
1,
"a shipping spacer paints nothing but the document's own background: {svg}"
);
}
#[cfg(not(alloc_frugal))]
#[test]
fn a_content_size_property_change_requests_layout() {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
macro_rules! assert_requests_layout {
($widget:expr, $label:literal) => {{
let mut widget = $widget;
let counter = Arc::new(AtomicUsize::new(0));
let _guard = widget.base.layout_requested.connect({
let counter = Arc::clone(&counter);
move || {
counter.fetch_add(1, Ordering::Relaxed);
}
});
widget
.set("content_width", CapabilityValue::UInt(300))
.expect("content_width is a published property");
assert!(
counter.load(Ordering::Relaxed) > 0,
concat!($label, " must request layout when its content size changes")
);
}};
}
assert_requests_layout!(Center::new(Rect::new(0, 0, 200, 200)), "Center");
assert_requests_layout!(Align::new(Rect::new(0, 0, 200, 200)), "Align");
assert_requests_layout!(Padding::new(Rect::new(0, 0, 200, 200)), "Padding");
assert_requests_layout!(Expanded::new(Rect::new(0, 0, 200, 200)), "Expanded");
assert_requests_layout!(Flexible::new(Rect::new(0, 0, 200, 200)), "Flexible");
assert_requests_layout!(AspectRatio::new(Rect::new(0, 0, 200, 200)), "AspectRatio");
}
}