#[cfg(all(target_os = "linux", feature = "linux-direct"))]
use cairo::{Context as CairoContext, LineCap, LineJoin};
#[cfg(all(
target_os = "linux",
feature = "linux-gtk",
not(feature = "linux-direct")
))]
use gtk4::cairo::{Context as CairoContext, LineCap, LineJoin};
use crate::ZsIcon;
#[cfg(all(
target_os = "linux",
any(feature = "linux-direct", feature = "linux-gtk")
))]
use crate::{Color, NativeDrawIconCommand};
pub(crate) trait SymbolicIconCanvas {
fn new_sub_path(&mut self);
fn move_to(&mut self, x: f64, y: f64);
fn line_to(&mut self, x: f64, y: f64);
fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64);
fn arc(&mut self, x: f64, y: f64, radius: f64, start: f64, end: f64);
fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64);
fn set_line_width(&mut self, width: f64);
fn close_path(&mut self);
fn stroke(&mut self);
fn fill(&mut self);
}
pub(crate) fn trace_arc(
canvas: &mut impl SymbolicIconCanvas,
x: f64,
y: f64,
radius: f64,
start: f64,
end: f64,
) {
let segments = ((end - start).abs() / std::f64::consts::FRAC_PI_2)
.ceil()
.max(1.0) as usize;
let delta = (end - start) / segments as f64;
canvas.line_to(x + radius * start.cos(), y + radius * start.sin());
for index in 0..segments {
let a = start + delta * index as f64;
let b = a + delta;
let k = (4.0 / 3.0) * (delta / 4.0).tan() * radius;
canvas.curve_to(
x + radius * a.cos() - k * a.sin(),
y + radius * a.sin() + k * a.cos(),
x + radius * b.cos() + k * b.sin(),
y + radius * b.sin() - k * b.cos(),
x + radius * b.cos(),
y + radius * b.sin(),
);
}
}
#[cfg(all(
target_os = "linux",
any(feature = "linux-direct", feature = "linux-gtk")
))]
pub(crate) fn draw_symbolic_icon(
context: &CairoContext,
command: &NativeDrawIconCommand,
color: Color,
) {
let rect = command.fitted_bounds(crate::Size {
width: 16,
height: 16,
});
if rect.width <= 0 || rect.height <= 0 || context.save().is_err() {
return;
}
context.rectangle(
f64::from(rect.x),
f64::from(rect.y),
f64::from(rect.width),
f64::from(rect.height),
);
context.clip();
context.translate(f64::from(rect.x), f64::from(rect.y));
context.scale(f64::from(rect.width) / 16.0, f64::from(rect.height) / 16.0);
context.set_source_rgba(
f64::from(color.r) / 255.0,
f64::from(color.g) / 255.0,
f64::from(color.b) / 255.0,
f64::from(color.a) / 255.0,
);
context.set_line_width(1.35);
context.set_line_cap(LineCap::Round);
context.set_line_join(LineJoin::Round);
let mut canvas = CairoIconCanvas(context);
draw_normalized_icon(&mut canvas, command.icon);
let _ = context.restore();
}
pub(crate) fn draw_normalized_icon(context: &mut impl SymbolicIconCanvas, icon: ZsIcon) {
match icon {
ZsIcon::App => {
for (x, y) in [(2.0, 2.0), (9.0, 2.0), (2.0, 9.0), (9.0, 9.0)] {
rounded_rect(context, RectF::new(x, y, 5.0, 5.0), 1.0);
fill(context);
}
}
ZsIcon::Calculator => {
rounded_rect(context, RectF::new(3.0, 1.5, 10.0, 13.0), 1.5);
stroke(context);
rounded_rect(context, RectF::new(5.0, 3.5, 6.0, 2.5), 0.5);
stroke(context);
for (x, y) in [
(5.2, 8.5),
(8.0, 8.5),
(10.8, 8.5),
(5.2, 11.5),
(8.0, 11.5),
(10.8, 11.5),
] {
context.arc(x, y, 0.65, 0.0, std::f64::consts::TAU);
fill(context);
}
}
ZsIcon::History => {
context.arc(8.0, 8.0, 5.2, -2.65, 2.55);
stroke(context);
path(context, &[(3.0, 3.8), (3.0, 7.0), (6.1, 7.0)]);
stroke(context);
path(context, &[(8.0, 4.8), (8.0, 8.0), (10.5, 9.5)]);
stroke(context);
}
ZsIcon::Backspace => {
polygon(
context,
&[
(1.5, 8.0),
(5.0, 3.2),
(14.2, 3.2),
(14.2, 12.8),
(5.0, 12.8),
],
);
stroke(context);
path(context, &[(7.0, 6.0), (11.0, 10.0)]);
path(context, &[(11.0, 6.0), (7.0, 10.0)]);
stroke(context);
}
ZsIcon::Add => {
path(context, &[(8.0, 3.0), (8.0, 13.0)]);
path(context, &[(3.0, 8.0), (13.0, 8.0)]);
stroke(context);
}
ZsIcon::Search => {
context.arc(6.8, 6.8, 4.1, 0.0, std::f64::consts::TAU);
stroke(context);
path(context, &[(9.8, 9.8), (13.6, 13.6)]);
stroke(context);
}
ZsIcon::Settings => {
context.arc(8.0, 8.0, 2.25, 0.0, std::f64::consts::TAU);
stroke(context);
context.arc(8.0, 8.0, 4.65, 0.0, std::f64::consts::TAU);
stroke(context);
for angle in (0..8).map(|index| f64::from(index) * std::f64::consts::FRAC_PI_4) {
let (sin, cos) = angle.sin_cos();
path(
context,
&[
(8.0 + cos * 5.1, 8.0 + sin * 5.1),
(8.0 + cos * 6.4, 8.0 + sin * 6.4),
],
);
}
stroke(context);
}
ZsIcon::Sidebar => {
rounded_rect(context, RectF::new(1.8, 2.3, 12.4, 11.4), 1.4);
stroke(context);
path(context, &[(5.6, 2.7), (5.6, 13.3)]);
stroke(context);
}
ZsIcon::Inspector => {
rounded_rect(context, RectF::new(2.0, 2.0, 12.0, 12.0), 1.2);
stroke(context);
path(context, &[(2.5, 5.3), (13.5, 5.3)]);
stroke(context);
context.arc(5.0, 3.7, 0.55, 0.0, std::f64::consts::TAU);
fill(context);
path(context, &[(5.0, 8.0), (11.2, 8.0)]);
path(context, &[(5.0, 10.5), (9.5, 10.5)]);
stroke(context);
}
ZsIcon::More => {
for x in [3.5, 8.0, 12.5] {
context.arc(x, 8.0, 1.0, 0.0, std::f64::consts::TAU);
fill(context);
}
}
ZsIcon::Attach => {
context.move_to(10.8, 5.0);
context.curve_to(10.8, 2.6, 7.5, 2.0, 6.0, 3.8);
context.line_to(2.9, 7.6);
context.curve_to(0.6, 10.4, 4.5, 14.7, 7.3, 11.8);
context.line_to(12.8, 6.0);
context.curve_to(14.6, 4.1, 12.0, 1.5, 10.2, 3.4);
context.line_to(5.0, 8.9);
context.curve_to(4.0, 9.9, 5.5, 11.2, 6.4, 10.2);
context.line_to(10.8, 5.5);
stroke(context);
}
ZsIcon::Enter => {
path(context, &[(13.0, 3.0), (13.0, 8.2), (4.0, 8.2)]);
path(context, &[(7.0, 5.2), (4.0, 8.2), (7.0, 11.2)]);
stroke(context);
}
ZsIcon::Send => {
polygon(context, &[(1.8, 2.2), (14.3, 8.0), (1.8, 13.8), (4.0, 8.0)]);
stroke(context);
path(context, &[(4.0, 8.0), (11.0, 8.0)]);
stroke(context);
}
ZsIcon::Stop => {
rounded_rect(context, RectF::new(3.0, 3.0, 10.0, 10.0), 1.5);
fill(context);
}
ZsIcon::Refresh | ZsIcon::Retry => {
context.arc(8.0, 8.0, 5.0, -2.7, 2.25);
stroke(context);
path(context, &[(3.1, 3.6), (3.1, 7.0), (6.4, 7.0)]);
stroke(context);
}
ZsIcon::Code => {
path(context, &[(6.2, 3.8), (2.0, 8.0), (6.2, 12.2)]);
path(context, &[(9.8, 3.8), (14.0, 8.0), (9.8, 12.2)]);
path(context, &[(9.2, 2.8), (6.8, 13.2)]);
stroke(context);
}
ZsIcon::Tool => {
context.arc(5.2, 5.0, 3.0, 0.7, 5.1);
stroke(context);
path(context, &[(7.0, 7.1), (13.5, 13.0)]);
stroke(context);
context.arc(13.0, 13.0, 1.0, 0.0, std::f64::consts::TAU);
stroke(context);
}
ZsIcon::Check => draw_check(context, 2.5, 5.2, 13.3),
ZsIcon::Info => {
circle_outline(context);
context.arc(8.0, 4.8, 0.75, 0.0, std::f64::consts::TAU);
fill(context);
path(context, &[(8.0, 7.2), (8.0, 11.2)]);
stroke(context);
}
ZsIcon::Success => {
circle_outline(context);
draw_check(context, 4.0, 6.3, 12.0);
}
ZsIcon::Warning => {
polygon(context, &[(8.0, 1.8), (14.2, 13.2), (1.8, 13.2)]);
stroke(context);
path(context, &[(8.0, 5.2), (8.0, 9.2)]);
stroke(context);
context.arc(8.0, 11.4, 0.65, 0.0, std::f64::consts::TAU);
fill(context);
}
ZsIcon::Error => {
circle_outline(context);
path(context, &[(5.3, 5.3), (10.7, 10.7)]);
path(context, &[(10.7, 5.3), (5.3, 10.7)]);
stroke(context);
}
ZsIcon::Minimize => {
path(context, &[(3.0, 11.0), (13.0, 11.0)]);
stroke(context);
}
ZsIcon::Close => {
path(context, &[(3.5, 3.5), (12.5, 12.5)]);
path(context, &[(12.5, 3.5), (3.5, 12.5)]);
stroke(context);
}
ZsIcon::Text => {
path(context, &[(3.0, 3.0), (13.0, 3.0)]);
path(context, &[(8.0, 3.0), (8.0, 13.0)]);
path(context, &[(5.5, 13.0), (10.5, 13.0)]);
stroke(context);
}
ZsIcon::Image => {
rounded_rect(context, RectF::new(2.0, 2.5, 12.0, 11.0), 1.2);
stroke(context);
context.arc(5.2, 5.7, 1.0, 0.0, std::f64::consts::TAU);
fill(context);
path(
context,
&[
(3.0, 12.0),
(7.0, 8.0),
(9.2, 10.2),
(11.0, 8.5),
(13.2, 11.0),
],
);
stroke(context);
}
ZsIcon::File => draw_file(context),
ZsIcon::Folder => {
context.move_to(1.8, 5.0);
context.line_to(6.4, 5.0);
context.line_to(7.6, 6.3);
context.line_to(14.2, 6.3);
context.line_to(13.2, 13.2);
context.line_to(2.8, 13.2);
context.close_path();
stroke(context);
path(context, &[(2.2, 5.0), (2.2, 3.3), (6.2, 3.3), (7.4, 5.0)]);
stroke(context);
}
ZsIcon::Save => {
rounded_rect(context, RectF::new(2.2, 2.0, 11.6, 12.0), 1.0);
stroke(context);
context.rectangle(4.0, 2.2, 6.5, 4.0);
stroke(context);
rounded_rect(context, RectF::new(4.2, 9.0, 7.6, 5.0), 0.7);
stroke(context);
}
ZsIcon::Undo => {
context.arc(8.8, 8.5, 4.5, -2.6, 2.2);
stroke(context);
path(context, &[(4.6, 3.6), (4.2, 7.0), (7.5, 6.3)]);
stroke(context);
}
ZsIcon::Cut => {
for (x, y) in [(4.2, 4.0), (4.2, 12.0)] {
context.arc(x, y, 2.0, 0.0, std::f64::consts::TAU);
stroke(context);
}
path(context, &[(5.8, 5.2), (13.2, 11.8)]);
path(context, &[(5.8, 10.8), (13.2, 4.2)]);
stroke(context);
}
ZsIcon::Pin => {
polygon(
context,
&[
(5.0, 2.0),
(11.0, 2.0),
(10.0, 6.2),
(12.2, 8.5),
(3.8, 8.5),
(6.0, 6.2),
],
);
stroke(context);
path(context, &[(8.0, 8.7), (8.0, 14.0)]);
stroke(context);
}
ZsIcon::Delete => {
rounded_rect(context, RectF::new(4.0, 4.7, 8.0, 9.0), 0.8);
stroke(context);
path(context, &[(2.8, 4.3), (13.2, 4.3)]);
path(context, &[(6.0, 2.3), (10.0, 2.3)]);
path(context, &[(6.5, 7.0), (6.5, 11.5)]);
path(context, &[(9.5, 7.0), (9.5, 11.5)]);
stroke(context);
}
ZsIcon::Copy => {
rounded_rect(context, RectF::new(5.0, 5.0, 8.5, 8.5), 1.0);
stroke(context);
rounded_rect(context, RectF::new(2.5, 2.5, 8.5, 8.5), 1.0);
stroke(context);
}
ZsIcon::Paste => {
rounded_rect(context, RectF::new(3.0, 3.2, 10.0, 11.0), 1.0);
stroke(context);
rounded_rect(context, RectF::new(5.3, 1.8, 5.4, 3.2), 0.8);
stroke(context);
path(context, &[(5.2, 8.0), (10.8, 8.0)]);
path(context, &[(5.2, 10.8), (9.2, 10.8)]);
stroke(context);
}
ZsIcon::Edit => {
polygon(
context,
&[
(3.0, 10.8),
(10.8, 3.0),
(13.0, 5.2),
(5.2, 13.0),
(2.4, 13.6),
],
);
stroke(context);
path(context, &[(9.6, 4.2), (11.8, 6.4)]);
stroke(context);
}
ZsIcon::Group => {
context.arc(8.0, 5.0, 2.2, 0.0, std::f64::consts::TAU);
stroke(context);
context.arc(3.7, 6.3, 1.6, 0.0, std::f64::consts::TAU);
context.arc(12.3, 6.3, 1.6, 0.0, std::f64::consts::TAU);
stroke(context);
context.arc(8.0, 13.2, 4.2, 3.35, 6.08);
stroke(context);
context.arc(3.8, 12.6, 2.7, 3.5, 5.75);
context.arc(12.2, 12.6, 2.7, 3.67, 5.92);
stroke(context);
}
ZsIcon::Phrase => {
rounded_rect(context, RectF::new(1.8, 2.5, 12.4, 9.0), 1.8);
stroke(context);
path(context, &[(5.0, 11.5), (4.0, 14.0), (8.0, 11.5)]);
stroke(context);
path(context, &[(4.5, 6.0), (11.5, 6.0)]);
path(context, &[(4.5, 8.5), (9.5, 8.5)]);
stroke(context);
}
ZsIcon::ChevronUp => draw_chevron(context, &[(3.5, 10.0), (8.0, 5.5), (12.5, 10.0)]),
ZsIcon::ChevronDown => draw_chevron(context, &[(3.5, 6.0), (8.0, 10.5), (12.5, 6.0)]),
ZsIcon::Calendar => {
rounded_rect(context, RectF::new(2.0, 3.2, 12.0, 10.8), 1.3);
stroke(context);
path(context, &[(2.5, 6.3), (13.5, 6.3)]);
path(context, &[(5.0, 1.8), (5.0, 4.5)]);
path(context, &[(11.0, 1.8), (11.0, 4.5)]);
stroke(context);
for (x, y) in [
(5.0, 9.0),
(8.0, 9.0),
(11.0, 9.0),
(5.0, 12.0),
(8.0, 12.0),
] {
context.arc(x, y, 0.55, 0.0, std::f64::consts::TAU);
fill(context);
}
}
ZsIcon::ChevronLeft => draw_chevron(context, &[(10.0, 3.5), (5.5, 8.0), (10.0, 12.5)]),
ZsIcon::ChevronRight => draw_chevron(context, &[(6.0, 3.5), (10.5, 8.0), (6.0, 12.5)]),
ZsIcon::PasswordReveal => {
context.move_to(1.5, 8.0);
context.curve_to(4.0, 3.8, 12.0, 3.8, 14.5, 8.0);
context.curve_to(12.0, 12.2, 4.0, 12.2, 1.5, 8.0);
context.close_path();
stroke(context);
context.arc(8.0, 8.0, 2.2, 0.0, std::f64::consts::TAU);
stroke(context);
}
}
}
fn draw_check(context: &mut impl SymbolicIconCanvas, left: f64, middle: f64, right: f64) {
path(context, &[(left, 8.0), (middle, 11.0), (right, 4.5)]);
stroke(context);
}
fn draw_file(context: &mut impl SymbolicIconCanvas) {
path(
context,
&[
(3.0, 1.8),
(9.5, 1.8),
(13.0, 5.3),
(13.0, 14.2),
(3.0, 14.2),
(3.0, 1.8),
],
);
stroke(context);
path(context, &[(9.5, 2.0), (9.5, 5.3), (12.8, 5.3)]);
stroke(context);
}
fn circle_outline(context: &mut impl SymbolicIconCanvas) {
context.arc(8.0, 8.0, 6.0, 0.0, std::f64::consts::TAU);
stroke(context);
}
fn draw_chevron(context: &mut impl SymbolicIconCanvas, points: &[(f64, f64)]) {
context.set_line_width(1.7);
path(context, points);
stroke(context);
}
fn path(context: &mut impl SymbolicIconCanvas, points: &[(f64, f64)]) {
if let Some((first, rest)) = points.split_first() {
context.move_to(first.0, first.1);
for point in rest {
context.line_to(point.0, point.1);
}
}
}
fn polygon(context: &mut impl SymbolicIconCanvas, points: &[(f64, f64)]) {
path(context, points);
context.close_path();
}
fn rounded_rect(context: &mut impl SymbolicIconCanvas, rect: RectF, radius: f64) {
let radius = radius.min(rect.width / 2.0).min(rect.height / 2.0);
context.new_sub_path();
context.arc(
rect.right() - radius,
rect.y + radius,
radius,
-std::f64::consts::FRAC_PI_2,
0.0,
);
context.arc(
rect.right() - radius,
rect.bottom() - radius,
radius,
0.0,
std::f64::consts::FRAC_PI_2,
);
context.arc(
rect.x + radius,
rect.bottom() - radius,
radius,
std::f64::consts::FRAC_PI_2,
std::f64::consts::PI,
);
context.arc(
rect.x + radius,
rect.y + radius,
radius,
std::f64::consts::PI,
std::f64::consts::PI * 1.5,
);
context.close_path();
}
fn stroke(context: &mut impl SymbolicIconCanvas) {
context.stroke();
}
fn fill(context: &mut impl SymbolicIconCanvas) {
context.fill();
}
#[cfg(all(
target_os = "linux",
any(feature = "linux-direct", feature = "linux-gtk")
))]
struct CairoIconCanvas<'a>(&'a CairoContext);
#[cfg(all(
target_os = "linux",
any(feature = "linux-direct", feature = "linux-gtk")
))]
impl SymbolicIconCanvas for CairoIconCanvas<'_> {
fn new_sub_path(&mut self) {
self.0.new_sub_path();
}
fn move_to(&mut self, x: f64, y: f64) {
self.0.move_to(x, y);
}
fn line_to(&mut self, x: f64, y: f64) {
self.0.line_to(x, y);
}
fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64) {
self.0.curve_to(x1, y1, x2, y2, x, y);
}
fn arc(&mut self, x: f64, y: f64, radius: f64, start: f64, end: f64) {
self.0.arc(x, y, radius, start, end);
}
fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) {
self.0.rectangle(x, y, width, height);
}
fn set_line_width(&mut self, width: f64) {
self.0.set_line_width(width);
}
fn close_path(&mut self) {
self.0.close_path();
}
fn stroke(&mut self) {
let _ = self.0.stroke();
}
fn fill(&mut self) {
let _ = self.0.fill();
}
}
#[derive(Clone, Copy)]
struct RectF {
x: f64,
y: f64,
width: f64,
height: f64,
}
impl RectF {
const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
Self {
x,
y,
width,
height,
}
}
fn right(self) -> f64 {
self.x + self.width
}
fn bottom(self) -> f64 {
self.y + self.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct RecordingCanvas {
points: usize,
paints: usize,
path_points: usize,
}
impl RecordingCanvas {
fn point(&mut self, x: f64, y: f64) {
assert!(x.is_finite() && y.is_finite());
assert!((-2.0..=18.0).contains(&x) && (-2.0..=18.0).contains(&y));
self.points += 1;
self.path_points += 1;
}
fn paint(&mut self) {
assert!(self.path_points > 0);
self.paints += 1;
self.path_points = 0;
}
}
impl SymbolicIconCanvas for RecordingCanvas {
fn new_sub_path(&mut self) {}
fn move_to(&mut self, x: f64, y: f64) {
self.point(x, y);
}
fn line_to(&mut self, x: f64, y: f64) {
self.point(x, y);
}
fn curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64) {
self.point(x1, y1);
self.point(x2, y2);
self.point(x, y);
}
fn arc(&mut self, x: f64, y: f64, radius: f64, start: f64, end: f64) {
trace_arc(self, x, y, radius, start, end);
}
fn rectangle(&mut self, x: f64, y: f64, width: f64, height: f64) {
self.point(x, y);
self.point(x + width, y + height);
}
fn set_line_width(&mut self, width: f64) {
assert!(width.is_finite() && width > 0.0);
}
fn close_path(&mut self) {}
fn stroke(&mut self) {
self.paint();
}
fn fill(&mut self) {
self.paint();
}
}
#[test]
fn every_semantic_icon_has_bounded_drawable_fallback_geometry() {
for icon in crate::ZsIcon::ALL {
let mut canvas = RecordingCanvas::default();
draw_normalized_icon(&mut canvas, icon);
assert!(canvas.points > 0 && canvas.paints > 0, "{icon:?}");
}
}
}