use crate::geometry::{Color, Rect, Size};
use crate::include_svg;
use crate::painter::Painter;
use crate::svg::SvgImage;
const DESKTOP_BACKGROUND: Color = Color::rgb(0x00, 0x80, 0x80);
const TITLEBAR_BG: Color = Color::NAVY;
const TITLEBAR_TEXT: Color = Color::WHITE;
const BORDER_DARK: Color = Color::BLACK;
const BORDER_MID: Color = Color::LIGHT_GRAY;
const BUTTON_BG: Color = Color::LIGHT_GRAY;
const BUTTON_HIGHLIGHT: Color = Color::WHITE;
const BUTTON_SHADOW: Color = Color::MID_GRAY;
const CLOSE_ICON: SvgImage = include_svg!("assets/chrome/close.svg");
const MINIMIZE_ICON: SvgImage = include_svg!("assets/chrome/minimize.svg");
const MAXIMIZE_ICON: SvgImage = include_svg!("assets/chrome/maximize.svg");
const SHADOW_ALPHA: f32 = 0x33 as f32;
const CHROME_FONT_SIZE: f32 = 12.0;
const BORDER_NORMAL: i32 = 4;
const BORDER_FIXED: i32 = 1;
const SHADOW_SIZE: i32 = 20;
const DEFAULT_MARGIN: i32 = 40;
const TITLE_PADDING: i32 = 6;
fn titlebar_height() -> i32 {
let base = (0.75 * CHROME_FONT_SIZE).round() as i32;
(base * 2 + 1).max(1)
}
fn icon_size() -> i32 {
let tbh = titlebar_height();
(tbh - 4).clamp(6, tbh.max(1))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WindowFrame {
Resizable,
Fixed,
Dialog,
}
impl WindowFrame {
fn border_width(self) -> i32 {
match self {
WindowFrame::Fixed => BORDER_FIXED,
_ => BORDER_NORMAL,
}
}
fn shows_minimize(self) -> bool {
matches!(self, WindowFrame::Resizable | WindowFrame::Fixed)
}
fn shows_maximize(self) -> bool {
matches!(self, WindowFrame::Resizable)
}
fn mid_color(self) -> Color {
match self {
WindowFrame::Dialog => TITLEBAR_BG,
_ => BORDER_MID,
}
}
}
#[derive(Clone, Debug)]
pub struct WindowChrome {
title: String,
frame: WindowFrame,
desktop_background: Color,
margin: i32,
}
impl WindowChrome {
pub fn new(title: impl Into<String>, frame: WindowFrame) -> Self {
Self {
title: title.into(),
frame,
desktop_background: DESKTOP_BACKGROUND,
margin: DEFAULT_MARGIN,
}
}
pub fn resizable(title: impl Into<String>) -> Self {
Self::new(title, WindowFrame::Resizable)
}
pub fn fixed(title: impl Into<String>) -> Self {
Self::new(title, WindowFrame::Fixed)
}
pub fn dialog(title: impl Into<String>) -> Self {
Self::new(title, WindowFrame::Dialog)
}
pub fn with_desktop_background(mut self, color: Color) -> Self {
self.desktop_background = color;
self
}
pub fn with_margin(mut self, margin: i32) -> Self {
self.margin = margin.max(0);
self
}
pub(crate) fn paints_background_pattern(&self) -> bool {
self.frame != WindowFrame::Dialog
}
}
pub(crate) struct Metrics {
scale: f32,
pub buffer: Size,
frame: Rect,
titlebar: Rect,
pub content: Rect,
bw: i32,
unit: i32,
shadow: i32,
}
pub(crate) fn metrics(content_phys: Size, scale: f32, chrome: &WindowChrome) -> Metrics {
let scale = scale.max(0.01);
let px = |logical: i32| (logical as f32 * scale).round() as i32;
let bw = px(chrome.frame.border_width());
let tbh = px(titlebar_height());
let unit = px(1).max(1);
let margin = px(chrome.margin);
let shadow = px(SHADOW_SIZE);
let cw = content_phys.w.max(1);
let ch = content_phys.h.max(1);
let frame_w = cw + bw * 2;
let frame_h = ch + tbh + unit + bw * 2;
let buffer = Size::new(frame_w + margin * 2, frame_h + margin * 2);
let frame = Rect::new(margin, margin, frame_w, frame_h);
let titlebar = Rect::new(frame.x + bw, frame.y + bw, cw, tbh);
let content = Rect::new(frame.x + bw, frame.y + bw + tbh + unit, cw, ch);
Metrics {
scale,
buffer,
frame,
titlebar,
content,
bw,
unit,
shadow,
}
}
pub(crate) fn paint(painter: &mut Painter, m: &Metrics, chrome: &WindowChrome) {
painter.fill(chrome.desktop_background);
draw_shadow(painter, m);
painter.fill_rect(m.frame, BORDER_DARK);
if chrome.frame != WindowFrame::Fixed {
let outer = m.unit;
let inner = m.unit;
let mid = (m.bw - outer - inner).max(0);
painter.fill_rect(m.frame.inset(outer), chrome.frame.mid_color());
painter.fill_rect(m.frame.inset(outer + mid), BORDER_DARK);
}
painter.fill_rect(m.titlebar, TITLEBAR_BG);
painter.fill_rect(
Rect::new(m.titlebar.x, m.titlebar.bottom(), m.titlebar.w, m.unit),
BORDER_DARK,
);
draw_buttons_and_title(painter, m, chrome);
}
struct ButtonSlots {
close: Rect,
minimize: Option<Rect>,
maximize: Option<Rect>,
}
fn button_slots(m: &Metrics, frame: WindowFrame) -> ButtonSlots {
let size = m.titlebar.h; let y = m.titlebar.y;
let close = Rect::new(m.titlebar.x, y, size, size);
let gap = m.unit.max(1);
let right_edge = m.titlebar.right();
let mut next_right = right_edge - size;
let maximize = frame.shows_maximize().then(|| {
let r = Rect::new(next_right, y, size, size);
next_right -= size + gap;
r
});
let minimize = frame
.shows_minimize()
.then(|| Rect::new(next_right, y, size, size));
ButtonSlots {
close,
minimize,
maximize,
}
}
fn draw_buttons_and_title(painter: &mut Painter, m: &Metrics, chrome: &WindowChrome) {
let slots = button_slots(m, chrome.frame);
let unit = m.unit.max(1);
let icon = (icon_size() as f32 * m.scale).round() as i32;
painter.fill_rect(slots.close, BUTTON_BG);
draw_divider(painter, slots.close.right(), m, unit);
CLOSE_ICON.draw(painter, icon_box(slots.close, icon));
if let Some(min) = slots.minimize {
draw_divider(painter, min.x - unit, m, unit);
draw_raised_button(painter, min, unit);
MINIMIZE_ICON.draw(painter, icon_box(min, icon));
}
if let Some(max) = slots.maximize {
draw_divider(painter, max.x - unit, m, unit);
draw_raised_button(painter, max, unit);
MAXIMIZE_ICON.draw(painter, icon_box(max, icon));
}
draw_title(painter, m, &slots, chrome);
}
fn icon_box(button: Rect, size: i32) -> Rect {
Rect::new(
button.x + (button.w - size) / 2,
button.y + (button.h - size) / 2,
size,
size,
)
}
fn draw_divider(painter: &mut Painter, x: i32, m: &Metrics, unit: i32) {
if x < m.titlebar.x {
return;
}
painter.fill_rect(Rect::new(x, m.titlebar.y, unit, m.titlebar.h), BORDER_DARK);
}
fn draw_raised_button(painter: &mut Painter, r: Rect, unit: i32) {
painter.fill_rect(r, BUTTON_BG);
painter.fill_rect(Rect::new(r.x, r.y, r.w, unit), BUTTON_HIGHLIGHT);
painter.fill_rect(Rect::new(r.x, r.y, unit, r.h), BUTTON_HIGHLIGHT);
if r.w >= 3 * unit && r.h >= 3 * unit {
painter.fill_rect(
Rect::new(r.x + unit, r.bottom() - 2 * unit, r.w - 2 * unit, unit),
BUTTON_SHADOW,
);
painter.fill_rect(
Rect::new(r.right() - 2 * unit, r.y + unit, unit, r.h - 2 * unit),
BUTTON_SHADOW,
);
}
painter.fill_rect(Rect::new(r.x, r.bottom() - unit, r.w, unit), BUTTON_SHADOW);
painter.fill_rect(Rect::new(r.right() - unit, r.y, unit, r.h), BUTTON_SHADOW);
}
fn draw_title(painter: &mut Painter, m: &Metrics, slots: &ButtonSlots, chrome: &WindowChrome) {
if chrome.title.is_empty() || painter.font().is_none() {
return;
}
let pad = (TITLE_PADDING as f32 * m.scale).round() as i32;
let gap = m.unit.max(1);
let text_start = slots.close.right() + gap + pad;
let right_x = slots
.minimize
.or(slots.maximize)
.map(|r| r.x)
.unwrap_or(m.titlebar.right());
let text_end = right_x - gap - pad;
let text_w = text_end - text_start;
if text_w <= 0 {
return;
}
let font_px = CHROME_FONT_SIZE * m.scale;
let text_h = painter.measure_text(&chrome.title, font_px).h;
let ty = m.titlebar.y + ((m.titlebar.h - text_h) / 2).max(0);
let clip = painter.push_clip(Rect::new(text_start, m.titlebar.y, text_w, m.titlebar.h));
painter.text(text_start, ty, &chrome.title, font_px, TITLEBAR_TEXT);
painter.restore_clip(clip);
}
fn draw_shadow(painter: &mut Painter, m: &Metrics) {
let s = m.shadow;
if s <= 0 {
return;
}
let s_f = s as f32;
let shift = s / 2;
let occ_x = m.frame.x;
let occ_y = m.frame.y + shift;
let occ_w = m.frame.w;
let occ_h = (m.frame.h - shift).max(1);
let cx = occ_x as f32 + occ_w as f32 / 2.0;
let cy = occ_y as f32 + occ_h as f32 / 2.0;
let hx = occ_w as f32 / 2.0;
let hy = occ_h as f32 / 2.0;
let r = (shift as f32).clamp(0.0, hx.min(hy).max(0.0));
let x0 = (occ_x - s - 1).max(0);
let x1 = (occ_x + occ_w + s + 1).min(m.buffer.w);
let y0 = (occ_y - s - 1).max(0);
let y1 = (occ_y + occ_h + s + 1).min(m.buffer.h);
for py in y0..y1 {
for px in x0..x1 {
let dx = (px as f32 + 0.5) - cx;
let dy = (py as f32 + 0.5) - cy;
let qx = dx.abs() - (hx - r);
let qy = dy.abs() - (hy - r);
let outside = (qx.max(0.0).powi(2) + qy.max(0.0).powi(2)).sqrt();
let inside = qx.max(qy).min(0.0);
let dist = outside + inside - r;
if dist <= 0.0 || dist > s_f {
continue;
}
let falloff = 1.0 - dist / s_f;
let alpha = (SHADOW_ALPHA * falloff * falloff).round();
if alpha <= 0.0 {
continue;
}
painter.blend_pixel_phys(px, py, Color::BLACK, alpha as u8);
}
}
}