use core_text::font::CTFont;
use crate::primitives::layout_metrics::pixel_toast_stack_layout;
use crate::primitives::toast::{ToastOverlay, ToastStackLayout};
#[cfg(test)]
use crate::theme::Theme;
#[allow(clippy::too_many_arguments)]
pub fn mac_toast_stack_layout(
stack: &ToastOverlay,
font: &CTFont,
origin_x: f32,
origin_y: f32,
viewport_width: f32,
viewport_height: f32,
line_height: f64,
) -> ToastStackLayout {
pixel_toast_stack_layout(
stack,
font,
origin_x,
origin_y,
viewport_width,
viewport_height,
line_height as f32,
)
}
#[cfg(test)]
mod tests {
use super::super::headless::BitmapSurface;
use super::super::text::make_font;
use super::super::MacBackend;
use super::*;
use crate::event::{Rect as QRect, Viewport};
use crate::primitives::layout_metrics::pixel;
use crate::primitives::toast::{Toast, ToastButton, ToastCorner, ToastHit, ToastSeverity};
use crate::types::WidgetId;
use crate::Backend;
const W: u32 = 400;
const H: u32 = 240;
fn font() -> CTFont {
make_font("Menlo", 14.0).expect("Menlo installed")
}
fn toast(id: &str, title: &str, severity: ToastSeverity) -> Toast {
Toast {
id: WidgetId::new(id),
title: title.into(),
body: String::new(),
severity,
actions: Vec::new(),
accent: None,
}
}
fn sample_stack() -> ToastOverlay {
ToastOverlay {
id: WidgetId::new("toasts"),
corner: ToastCorner::BottomRight,
toasts: vec![
toast("t1", "Saved", ToastSeverity::Success),
toast("t2", "Error", ToastSeverity::Error),
],
focus: None,
}
}
fn paint_via_backend(stack: &ToastOverlay) -> (BitmapSurface, ToastStackLayout) {
paint_via_backend_at(stack, 0.0, 0.0)
}
fn paint_via_backend_at(
stack: &ToastOverlay,
origin_x: f32,
origin_y: f32,
) -> (BitmapSurface, ToastStackLayout) {
let surface = BitmapSurface::new(W, H);
surface.fill(0.0, 0.0, 0.0, 0.0);
let mut backend = MacBackend::new();
backend.set_current_font(font());
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let layout = std::cell::RefCell::new(None);
backend.enter_frame_scope(surface.context_ptr(), |b| {
let l = b.draw_toast_overlay(
QRect::new(origin_x, origin_y, W as f32 - origin_x, H as f32 - origin_y),
stack,
);
*layout.borrow_mut() = Some(l);
});
backend.end_frame();
(surface, layout.into_inner().unwrap())
}
#[test]
fn bottom_right_corner_positions_toasts_at_bottom_right() {
let stack = sample_stack();
let (_surface, layout) = paint_via_backend(&stack);
assert_eq!(layout.visible_toasts.len(), 2);
let first = &layout.visible_toasts[0];
assert!(
(first.bounds.x + first.bounds.width - (W as f32 - pixel::TOAST_MARGIN)).abs() < 1.0,
"toast right edge should be near viewport right: got x={}, width={}",
first.bounds.x,
first.bounds.width,
);
assert!(
(first.bounds.y + first.bounds.height - (H as f32 - pixel::TOAST_MARGIN)).abs() < 1.0,
"toast bottom edge should be near viewport bottom",
);
}
fn paint_settled(stack: &ToastOverlay) -> (BitmapSurface, ToastStackLayout) {
let surface = BitmapSurface::new(W, H);
surface.fill(0.0, 0.0, 0.0, 0.0);
let mut backend = MacBackend::new();
backend.set_current_font(font());
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let theme = Theme::default();
let style = backend.style();
let line_height = backend.line_height();
let layout = std::cell::RefCell::new(None);
backend.enter_frame_scope(surface.context_ptr(), |b| {
let l = crate::primitives::toast::native_surface_paint::paint(
stack,
b,
&theme,
&style,
0.0,
0.0,
W as f32,
H as f32,
line_height,
);
*layout.borrow_mut() = Some(l);
});
backend.end_frame();
(surface, layout.into_inner().unwrap())
}
#[test]
fn severity_tint_painted() {
let stack = ToastOverlay {
id: WidgetId::new("toasts"),
corner: ToastCorner::BottomRight,
toasts: vec![toast("err", "Boom", ToastSeverity::Error)],
focus: None,
};
let (surface, layout) = paint_settled(&stack);
let theme = Theme::default();
let t = &layout.visible_toasts[0];
let px = (t.bounds.x + t.bounds.width - pixel::TOAST_DISMISS_WIDTH - 4.0) as u32;
let py = (t.bounds.y + t.bounds.height - 2.0) as u32;
let (r, g, b, _) = surface.pixel(px, py);
let expected = theme.error_fg;
assert_eq!((r, g, b), (expected.r, expected.g, expected.b));
}
fn hit_test_dismiss_vs_body_at(origin_x: f32, origin_y: f32) {
let stack = sample_stack();
let (_surface, layout) = paint_via_backend_at(&stack, origin_x, origin_y);
let t = &layout.visible_toasts[0];
let db = t.dismiss_bounds.expect("dismiss bounds present");
let hit = layout.hit_test(db.x + db.width * 0.5, db.y + db.height * 0.5);
assert!(matches!(hit, ToastHit::Dismiss(_)), "hit was {:?}", hit);
let hit = layout.hit_test(t.bounds.x + 10.0, t.bounds.y + t.bounds.height * 0.5);
assert!(matches!(hit, ToastHit::Body(_)));
}
#[test]
fn hit_test_dismiss_vs_body() {
hit_test_dismiss_vs_body_at(0.0, 0.0);
}
#[test]
fn hit_test_dismiss_vs_body_at_nonzero_origin() {
hit_test_dismiss_vs_body_at(7.0, 13.0);
}
fn action_button_reserves_action_bounds_at(origin_x: f32, origin_y: f32) {
let stack = ToastOverlay {
id: WidgetId::new("toasts"),
corner: ToastCorner::BottomRight,
toasts: vec![Toast {
actions: vec![ToastButton {
id: WidgetId::new("undo"),
label: "Undo".into(),
primary: false,
}],
..toast("t", "Did the thing", ToastSeverity::Info)
}],
focus: None,
};
let (_surface, layout) = paint_via_backend_at(&stack, origin_x, origin_y);
let t = &layout.visible_toasts[0];
let ab = *t.action_rects.first().expect("action bounds present");
let hit = layout.hit_test(ab.x + ab.width * 0.5, ab.y + ab.height * 0.5);
assert!(matches!(hit, ToastHit::Action(_)), "hit was {:?}", hit);
}
#[test]
fn action_button_reserves_action_bounds() {
action_button_reserves_action_bounds_at(0.0, 0.0);
}
#[test]
fn action_button_reserves_action_bounds_at_nonzero_origin() {
action_button_reserves_action_bounds_at(7.0, 13.0);
}
#[test]
fn empty_stack_no_visible_toasts() {
let stack = ToastOverlay {
id: WidgetId::new("toasts"),
corner: ToastCorner::BottomRight,
toasts: vec![],
focus: None,
};
let (_surface, layout) = paint_via_backend(&stack);
assert!(layout.visible_toasts.is_empty());
}
}