use crate::backend::ImagePaintResult;
use crate::{Color, Image, Point, Rect, Viewport};
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum PathVerb {
MoveTo(Point),
LineTo(Point),
Close,
}
pub trait PaintSurface {
fn surface_begin_frame(&mut self, viewport: Viewport);
fn surface_end_frame(&mut self);
fn surface_viewport(&self) -> Viewport;
fn surface_line_height(&self) -> f32;
fn surface_char_width(&self) -> f32;
fn surface_measure_text(&self, text: &str) -> (f32, f32);
fn surface_measure_text_styled(&self, text: &str, bold: bool) -> (f32, f32) {
let _ = bold;
self.surface_measure_text(text)
}
fn surface_fill_rect(&mut self, rect: Rect, color: Color);
fn surface_fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color);
fn surface_fill_rect_alpha(&mut self, rect: Rect, color: Color, alpha: f32) {
self.surface_fill_rect(rect, color.with_alpha(alpha as f64));
}
fn surface_stroke_rect(&mut self, rect: Rect, color: Color, stroke_width: f32);
fn surface_stroke_rounded_rect(
&mut self,
rect: Rect,
radius: f32,
color: Color,
stroke_width: f32,
) {
let _ = radius;
self.surface_stroke_rect(rect, color, stroke_width);
}
fn surface_draw_text_run(&mut self, rect: Rect, text: &str, color: Color);
#[allow(clippy::too_many_arguments)]
fn surface_draw_text_run_styled(
&mut self,
rect: Rect,
text: &str,
color: Color,
bold: bool,
italic: bool,
underline: bool,
scale_x: f32,
) {
let _ = (bold, italic, underline, scale_x);
self.surface_draw_text_run(rect, text, color);
}
fn surface_draw_text_run_with_role(
&mut self,
rect: Rect,
text: &str,
color: Color,
role: crate::FontRole,
italic: bool,
) {
let _ = role;
self.surface_draw_text_run_styled(rect, text, color, false, italic, false, 1.0)
}
fn surface_draw_icon_glyph(&mut self, rect: Rect, text: &str, color: Color) {
self.surface_draw_text_run(rect, text, color)
}
fn surface_draw_line(&mut self, from: Point, to: Point, color: Color, stroke_width: f32);
fn surface_draw_shadow(&mut self, rect: Rect, radius: f32, elevation: u8, color: Color) {
let elevation = elevation.min(3);
if elevation == 0 {
return;
}
let elevation = f32::from(elevation);
let base_alpha = color.a as f64 / 255.0;
const LAYERS: u8 = 3;
for layer in (1..=LAYERS).rev() {
let t = f32::from(layer) / f32::from(LAYERS);
let spread = elevation * t * 1.5;
let offset_y = elevation * t * 2.0;
let alpha = base_alpha * f64::from(1.0 - t) * 0.35 + base_alpha * 0.05;
let shadow_rect = Rect::new(
rect.x - spread,
rect.y - spread + offset_y,
rect.width + spread * 2.0,
rect.height + spread * 2.0,
);
self.surface_fill_rounded_rect(shadow_rect, radius + spread, color.with_alpha(alpha));
}
}
fn surface_draw_path(&mut self, verbs: &[PathVerb], color: Color, stroke_width: f32) {
let mut current: Option<Point> = None;
let mut start: Option<Point> = None;
for verb in verbs {
match *verb {
PathVerb::MoveTo(p) => {
current = Some(p);
start = Some(p);
}
PathVerb::LineTo(p) => {
if let Some(from) = current {
self.surface_draw_line(from, p, color, stroke_width);
}
current = Some(p);
}
PathVerb::Close => {
if let (Some(from), Some(to)) = (current, start) {
self.surface_draw_line(from, to, color, stroke_width);
current = Some(to);
}
}
}
}
}
fn surface_push_clip(&mut self, rect: Rect);
fn surface_pop_clip(&mut self);
fn surface_draw_image(&mut self, rect: Rect, image: &Image) -> ImagePaintResult;
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
pub(crate) fn fit_text_prefix(
surface: &dyn PaintSurface,
text: &str,
bold: bool,
max_width: f32,
) -> (String, f32) {
const ELLIPSIS: char = '…';
let (ellipsis_w, _) = surface.surface_measure_text_styled(&ELLIPSIS.to_string(), bold);
if ellipsis_w > max_width {
return (String::new(), 0.0);
}
let chars: Vec<char> = text.chars().collect();
let mut lo = 0usize;
let mut hi = chars.len();
while lo < hi {
let mid = lo + (hi - lo).div_ceil(2);
let prefix: String = chars[..mid].iter().collect();
let candidate = format!("{prefix}{ELLIPSIS}");
let (w, _) = surface.surface_measure_text_styled(&candidate, bold);
if w <= max_width {
lo = mid;
} else {
hi = mid - 1;
}
}
if lo == 0 {
return (ELLIPSIS.to_string(), ellipsis_w);
}
let prefix: String = chars[..lo].iter().collect();
let candidate = format!("{prefix}{ELLIPSIS}");
let (w, _) = surface.surface_measure_text_styled(&candidate, bold);
(candidate, w)
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct RecordingSurface {
fill_rounded_rects: Vec<(Rect, f32, Color)>,
lines: Vec<(Point, Point, 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(0.0, 0.0, 1.0)
}
fn surface_line_height(&self) -> f32 {
0.0
}
fn surface_char_width(&self) -> f32 {
0.0
}
fn surface_measure_text(&self, _text: &str) -> (f32, f32) {
(0.0, 0.0)
}
fn surface_fill_rect(&mut self, _rect: Rect, _color: Color) {}
fn surface_fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
self.fill_rounded_rects.push((rect, radius, color));
}
fn surface_stroke_rect(&mut self, _rect: Rect, _color: Color, _stroke_width: f32) {}
fn surface_stroke_rounded_rect(
&mut self,
_rect: Rect,
_radius: f32,
_color: Color,
_stroke_width: f32,
) {
}
fn surface_draw_text_run(&mut self, _rect: Rect, _text: &str, _color: Color) {}
fn surface_draw_line(&mut self, from: Point, to: Point, color: Color, stroke_width: f32) {
self.lines.push((from, to, color, stroke_width));
}
fn surface_push_clip(&mut self, _rect: Rect) {}
fn surface_pop_clip(&mut self) {}
fn surface_draw_image(&mut self, _rect: Rect, _image: &Image) -> ImagePaintResult {
ImagePaintResult::Unsupported
}
}
#[test]
fn surface_draw_shadow_elevation_zero_calls_nothing() {
let mut surface = RecordingSurface::default();
surface.surface_draw_shadow(Rect::new(0.0, 0.0, 10.0, 10.0), 4.0, 0, Color::rgb(0, 0, 0));
assert!(
surface.fill_rounded_rects.is_empty(),
"elevation 0 must not paint any shadow layer"
);
}
#[test]
fn surface_draw_shadow_nonzero_elevation_paints_three_layers() {
let mut surface = RecordingSurface::default();
surface.surface_draw_shadow(
Rect::new(10.0, 10.0, 20.0, 20.0),
6.0,
2,
Color::rgba(0, 0, 0, 128),
);
assert_eq!(
surface.fill_rounded_rects.len(),
3,
"every nonzero elevation paints exactly three layers"
);
let (first_rect, _first_radius, first_color) = surface.fill_rounded_rects[0];
let (last_rect, _last_radius, last_color) = surface.fill_rounded_rects[2];
assert!(
first_rect.width > last_rect.width,
"the first layer must be wider than the last — widest first"
);
assert!(
first_color.a < last_color.a,
"the first (widest) layer must be the faintest"
);
}
#[test]
fn surface_draw_shadow_clamps_elevation_above_three() {
let mut clamped = RecordingSurface::default();
clamped.surface_draw_shadow(
Rect::new(0.0, 0.0, 10.0, 10.0),
4.0,
200,
Color::rgb(0, 0, 0),
);
let mut exact = RecordingSurface::default();
exact.surface_draw_shadow(Rect::new(0.0, 0.0, 10.0, 10.0), 4.0, 3, Color::rgb(0, 0, 0));
assert_eq!(clamped.fill_rounded_rects, exact.fill_rounded_rects);
}
#[test]
fn surface_draw_path_decomposes_into_lines() {
let mut surface = RecordingSurface::default();
let a = Point::new(0.0, 0.0);
let b = Point::new(10.0, 0.0);
let c = Point::new(10.0, 10.0);
surface.surface_draw_path(
&[
PathVerb::MoveTo(a),
PathVerb::LineTo(b),
PathVerb::LineTo(c),
PathVerb::Close,
],
Color::rgb(0, 255, 0),
2.0,
);
assert_eq!(
surface.lines,
vec![
(a, b, Color::rgb(0, 255, 0), 2.0),
(b, c, Color::rgb(0, 255, 0), 2.0),
(c, a, Color::rgb(0, 255, 0), 2.0),
],
"MoveTo draws no segment; each LineTo draws from the current point; \
Close draws back to the last MoveTo"
);
}
#[test]
fn surface_draw_path_without_a_moveto_draws_nothing() {
let mut surface = RecordingSurface::default();
surface.surface_draw_path(
&[PathVerb::LineTo(Point::new(5.0, 5.0)), PathVerb::Close],
Color::rgb(0, 255, 0),
1.0,
);
assert!(surface.lines.is_empty());
}
#[derive(Default)]
struct SquareOnlySurface {
strokes: Vec<(Rect, Color, f32)>,
}
impl PaintSurface for SquareOnlySurface {
fn surface_begin_frame(&mut self, _viewport: Viewport) {}
fn surface_end_frame(&mut self) {}
fn surface_viewport(&self) -> Viewport {
Viewport::new(0.0, 0.0, 1.0)
}
fn surface_line_height(&self) -> f32 {
0.0
}
fn surface_char_width(&self) -> f32 {
0.0
}
fn surface_measure_text(&self, _text: &str) -> (f32, f32) {
(0.0, 0.0)
}
fn surface_fill_rect(&mut self, _rect: Rect, _color: Color) {}
fn surface_fill_rounded_rect(&mut self, _rect: Rect, _radius: f32, _color: Color) {}
fn surface_stroke_rect(&mut self, rect: Rect, color: Color, stroke_width: f32) {
self.strokes.push((rect, color, stroke_width));
}
fn surface_draw_text_run(&mut self, _rect: Rect, _text: &str, _color: Color) {}
fn surface_draw_line(&mut self, _from: Point, _to: Point, _color: Color, _w: f32) {}
fn surface_push_clip(&mut self, _rect: Rect) {}
fn surface_pop_clip(&mut self) {}
fn surface_draw_image(&mut self, _rect: Rect, _image: &Image) -> ImagePaintResult {
ImagePaintResult::Unsupported
}
}
#[test]
fn surface_stroke_rounded_rect_default_falls_back_to_square_stroke() {
let mut surface = SquareOnlySurface::default();
let rect = Rect::new(1.0, 2.0, 30.0, 40.0);
surface.surface_stroke_rounded_rect(rect, 6.0, Color::rgb(9, 9, 9), 1.5);
assert_eq!(surface.strokes, vec![(rect, Color::rgb(9, 9, 9), 1.5)]);
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[derive(Default)]
struct WidthSurface;
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
impl PaintSurface for WidthSurface {
fn surface_begin_frame(&mut self, _viewport: Viewport) {}
fn surface_end_frame(&mut self) {}
fn surface_viewport(&self) -> Viewport {
Viewport::new(0.0, 0.0, 1.0)
}
fn surface_line_height(&self) -> f32 {
14.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, 14.0)
}
fn surface_fill_rect(&mut self, _rect: Rect, _color: Color) {}
fn surface_fill_rounded_rect(&mut self, _rect: Rect, _radius: f32, _color: Color) {}
fn surface_stroke_rect(&mut self, _rect: Rect, _color: Color, _stroke_width: f32) {}
fn surface_draw_text_run(&mut self, _rect: Rect, _text: &str, _color: Color) {}
fn surface_draw_line(&mut self, _from: Point, _to: Point, _color: Color, _w: f32) {}
fn surface_push_clip(&mut self, _rect: Rect) {}
fn surface_pop_clip(&mut self) {}
fn surface_draw_image(&mut self, _rect: Rect, _image: &Image) -> ImagePaintResult {
ImagePaintResult::Unsupported
}
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[test]
fn fit_text_prefix_returns_whole_text_unchanged_budget_irrelevant_when_caller_checks_first() {
let surface = WidthSurface;
let (fitted, w) = fit_text_prefix(&surface, "ab", false, 100.0);
assert_eq!(fitted, "ab…");
assert_eq!(w, 3.0 * 8.0);
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[test]
fn fit_text_prefix_finds_longest_fitting_prefix() {
let surface = WidthSurface;
let (fitted, w) = fit_text_prefix(&surface, "hello world", false, 50.0);
assert_eq!(fitted, "hello…");
assert_eq!(w, 48.0);
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[test]
fn fit_text_prefix_with_budget_too_small_for_even_the_ellipsis_returns_empty() {
let surface = WidthSurface;
let (fitted, w) = fit_text_prefix(&surface, "hello world", false, 4.0);
assert_eq!(fitted, "");
assert_eq!(w, 0.0);
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[test]
fn fit_text_prefix_with_budget_for_only_the_ellipsis_returns_bare_ellipsis() {
let surface = WidthSurface;
let (fitted, w) = fit_text_prefix(&surface, "hello world", false, 8.0);
assert_eq!(fitted, "…");
assert_eq!(w, 8.0);
}
}