use teksilo_canvas::{Canvas, Paint, Rect, Size, SizeProposal};
use teksilo_tokens::{Color, CornerRadius};
use teksilo_core::accessibility::AccessNodeBuilder;
use teksilo_core::color_prop::ColorProp;
use teksilo_core::paint_prop::PaintProp;
use teksilo_core::signal::Prop;
use teksilo_core::styles::{BorderPosition, BorderSides, apply_border_position};
use teksilo_core::widget::{LayoutContext, PaintContext, Widget};
pub struct RectWidget {
background: PaintProp,
border_color: ColorProp,
border_width: Prop<f32>,
corner_radius: Prop<CornerRadius>,
border_sides: Prop<Option<BorderSides>>,
border_position: BorderPosition,
}
impl RectWidget {
pub fn new() -> Self {
Self {
background: PaintProp::Solid(ColorProp::Static(Color::TRANSPARENT)),
border_color: ColorProp::Static(Color::TRANSPARENT),
border_width: Prop::Static(0.0),
corner_radius: Prop::Static(CornerRadius::ZERO),
border_sides: Prop::Static(None),
border_position: BorderPosition::Center,
}
}
pub fn background(mut self, paint: impl Into<PaintProp>) -> Self {
self.background = paint.into();
self
}
pub fn border_sides(mut self, sides: impl Into<Prop<Option<BorderSides>>>) -> Self {
self.border_sides = sides.into();
self
}
pub fn border_position(mut self, position: BorderPosition) -> Self {
self.border_position = position;
self
}
pub fn border_color(mut self, color: impl Into<ColorProp>) -> Self {
self.border_color = color.into();
self
}
pub fn border_width(mut self, width: impl Into<Prop<f32>>) -> Self {
self.border_width = width.into();
self
}
pub fn corner_radius(mut self, radius: impl Into<Prop<CornerRadius>>) -> Self {
self.corner_radius = radius.into();
self
}
}
impl Default for RectWidget {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for RectWidget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RectWidget").finish()
}
}
impl Widget for RectWidget {
fn build(
&mut self,
ctx: &mut teksilo_core::build_context::BuildContext,
) -> Vec<teksilo_core::widget_id::WidgetId> {
let self_id = ctx.self_id();
let registry = ctx.binding_registry();
self.background.register_if_bound(
self_id,
registry,
teksilo_core::binding::BindingLevel::RepaintOnly,
);
self.border_color.register_if_bound(
self_id,
registry,
teksilo_core::binding::BindingLevel::RepaintOnly,
);
self.border_width.register_if_bound(
self_id,
registry,
teksilo_core::binding::BindingLevel::RepaintOnly,
);
self.corner_radius.register_if_bound(
self_id,
registry,
teksilo_core::binding::BindingLevel::RepaintOnly,
);
self.border_sides.register_if_bound(
self_id,
registry,
teksilo_core::binding::BindingLevel::RepaintOnly,
);
Vec::new()
}
fn layout_response(
&self,
proposal: SizeProposal,
_ctx: &LayoutContext,
) -> teksilo_core::widget::LayoutResponse {
proposal.resolve(0.0, 0.0).into()
}
fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
let radius = self.corner_radius.get();
let paint = self.background.resolve(
ctx.theme,
ctx.effective_enabled,
Size::new(bounds.width, bounds.height),
);
let skip_fill = matches!(&paint, Paint::Solid(c) if c.a() <= 0.0);
if !skip_fill {
canvas.fill_rounded_rect(bounds, radius, paint);
}
let bc = self.border_color.resolve(ctx.theme, ctx.effective_enabled);
if bc.a() <= 0.0 {
return;
}
match self.border_sides.get() {
Some(sides) => paint_border_sides(canvas, bounds, sides, bc),
None => {
let bw = self.border_width.get();
if bw > 0.0 {
let stroke_rect = apply_border_position(bounds, bw, self.border_position);
canvas.stroke_rounded_rect(stroke_rect, radius, bc, bw);
}
}
}
}
fn accessibility(&self, _builder: &mut AccessNodeBuilder) {}
}
fn paint_border_sides(canvas: &mut Canvas, bounds: Rect, sides: BorderSides, color: Color) {
if sides.top > 0.0 {
canvas.fill_rect(
Rect::new(bounds.x, bounds.y, bounds.width, sides.top),
color,
);
}
if sides.bottom > 0.0 {
canvas.fill_rect(
Rect::new(
bounds.x,
bounds.y + bounds.height - sides.bottom,
bounds.width,
sides.bottom,
),
color,
);
}
if sides.leading > 0.0 {
canvas.fill_rect(
Rect::new(bounds.x, bounds.y, sides.leading, bounds.height),
color,
);
}
if sides.trailing > 0.0 {
canvas.fill_rect(
Rect::new(
bounds.x + bounds.width - sides.trailing,
bounds.y,
sides.trailing,
bounds.height,
),
color,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use teksilo_core::signal::Signal;
use teksilo_core::widget_tree::WidgetTree;
#[test]
fn static_background_paints_correctly() {
let mut tree = WidgetTree::new();
tree.add(
RectWidget::new()
.background(Color::RED)
.corner_radius(CornerRadius::uniform(4.0)),
);
tree.layout(SizeProposal::exact(100.0, 40.0));
let frame = tree.render();
assert_eq!(frame.shapes.len(), 1);
assert_eq!(frame.shapes[0].color, Color::RED.to_array());
}
#[test]
fn accent_role_desaturates_when_window_inactive() {
use teksilo_tokens::SurfaceRole;
let theme = teksilo_core::presets::intui::light();
let accent = theme.colors.accent.to_array();
let inactive_accent = theme.colors.for_inactive_window().accent.to_array();
assert_ne!(accent, inactive_accent);
let mut tree = WidgetTree::new().with_theme(theme);
tree.add(
RectWidget::new()
.background(SurfaceRole::Accent)
.corner_radius(CornerRadius::uniform(4.0)),
);
tree.layout(SizeProposal::exact(100.0, 40.0));
let frame = tree.render();
assert_eq!(frame.shapes.len(), 1);
assert_eq!(
frame.shapes[0].color, accent,
"active window paints the vivid accent"
);
tree.set_window_active(false);
let frame = tree.render();
assert_eq!(frame.shapes.len(), 1);
assert_eq!(
frame.shapes[0].color, inactive_accent,
"inactive window desaturates the accent"
);
tree.set_window_active(true);
let frame = tree.render();
assert_eq!(frame.shapes[0].color, accent);
}
#[test]
fn background_reads_from_state() {
let color = Signal::new(Color::BLUE);
let mut tree = WidgetTree::new();
let w = tree.add(
RectWidget::new()
.background(color.clone())
.corner_radius(CornerRadius::uniform(4.0)),
);
color.bind_to(
w,
tree.binding_registry(),
teksilo_core::binding::BindingLevel::RepaintOnly,
);
tree.layout(SizeProposal::exact(100.0, 40.0));
let frame = tree.render();
assert_eq!(frame.shapes[0].color, Color::BLUE.to_array());
}
#[test]
fn underline_draws_a_bottom_decoration() {
let mut tree = WidgetTree::new();
tree.add(
RectWidget::new()
.border_color(Color::RED)
.border_sides(Some(BorderSides::bottom(2.0))),
);
tree.layout(SizeProposal::exact(100.0, 40.0));
let frame = tree.render();
let underline = frame
.decorations
.iter()
.find(|d| d.color == Color::RED.to_array())
.expect("underline decoration present");
assert_eq!(underline.rect[1], 38.0);
assert_eq!(underline.rect[3], 2.0);
assert!(frame.shapes.iter().all(|s| s.stroke_width == 0.0));
}
#[test]
fn gradient_background_emits_linear_gradient_paint() {
use teksilo_canvas::render_frame::PaintData;
use teksilo_core::paint_prop::{GradientStopProp, PaintProp};
let mut tree = WidgetTree::new();
tree.add(RectWidget::new().background(PaintProp::Linear {
stops: vec![
GradientStopProp {
offset: 0.0,
color: Color::RED.into(),
},
GradientStopProp {
offset: 1.0,
color: Color::BLUE.into(),
},
],
angle_deg: 90.0,
}));
tree.layout(SizeProposal::exact(100.0, 40.0));
let frame = tree.render();
assert_eq!(frame.shapes.len(), 1);
assert!(matches!(
frame.shapes[0].paint_data,
PaintData::LinearGradient { .. }
));
}
#[test]
fn background_updates_on_state_change() {
let color = Signal::new(Color::RED);
let mut tree = WidgetTree::new();
let w = tree.add(
RectWidget::new()
.background(color.clone())
.corner_radius(CornerRadius::uniform(4.0)),
);
color.bind_to(
w,
tree.binding_registry(),
teksilo_core::binding::BindingLevel::RepaintOnly,
);
tree.layout(SizeProposal::exact(100.0, 40.0));
let frame = tree.render();
assert_eq!(frame.shapes[0].color, Color::RED.to_array());
color.set(Color::GREEN);
tree.layout(SizeProposal::exact(100.0, 40.0));
let frame = tree.render();
assert_eq!(frame.shapes[0].color, Color::GREEN.to_array());
}
}