use crate::event::{Point, Rect};
use crate::layout::{Anchor, ResolvedSide, Side};
use crate::types::{Color, WidgetId};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Float {
pub id: WidgetId,
pub anchor: Anchor,
#[serde(default = "default_focusable")]
pub focusable: bool,
#[serde(default = "default_border")]
pub border: bool,
#[serde(default)]
pub bg: Option<Color>,
}
fn default_border() -> bool {
true
}
fn default_focusable() -> bool {
true
}
impl Float {
pub fn new(id: WidgetId, anchor: Anchor) -> Self {
Self {
id,
anchor,
focusable: true,
border: true,
bg: None,
}
}
}
impl Default for Float {
fn default() -> Self {
Self::new(
WidgetId::new(""),
Anchor::new(Rect::default(), Side::default()),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct FloatMeasure {
pub width: f32,
pub height: f32,
}
impl FloatMeasure {
pub fn new(width: f32, height: f32) -> Self {
Self { width, height }
}
}
const BORDER_INSET: f32 = 1.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FloatLayout {
pub bounds: Rect,
pub content_bounds: Rect,
pub resolved_side: ResolvedSide,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FloatHit {
Body,
Outside,
}
impl Float {
pub fn layout(&self, viewport: Rect, measure: FloatMeasure) -> FloatLayout {
let (origin, resolved_side) = self
.anchor
.resolve((measure.width, measure.height), viewport);
let bounds = Rect::new(origin.x, origin.y, measure.width, measure.height);
let inset = if self.border { BORDER_INSET } else { 0.0 };
let content_bounds = Rect::new(
bounds.x + inset,
bounds.y + inset,
(bounds.width - inset * 2.0).max(0.0),
(bounds.height - inset * 2.0).max(0.0),
);
FloatLayout {
bounds,
content_bounds,
resolved_side,
}
}
}
impl FloatLayout {
pub fn hit_test(&self, p: Point) -> FloatHit {
if self.bounds.contains(p) {
FloatHit::Body
} else {
FloatHit::Outside
}
}
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[allow(dead_code)]
pub(crate) mod native_surface_paint {
use super::{Float, FloatLayout};
use crate::paint_surface::PaintSurface;
use crate::theme::Theme;
pub(crate) fn paint(
float: &Float,
layout: &FloatLayout,
surface: &mut dyn PaintSurface,
theme: &Theme,
) {
let bounds = layout.bounds;
if bounds.width <= 0.0 || bounds.height <= 0.0 {
return;
}
let bg = float.bg.unwrap_or(theme.surface_bg);
surface.surface_fill_rect(bounds, bg);
if float.border {
surface.surface_stroke_rect(bounds, theme.border_fg, 1.0);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{Rect, Viewport};
use crate::layout::{Anchor, Side};
use crate::primitives::float::FloatMeasure;
use crate::types::{Color, WidgetId};
use crate::Image;
#[derive(Default)]
struct RecordingSurface {
fills: Vec<(Rect, Color)>,
strokes: Vec<(Rect, Color, f32)>,
}
impl PaintSurface for RecordingSurface {
fn surface_begin_frame(&mut self, _viewport: Viewport) {}
fn surface_end_frame(&mut self) {}
fn surface_viewport(&self) -> Viewport {
Viewport::new(200.0, 100.0, 1.0)
}
fn surface_line_height(&self) -> f32 {
16.0
}
fn surface_char_width(&self) -> f32 {
8.0
}
fn surface_measure_text(&self, text: &str) -> (f32, f32) {
(text.chars().count() as f32 * 8.0, 16.0)
}
fn surface_fill_rect(&mut self, rect: Rect, color: Color) {
self.fills.push((rect, color));
}
fn surface_fill_rounded_rect(&mut self, rect: Rect, _radius: f32, color: Color) {
self.fills.push((rect, color));
}
fn surface_stroke_rect(&mut self, rect: Rect, color: Color, width: f32) {
self.strokes.push((rect, color, width));
}
fn surface_stroke_rounded_rect(
&mut self,
rect: Rect,
_radius: f32,
color: Color,
width: f32,
) {
self.strokes.push((rect, color, width));
}
fn surface_draw_line(
&mut self,
_from: crate::event::Point,
_to: crate::event::Point,
_color: Color,
_width: f32,
) {
}
fn surface_draw_text_run(&mut self, _rect: Rect, _text: &str, _color: Color) {}
fn surface_draw_image(
&mut self,
_rect: Rect,
_image: &Image,
) -> crate::backend::ImagePaintResult {
crate::backend::ImagePaintResult::Unsupported
}
fn surface_push_clip(&mut self, _rect: Rect) {}
fn surface_pop_clip(&mut self) {}
}
fn float(border: bool) -> Float {
let mut f = Float::new(
WidgetId::new("f"),
Anchor::new(Rect::new(10.0, 10.0, 5.0, 1.0), Side::Bottom),
);
f.border = border;
f
}
#[test]
fn paint_fills_background_and_strokes_border() {
let f = float(true);
let layout = f.layout(
Rect::new(0.0, 0.0, 200.0, 100.0),
FloatMeasure::new(20.0, 10.0),
);
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&f, &layout, &mut surface, &theme);
assert_eq!(surface.fills.len(), 1);
assert_eq!(surface.fills[0].0, layout.bounds);
assert_eq!(surface.strokes.len(), 1);
assert_eq!(surface.strokes[0].0, layout.bounds);
}
#[test]
fn paint_skips_border_stroke_when_disabled() {
let f = float(false);
let layout = f.layout(
Rect::new(0.0, 0.0, 200.0, 100.0),
FloatMeasure::new(20.0, 10.0),
);
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&f, &layout, &mut surface, &theme);
assert_eq!(surface.fills.len(), 1);
assert!(surface.strokes.is_empty());
}
#[test]
fn paint_uses_bg_override_when_set() {
let mut f = float(true);
f.bg = Some(Color::rgb(1, 2, 3));
let layout = f.layout(
Rect::new(0.0, 0.0, 200.0, 100.0),
FloatMeasure::new(20.0, 10.0),
);
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&f, &layout, &mut surface, &theme);
assert_eq!(surface.fills[0].1, Color::rgb(1, 2, 3));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Side;
fn viewport() -> Rect {
Rect::new(0.0, 0.0, 100.0, 50.0)
}
#[test]
fn layout_places_float_relative_to_anchor() {
let anchor = Anchor::new(Rect::new(40.0, 10.0, 10.0, 2.0), Side::Bottom).with_margin(1.0);
let f = Float::new(WidgetId::new("f"), anchor);
let layout = f.layout(viewport(), FloatMeasure::new(20.0, 5.0));
assert_eq!(layout.bounds.x, 35.0);
assert_eq!(layout.bounds.y, 13.0);
assert_eq!(layout.resolved_side, ResolvedSide::Bottom);
}
#[test]
fn layout_clamps_to_viewport_edge_when_anchor_is_near_the_edge() {
let anchor = Anchor::new(Rect::new(40.0, 48.0, 10.0, 2.0), Side::Bottom).with_margin(1.0);
let f = Float::new(WidgetId::new("f"), anchor);
let layout = f.layout(viewport(), FloatMeasure::new(20.0, 5.0));
assert_eq!(layout.resolved_side, ResolvedSide::Top);
assert!(layout.bounds.y >= 0.0);
assert!(layout.bounds.y + layout.bounds.height <= 50.0);
}
#[test]
fn content_bounds_insets_by_border_width_when_bordered() {
let anchor = Anchor::new(Rect::new(0.0, 0.0, 1.0, 1.0), Side::AtPoint);
let f = Float::new(WidgetId::new("f"), anchor);
let layout = f.layout(viewport(), FloatMeasure::new(10.0, 6.0));
assert_eq!(layout.content_bounds.x, layout.bounds.x + BORDER_INSET);
assert_eq!(layout.content_bounds.y, layout.bounds.y + BORDER_INSET);
assert_eq!(layout.content_bounds.width, 10.0 - BORDER_INSET * 2.0);
assert_eq!(layout.content_bounds.height, 6.0 - BORDER_INSET * 2.0);
}
#[test]
fn content_bounds_fills_the_whole_box_when_borderless() {
let anchor = Anchor::new(Rect::new(0.0, 0.0, 1.0, 1.0), Side::AtPoint);
let mut f = Float::new(WidgetId::new("f"), anchor);
f.border = false;
let layout = f.layout(viewport(), FloatMeasure::new(10.0, 6.0));
assert_eq!(layout.content_bounds, layout.bounds);
}
#[test]
fn hit_test_inside_and_outside() {
let anchor = Anchor::new(Rect::new(0.0, 0.0, 1.0, 1.0), Side::AtPoint);
let f = Float::new(WidgetId::new("f"), anchor);
let layout = f.layout(viewport(), FloatMeasure::new(10.0, 6.0));
let inside = Point::new(layout.bounds.x + 1.0, layout.bounds.y + 1.0);
let outside = Point::new(layout.bounds.x - 1.0, layout.bounds.y - 1.0);
assert_eq!(layout.hit_test(inside), FloatHit::Body);
assert_eq!(layout.hit_test(outside), FloatHit::Outside);
}
#[test]
fn new_defaults_to_focusable_with_border_and_no_bg_override() {
let anchor = Anchor::new(Rect::new(0.0, 0.0, 1.0, 1.0), Side::AtPoint);
let f = Float::new(WidgetId::new("f"), anchor);
assert!(f.focusable);
assert!(f.border);
assert!(f.bg.is_none());
}
#[test]
fn serde_round_trip() {
let anchor = Anchor::new(Rect::new(1.0, 2.0, 3.0, 4.0), Side::Left).with_margin(2.0);
let mut f = Float::new(WidgetId::new("hint"), anchor);
f.focusable = false;
f.bg = Some(Color::rgb(10, 20, 30));
let json = serde_json::to_string(&f).unwrap();
let back: Float = serde_json::from_str(&json).unwrap();
assert_eq!(f, back);
}
#[test]
fn serde_defaults_focusable_true_and_border_true_when_omitted() {
let json = r#"{"id":"f","anchor":{"rect":{"x":0.0,"y":0.0,"width":1.0,"height":1.0},"preferred":"Bottom","margin":0.0}}"#;
let f: Float = serde_json::from_str(json).unwrap();
assert!(f.focusable);
assert!(f.border);
assert!(f.bg.is_none());
}
#[test]
fn float_measure_serde_round_trip() {
let m = FloatMeasure::new(20.0, 5.0);
let json = serde_json::to_string(&m).unwrap();
let back: FloatMeasure = serde_json::from_str(&json).unwrap();
assert_eq!(m, back);
}
}