chore: share macOS traffic lights for screenshots

This commit is contained in:
Tomás Romero committed 2026-10-11 16:29:11 +00:00
1 parent 12629c6531
commit 7823ecc422
4 files changed
+155 -104

No files matched your search

@@ -0,0 +1,32 @@
//! Paint macOS traffic lights onto desktop screenshots that slint-viewer
//! rendered with the integrated macOS window frame.
#[path = "../src/screenshot_traffic_lights.rs"]
mod screenshot_traffic_lights;
use screenshot_traffic_lights::Metrics;
use std::env;
/// generate-screenshots.sh renders at SLINT_SCALE_FACTOR=2.
const SCALE: f32 = 2.0;
/// `WindowChrome`'s macOS 26 defaults, centred on the 44px desktop top bar.
const METRICS: Metrics = Metrics {
x: 20.0,
spacing: 23.0,
size: 14.0,
center_y: 22.0,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let paths: Vec<_> = env::args_os().skip(1).collect();
if paths.is_empty() {
return Err("usage: paint-screenshot-traffic-lights SCREENSHOT...".into());
}
for path in paths {
let mut image = image::open(&path)?.into_rgb8();
screenshot_traffic_lights::paint(&mut image, SCALE, &METRICS);
image.save(&path)?;
}
Ok(())
}
+2
View File
@@ -35,6 +35,8 @@ mod reader_validation;
mod screenshot_frameless;
#[cfg(test)]
mod screenshot_fixture;
#[cfg(test)]
mod screenshot_traffic_lights;
mod remote;
mod renderer;
mod renderer_input_controller;
+17 -104
View File
@@ -5,9 +5,8 @@
//! application window loads the same prepared Cloudflare fixture as the normal
//! screenshots and is rendered offscreen, so the images
//! follow the shipping layout. macOS keeps AppKit's native traffic lights,
//! which an offscreen renderer cannot draw; they are painted in the standard
//! macOS colours at macOS 26's size and spacing, where the application moves
//! them to.
//! which an offscreen renderer cannot draw; `screenshot_traffic_lights` paints
//! them at macOS 26's size and spacing, where the application moves them to.
use super::*;
use slint::Rgb8Pixel;
use slint::platform::{
@@ -26,100 +25,20 @@ impl Platform for Headless {
}
}
/// The standard macOS window-control colours: each light's fill and the
/// slightly darker rim AppKit draws around it.
struct TrafficLight {
fill: [u8; 3],
rim: [u8; 3],
}
const TRAFFIC_LIGHTS: [TrafficLight; 3] = [
TrafficLight {
fill: [0xff, 0x5f, 0x57],
rim: [0xe0, 0x44, 0x3e],
},
TrafficLight {
fill: [0xfe, 0xbc, 0x2e],
rim: [0xde, 0xa1, 0x23],
},
TrafficLight {
fill: [0x28, 0xc8, 0x40],
rim: [0x1a, 0xab, 0x29],
},
];
/// Paint one antialiased light of `diameter` (physical pixels) with a
/// hairline rim.
fn paint_traffic_light(
pixels: &mut [Rgb8Pixel],
width: usize,
center: (f32, f32),
diameter: f32,
light: &TrafficLight,
) {
let radius = diameter / 2.0;
let rim_width = 0.5 * SCALE;
let (cx, cy) = center;
let x0 = (cx - radius - 1.0).floor().max(0.0) as usize;
let y0 = (cy - radius - 1.0).floor().max(0.0) as usize;
let x1 = (cx + radius + 1.0).ceil() as usize;
let y1 = (cy + radius + 1.0).ceil() as usize;
const SAMPLES: usize = 4;
for y in y0..y1 {
for x in x0..x1 {
let mut inside = 0.0;
let mut rim = 0.0;
for sy in 0..SAMPLES {
for sx in 0..SAMPLES {
let px = x as f32 + (sx as f32 + 0.5) / SAMPLES as f32 - cx;
let py = y as f32 + (sy as f32 + 0.5) / SAMPLES as f32 - cy;
let distance = (px * px + py * py).sqrt();
if distance <= radius {
inside += 1.0;
if distance > radius - rim_width {
rim += 1.0;
}
}
}
}
if inside == 0.0 {
continue;
}
let coverage = inside / (SAMPLES * SAMPLES) as f32;
let rim = rim / inside;
let shade = |channel: usize| {
let fill = f32::from(light.fill[channel]);
fill + (f32::from(light.rim[channel]) - fill) * rim
};
let pixel = &mut pixels[y * width + x];
let blend = |base: u8, color: f32| {
(f32::from(base) * (1.0 - coverage) + color * coverage).round() as u8
};
pixel.r = blend(pixel.r, shade(0));
pixel.g = blend(pixel.g, shade(1));
pixel.b = blend(pixel.b, shade(2));
}
}
}
/// Paint the lights where the application places the native buttons, using
/// the macOS 26 metrics `WindowChrome` defaults to.
fn paint_traffic_lights(app: &AppWindow, pixels: &mut [Rgb8Pixel], width: usize) {
fn paint_traffic_lights(app: &AppWindow, image: &mut image::RgbImage) {
let chrome = app.global::<WindowChrome>();
let x = chrome.get_traffic_light_x();
let spacing = chrome.get_traffic_light_spacing();
let size = chrome.get_traffic_light_size();
let center_y = chrome.get_band_height() / 2.0;
for (index, light) in TRAFFIC_LIGHTS.iter().enumerate() {
let center_x = x + index as f32 * spacing + size / 2.0;
paint_traffic_light(
pixels,
width,
(center_x * SCALE, center_y * SCALE),
size * SCALE,
light,
);
}
screenshot_traffic_lights::paint(
image,
SCALE,
&screenshot_traffic_lights::Metrics {
x: chrome.get_traffic_light_x(),
spacing: chrome.get_traffic_light_spacing(),
size: chrome.get_traffic_light_size(),
center_y: chrome.get_band_height() / 2.0,
},
);
}
#[test]
@@ -191,19 +110,13 @@ fn frameless_screenshots() {
window.draw_if_needed(|r| {
r.render(&mut pixels, physical_width as usize);
});
let bytes: Vec<u8> = pixels.iter().flat_map(|p| [p.r, p.g, p.b]).collect();
let mut image = image::RgbImage::from_raw(physical_width, physical_height, bytes).unwrap();
let chrome = app.global::<WindowChrome>();
if chrome.get_platform() == "macos" && chrome.get_controls_visible() {
paint_traffic_lights(&app, &mut pixels, physical_width as usize);
paint_traffic_lights(&app, &mut image);
}
let bytes: Vec<u8> = pixels.iter().flat_map(|p| [p.r, p.g, p.b]).collect();
image::save_buffer(
out.join(format!("{name}.png")),
&bytes,
physical_width,
physical_height,
image::ColorType::Rgb8,
)
.unwrap();
image.save(out.join(format!("{name}.png"))).unwrap();
};
let chrome = app.global::<WindowChrome>();
let controls = |names: &[&str]| {
+104
View File
@@ -0,0 +1,104 @@
//! macOS traffic lights for screenshots.
//!
//! AppKit draws the native window controls, so neither the offscreen
//! renderer nor slint-viewer can. Screenshots paint them in the standard
//! macOS colours where the application moves the native buttons. Shared by
//! the frameless screenshot test and `examples/paint-screenshot-traffic-lights.rs`,
//! so this file depends on nothing but `image`.
use image::RgbImage;
/// Where the application places the native buttons, in logical pixels.
pub(crate) struct Metrics {
/// Left edge of the first light.
pub(crate) x: f32,
/// Distance between the left edges of neighbouring lights.
pub(crate) spacing: f32,
pub(crate) size: f32,
pub(crate) center_y: f32,
}
/// The standard macOS window-control colours: each light's fill and the
/// slightly darker rim AppKit draws around it.
struct TrafficLight {
fill: [u8; 3],
rim: [u8; 3],
}
const TRAFFIC_LIGHTS: [TrafficLight; 3] = [
TrafficLight {
fill: [0xff, 0x5f, 0x57],
rim: [0xe0, 0x44, 0x3e],
},
TrafficLight {
fill: [0xfe, 0xbc, 0x2e],
rim: [0xde, 0xa1, 0x23],
},
TrafficLight {
fill: [0x28, 0xc8, 0x40],
rim: [0x1a, 0xab, 0x29],
},
];
/// Paint the three lights onto an image rendered at `scale`.
pub(crate) fn paint(image: &mut RgbImage, scale: f32, metrics: &Metrics) {
for (index, light) in TRAFFIC_LIGHTS.iter().enumerate() {
let center_x = metrics.x + index as f32 * metrics.spacing + metrics.size / 2.0;
paint_light(
image,
(center_x * scale, metrics.center_y * scale),
metrics.size * scale,
0.5 * scale,
light,
);
}
}
/// Paint one antialiased light of `diameter` (physical pixels) with a
/// hairline rim.
fn paint_light(
image: &mut RgbImage,
center: (f32, f32),
diameter: f32,
rim_width: f32,
light: &TrafficLight,
) {
let radius = diameter / 2.0;
let (cx, cy) = center;
let x0 = (cx - radius - 1.0).floor().max(0.0) as u32;
let y0 = (cy - radius - 1.0).floor().max(0.0) as u32;
let x1 = ((cx + radius + 1.0).ceil() as u32).min(image.width());
let y1 = ((cy + radius + 1.0).ceil() as u32).min(image.height());
const SAMPLES: usize = 4;
for y in y0..y1 {
for x in x0..x1 {
let mut inside = 0.0;
let mut rim = 0.0;
for sy in 0..SAMPLES {
for sx in 0..SAMPLES {
let px = x as f32 + (sx as f32 + 0.5) / SAMPLES as f32 - cx;
let py = y as f32 + (sy as f32 + 0.5) / SAMPLES as f32 - cy;
let distance = (px * px + py * py).sqrt();
if distance <= radius {
inside += 1.0;
if distance > radius - rim_width {
rim += 1.0;
}
}
}
}
if inside == 0.0 {
continue;
}
let coverage = inside / (SAMPLES * SAMPLES) as f32;
let rim = rim / inside;
let pixel = image.get_pixel_mut(x, y);
for channel in 0..3 {
let fill = f32::from(light.fill[channel]);
let color = fill + (f32::from(light.rim[channel]) - fill) * rim;
let base = f32::from(pixel.0[channel]);
pixel.0[channel] = (base * (1.0 - coverage) + color * coverage).round() as u8;
}
}
}
}