@@ -4208,108 +4208,63 @@ fn tray_icon_should_use_template() -> bool {
42084208 cfg!(target_os = "macos")
42094209}
42104210
4211- fn draw_short_logo_mark(rgba: &mut [u8], size: usize, color: [u8; 4], halo: bool) {
4212- let top = if halo {
4213- [
4214- (18.0, 1.8),
4215- (32.6, 8.8),
4216- (32.9, 13.4),
4217- (21.0, 19.3),
4218- (15.0, 19.3),
4219- (3.1, 13.4),
4220- (3.4, 8.8),
4221- ]
4222- } else {
4223- [
4224- (18.0, 3.1),
4225- (30.9, 9.2),
4226- (31.1, 12.3),
4227- (20.4, 17.5),
4228- (15.6, 17.5),
4229- (4.9, 12.3),
4230- (5.1, 9.2),
4231- ]
4232- };
4233- let bottom = if halo {
4234- [
4235- (14.0, 18.8),
4236- (22.0, 18.8),
4237- (33.0, 25.5),
4238- (32.4, 29.4),
4239- (21.0, 34.4),
4240- (15.0, 34.4),
4241- (3.6, 29.4),
4242- (3.0, 25.5),
4243- ]
4244- } else {
4245- [
4246- (14.8, 20.3),
4247- (21.2, 20.3),
4248- (31.0, 26.1),
4249- (30.5, 28.6),
4250- (20.4, 32.7),
4251- (15.6, 32.7),
4252- (5.5, 28.6),
4253- (5.0, 26.1),
4254- ]
4255- };
4256-
4257- draw_polygon(rgba, size, &top, color);
4258- draw_polygon(rgba, size, &bottom, color);
4259-
4260- let dot_radius = if halo { 2.8 } else { 1.65 };
4261- for center in [
4262- (4.6, 15.4),
4263- (4.6, 18.5),
4264- (4.6, 21.6),
4265- (31.4, 15.4),
4266- (31.4, 18.5),
4267- (31.4, 21.6),
4268- ] {
4269- draw_disc(rgba, size, center, dot_radius, color);
4270- }
4271- }
4211+ const TRAY_LOGO_MARK_PNG: &[u8] = include_bytes!("../assets/tray-locality-mark.png");
42724212
4273- fn draw_polygon(rgba: &mut [u8], size: usize, points: &[(f64, f64)], color: [u8; 4]) {
4274- for y in 0..size {
4275- for x in 0..size {
4276- let px = x as f64 + 0.5;
4277- let py = y as f64 + 0.5;
4278- let inside = point_in_polygon((px, py), points);
4279- let distance = points
4280- .iter()
4281- .zip(points.iter().cycle().skip(1))
4282- .take(points.len())
4283- .map(|(start, end)| distance_to_segment((px, py), *start, *end))
4284- .fold(f64::INFINITY, f64::min);
4285- let alpha = if inside {
4286- (distance + 0.6).clamp(0.0, 1.0)
4287- } else {
4288- (0.6 - distance).clamp(0.0, 1.0)
4289- };
4290- if alpha > 0.0 {
4291- blend_pixel(rgba, size, x, y, color, alpha);
4213+ fn draw_short_logo_mark(rgba: &mut [u8], size: usize, color: [u8; 4], halo: bool) {
4214+ let mark = tray_logo_mark_image();
4215+ if halo {
4216+ for y_offset in -1..=1 {
4217+ for x_offset in -1..=1 {
4218+ if x_offset != 0 || y_offset != 0 {
4219+ draw_image_alpha_mask(rgba, size, mark, x_offset, y_offset, color);
4220+ }
42924221 }
42934222 }
42944223 }
4224+ draw_image_alpha_mask(rgba, size, mark, 0, 0, color);
4225+ }
4226+
4227+ fn tray_logo_mark_image() -> &'static Image<'static> {
4228+ static TRAY_LOGO_MARK: OnceLock<Image<'static>> = OnceLock::new();
4229+ TRAY_LOGO_MARK.get_or_init(|| {
4230+ Image::from_bytes(TRAY_LOGO_MARK_PNG).expect("embedded tray logo PNG should decode")
4231+ })
42954232}
42964233
4297- fn point_in_polygon(point: (f64, f64), points: &[(f64, f64)]) -> bool {
4298- let mut inside = false;
4299- let mut previous = points[points.len() - 1];
4300- for current in points {
4301- let crosses = (current.1 > point.1) != (previous.1 > point.1);
4302- if crosses {
4303- let x_intersection = (previous.0 - current.0) * (point.1 - current.1)
4304- / (previous.1 - current.1)
4305- + current.0;
4306- if point.0 < x_intersection {
4307- inside = !inside;
4234+ fn draw_image_alpha_mask(
4235+ rgba: &mut [u8],
4236+ size: usize,
4237+ image: &Image<'_>,
4238+ x_offset: i32,
4239+ y_offset: i32,
4240+ color: [u8; 4],
4241+ ) {
4242+ let image_width = image.width() as usize;
4243+ let image_height = image.height() as usize;
4244+ let image_rgba = image.rgba();
4245+ for y in 0..image_height {
4246+ let target_y = y as i32 + y_offset;
4247+ if !(0..size as i32).contains(&target_y) {
4248+ continue;
4249+ }
4250+ for x in 0..image_width {
4251+ let target_x = x as i32 + x_offset;
4252+ if !(0..size as i32).contains(&target_x) {
4253+ continue;
4254+ }
4255+ let alpha = image_rgba[(y * image_width + x) * 4 + 3] as f64 / 255.0;
4256+ if alpha > 0.0 {
4257+ blend_pixel(
4258+ rgba,
4259+ size,
4260+ target_x as usize,
4261+ target_y as usize,
4262+ color,
4263+ alpha,
4264+ );
43084265 }
43094266 }
4310- previous = *current;
43114267 }
4312- inside
43134268}
43144269
43154270fn draw_disc(rgba: &mut [u8], size: usize, center: (f64, f64), radius: f64, color: [u8; 4]) {
@@ -4326,21 +4281,6 @@ fn draw_disc(rgba: &mut [u8], size: usize, center: (f64, f64), radius: f64, colo
43264281 }
43274282}
43284283
4329- fn distance_to_segment(point: (f64, f64), start: (f64, f64), end: (f64, f64)) -> f64 {
4330- let vx = end.0 - start.0;
4331- let vy = end.1 - start.1;
4332- let wx = point.0 - start.0;
4333- let wy = point.1 - start.1;
4334- let length_squared = vx * vx + vy * vy;
4335- if length_squared == 0.0 {
4336- return ((point.0 - start.0).powi(2) + (point.1 - start.1).powi(2)).sqrt();
4337- }
4338-
4339- let t = ((wx * vx + wy * vy) / length_squared).clamp(0.0, 1.0);
4340- let closest = (start.0 + t * vx, start.1 + t * vy);
4341- ((point.0 - closest.0).powi(2) + (point.1 - closest.1).powi(2)).sqrt()
4342- }
4343-
43444284fn blend_pixel(rgba: &mut [u8], size: usize, x: usize, y: usize, color: [u8; 4], coverage: f64) {
43454285 let idx = (y * size + x) * 4;
43464286 let src_alpha = (color[3] as f64 / 255.0) * coverage;
@@ -12412,20 +12352,40 @@ mod tests {
1241212352 .chunks_exact(4)
1241312353 .any(|pixel| { pixel[0] < 40 && pixel[1] < 50 && pixel[2] < 70 && pixel[3] > 200 })
1241412354 );
12415- for (x, y) in [(4 , 15), (4 , 18), (4 , 21), (32 , 15), (32 , 18), (32 , 21)] {
12355+ for (x, y) in [(5 , 15), (5 , 18), (5 , 21), (30 , 15), (30 , 18), (30 , 21)] {
1241612356 let pixel = tray_icon_pixel(&ready, x, y);
1241712357 assert!(
1241812358 pixel[0] < 40 && pixel[1] < 50 && pixel[2] < 70 && pixel[3] > 200,
1241912359 "expected short-logo side dot at ({x}, {y}), got {pixel:?}"
1242012360 );
1242112361 }
12422- for (x, y) in [(18, 6 ), (18, 27 )] {
12362+ for (x, y) in [(3, 15 ), (33, 15 )] {
1242312363 let pixel = tray_icon_pixel(&ready, x, y);
1242412364 assert!(
12425- pixel[0] < 40 && pixel[1] < 50 && pixel[2] < 70 && pixel[3] > 200,
12426- "expected short-logo stacked body at ({x}, {y}), got {pixel:?}"
12365+ pixel[3] < 80,
12366+ "expected side dot to stay inside the logo cap span at ({x}, {y}), got {pixel:?}"
12367+ );
12368+ }
12369+ for (x, y) in [(18, 4), (6, 11), (29, 11)] {
12370+ let pixel = tray_icon_pixel(&ready, x, y);
12371+ assert!(
12372+ pixel[0] < 40 && pixel[1] < 50 && pixel[2] < 70 && pixel[3] > 100,
12373+ "expected short-logo top outline at ({x}, {y}), got {pixel:?}"
1242712374 );
1242812375 }
12376+ let bottom_body = tray_icon_pixel(&ready, 18, 27);
12377+ assert!(
12378+ bottom_body[0] < 40
12379+ && bottom_body[1] < 50
12380+ && bottom_body[2] < 70
12381+ && bottom_body[3] > 200,
12382+ "expected short-logo bottom body, got {bottom_body:?}"
12383+ );
12384+ let top_center = tray_icon_pixel(&ready, 18, 9);
12385+ assert!(
12386+ top_center[3] < 80,
12387+ "expected short-logo top interior to stay transparent, got {top_center:?}"
12388+ );
1242912389 if tray_icon_should_use_template() {
1243012390 let ready_opaque = tray_icon_opaque_pixel_count(&ready);
1243112391 assert!(
0 commit comments