Vendor things

This commit is contained in:
John Doty 2024-03-08 11:03:01 -08:00
parent 5deceec006
commit 977e3c17e5
19434 changed files with 10682014 additions and 0 deletions

View file

@ -0,0 +1,345 @@
use smithay_client_toolkit::window::ButtonState;
use tiny_skia::{FillRule, PathBuilder, PixmapMut, Rect, Stroke, Transform};
use crate::{
theme::{ColorMap, BORDER_SIZE},
Location, SkiaResult,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ButtonKind {
Close,
Maximize,
Minimize,
}
#[derive(Default, Debug)]
pub(crate) struct Button {
x: f32,
y: f32,
size: f32,
}
impl Button {
pub fn radius(&self) -> f32 {
self.size / 2.0
}
pub fn x(&self) -> f32 {
self.x
}
pub fn center_x(&self) -> f32 {
self.x + self.radius()
}
pub fn center_y(&self) -> f32 {
self.y + self.radius()
}
fn contains(&self, x: f32, y: f32) -> bool {
x > self.x && x < self.x + self.size && y > self.y && y < self.y + self.size
}
}
impl Button {
pub fn draw_minimize(
&self,
scale: f32,
colors: &ColorMap,
mouses: &[Location],
pixmap: &mut PixmapMut,
) -> SkiaResult {
let btn_state = if mouses.contains(&Location::Button(ButtonKind::Minimize)) {
ButtonState::Hovered
} else {
ButtonState::Idle
};
let radius = self.radius();
let x = self.center_x();
let y = self.center_y();
let circle = PathBuilder::from_circle(x, y, radius)?;
let button_bg = if btn_state == ButtonState::Hovered {
colors.button_hover_paint()
} else {
colors.button_idle_paint()
};
pixmap.fill_path(
&circle,
&button_bg,
FillRule::Winding,
Transform::identity(),
None,
);
let mut button_icon_paint = colors.button_icon_paint();
button_icon_paint.anti_alias = false;
let len = 8.0 * scale;
let hlen = len / 2.0;
pixmap.fill_rect(
Rect::from_xywh(x - hlen, y + hlen, len, 1.0 * scale)?,
&button_icon_paint,
Transform::identity(),
None,
);
Some(())
}
pub fn draw_maximize(
&self,
scale: f32,
colors: &ColorMap,
mouses: &[Location],
maximizable: bool,
is_maximized: bool,
pixmap: &mut PixmapMut,
) -> SkiaResult {
let btn_state = if !maximizable {
ButtonState::Disabled
} else if mouses
.iter()
.any(|&l| l == Location::Button(ButtonKind::Maximize))
{
ButtonState::Hovered
} else {
ButtonState::Idle
};
let radius = self.radius();
let x = self.center_x();
let y = self.center_y();
let path1 = {
let mut pb = PathBuilder::new();
pb.push_circle(x, y, radius);
pb.finish()?
};
let button_bg = if btn_state == ButtonState::Hovered {
colors.button_hover_paint()
} else {
colors.button_idle_paint()
};
pixmap.fill_path(
&path1,
&button_bg,
FillRule::Winding,
Transform::identity(),
None,
);
let path2 = {
let size = 8.0 * scale;
let hsize = size / 2.0;
let mut pb = PathBuilder::new();
let x = x - hsize;
let y = y - hsize;
pb.push_rect(x, y, size, size);
if is_maximized {
if let Some(rect) = Rect::from_xywh(x + 2.0, y - 2.0, size, size) {
pb.move_to(rect.left(), rect.top());
pb.line_to(rect.right(), rect.top());
pb.line_to(rect.right(), rect.bottom());
}
}
pb.finish()?
};
let mut button_icon_paint = colors.button_icon_paint();
button_icon_paint.anti_alias = false;
pixmap.stroke_path(
&path2,
&button_icon_paint,
&Stroke {
width: 1.0 * scale,
..Default::default()
},
Transform::identity(),
None,
);
Some(())
}
pub fn draw_close(
&self,
scale: f32,
colors: &ColorMap,
mouses: &[Location],
pixmap: &mut PixmapMut,
) -> SkiaResult {
// Draw the close button
let btn_state = if mouses
.iter()
.any(|&l| l == Location::Button(ButtonKind::Close))
{
ButtonState::Hovered
} else {
ButtonState::Idle
};
let radius = self.radius();
let x = self.center_x();
let y = self.center_y();
let path1 = {
let mut pb = PathBuilder::new();
pb.push_circle(x, y, radius);
pb.finish()?
};
let button_bg = if btn_state == ButtonState::Hovered {
colors.button_hover_paint()
} else {
colors.button_idle_paint()
};
pixmap.fill_path(
&path1,
&button_bg,
FillRule::Winding,
Transform::identity(),
None,
);
let x_icon = {
let size = 3.5 * scale;
let mut pb = PathBuilder::new();
{
let sx = x - size;
let sy = y - size;
let ex = x + size;
let ey = y + size;
pb.move_to(sx, sy);
pb.line_to(ex, ey);
pb.close();
}
{
let sx = x - size;
let sy = y + size;
let ex = x + size;
let ey = y - size;
pb.move_to(sx, sy);
pb.line_to(ex, ey);
pb.close();
}
pb.finish()?
};
let mut button_icon_paint = colors.button_icon_paint();
button_icon_paint.anti_alias = true;
pixmap.stroke_path(
&x_icon,
&button_icon_paint,
&Stroke {
width: 1.1 * scale,
..Default::default()
},
Transform::identity(),
None,
);
Some(())
}
}
#[derive(Debug)]
pub(crate) struct Buttons {
pub close: Button,
pub maximize: Button,
pub minimize: Button,
w: u32,
h: u32,
scale: u32,
}
impl Default for Buttons {
fn default() -> Self {
Self {
close: Default::default(),
maximize: Default::default(),
minimize: Default::default(),
scale: 1,
w: 0,
h: super::theme::HEADER_SIZE,
}
}
}
impl Buttons {
pub fn arrange(&mut self, w: u32) {
self.w = w;
let scale = self.scale as f32;
let margin_top = BORDER_SIZE as f32 * scale;
let margin = 5.0 * scale;
let spacing = 13.0 * scale;
let size = 12.0 * 2.0 * scale;
let mut x = w as f32 * scale - margin - BORDER_SIZE as f32 * scale;
let y = margin + margin_top;
x -= size;
self.close.x = x;
self.close.y = y;
self.close.size = size;
x -= size;
x -= spacing;
self.maximize.x = x;
self.maximize.y = y;
self.maximize.size = size;
x -= size;
x -= spacing;
self.minimize.x = x;
self.minimize.y = y;
self.minimize.size = size;
}
pub fn update_scale(&mut self, scale: u32) {
if self.scale != scale {
self.scale = scale;
self.arrange(self.w);
}
}
pub fn find_button(&self, x: f64, y: f64) -> Location {
let x = x as f32 * self.scale as f32;
let y = y as f32 * self.scale as f32;
if self.close.contains(x, y) {
Location::Button(ButtonKind::Close)
} else if self.maximize.contains(x, y) {
Location::Button(ButtonKind::Maximize)
} else if self.minimize.contains(x, y) {
Location::Button(ButtonKind::Minimize)
} else {
Location::Head
}
}
pub fn scaled_size(&self) -> (u32, u32) {
(self.w * self.scale, self.h * self.scale)
}
}

View file

@ -0,0 +1,24 @@
//! System configuration.
use std::process::Command;
/// Query system to see if dark theming should be preferred.
pub(crate) fn prefer_dark() -> bool {
// outputs something like: `variant variant uint32 1`
let stdout = Command::new("dbus-send")
.arg("--reply-timeout=100")
.arg("--print-reply=literal")
.arg("--dest=org.freedesktop.portal.Desktop")
.arg("/org/freedesktop/portal/desktop")
.arg("org.freedesktop.portal.Settings.Read")
.arg("string:org.freedesktop.appearance")
.arg("string:color-scheme")
.output()
.ok()
.and_then(|out| String::from_utf8(out.stdout).ok());
if matches!(stdout, Some(ref s) if s.is_empty()) {
log::error!("XDG Settings Portal did not return response in time: timeout: 100ms, key: color-scheme");
}
matches!(stdout, Some(s) if s.trim().ends_with("uint32 1"))
}

View file

@ -0,0 +1,827 @@
mod buttons;
mod config;
mod parts;
mod pointer;
mod surface;
pub mod theme;
mod title;
use crate::theme::ColorMap;
use buttons::{ButtonKind, Buttons};
use client::{
protocol::{wl_compositor, wl_seat, wl_shm, wl_subcompositor, wl_surface},
Attached, DispatchData,
};
use parts::Parts;
use pointer::PointerUserData;
use smithay_client_toolkit::{
reexports::client,
seat::pointer::{ThemeManager, ThemeSpec, ThemedPointer},
shm::AutoMemPool,
window::{Frame, FrameRequest, State, WindowState},
};
use std::{cell::RefCell, fmt, rc::Rc};
use theme::{ColorTheme, BORDER_SIZE, HEADER_SIZE};
use tiny_skia::{
ClipMask, Color, FillRule, Paint, Path, PathBuilder, Pixmap, PixmapMut, PixmapPaint, Point,
Rect, Transform,
};
use title::TitleText;
type SkiaResult = Option<()>;
/*
* Utilities
*/
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Location {
None,
Head,
Top,
TopRight,
Right,
BottomRight,
Bottom,
BottomLeft,
Left,
TopLeft,
Button(ButtonKind),
}
/*
* The core frame
*/
struct Inner {
parts: Parts,
size: (u32, u32),
resizable: bool,
theme_over_surface: bool,
implem: Box<dyn FnMut(FrameRequest, u32, DispatchData)>,
maximized: bool,
fullscreened: bool,
tiled: bool,
}
impl fmt::Debug for Inner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Inner")
.field("parts", &self.parts)
.field("size", &self.size)
.field("resizable", &self.resizable)
.field("theme_over_surface", &self.theme_over_surface)
.field(
"implem",
&"FnMut(FrameRequest, u32, DispatchData) -> { ... }",
)
.field("maximized", &self.maximized)
.field("fullscreened", &self.fullscreened)
.finish()
}
}
fn precise_location(buttons: &Buttons, old: Location, width: u32, x: f64, y: f64) -> Location {
match old {
Location::Head
| Location::Button(_)
| Location::Top
| Location::TopLeft
| Location::TopRight => match buttons.find_button(x, y) {
Location::Head => {
if y <= f64::from(BORDER_SIZE) {
if x <= f64::from(BORDER_SIZE) {
Location::TopLeft
} else if x >= f64::from(width + BORDER_SIZE) {
Location::TopRight
} else {
Location::Top
}
} else if x < f64::from(BORDER_SIZE) {
Location::TopLeft
} else if x > f64::from(width) {
Location::TopRight
} else {
Location::Head
}
}
other => other,
},
Location::Bottom | Location::BottomLeft | Location::BottomRight => {
if x <= f64::from(BORDER_SIZE) {
Location::BottomLeft
} else if x >= f64::from(width + BORDER_SIZE) {
Location::BottomRight
} else {
Location::Bottom
}
}
other => other,
}
}
#[derive(Debug, Clone)]
pub struct FrameConfig {
pub theme: ColorTheme,
}
impl FrameConfig {
pub fn auto() -> Self {
Self {
theme: ColorTheme::auto(),
}
}
pub fn light() -> Self {
Self {
theme: ColorTheme::light(),
}
}
pub fn dark() -> Self {
Self {
theme: ColorTheme::dark(),
}
}
}
/// A simple set of decorations
#[derive(Debug)]
pub struct AdwaitaFrame {
base_surface: wl_surface::WlSurface,
compositor: Attached<wl_compositor::WlCompositor>,
subcompositor: Attached<wl_subcompositor::WlSubcompositor>,
inner: Rc<RefCell<Inner>>,
pool: AutoMemPool,
active: WindowState,
hidden: bool,
pointers: Vec<ThemedPointer>,
themer: ThemeManager,
surface_version: u32,
buttons: Rc<RefCell<Buttons>>,
colors: ColorTheme,
title: Option<String>,
title_text: Option<TitleText>,
}
impl Frame for AdwaitaFrame {
type Error = ::std::io::Error;
type Config = FrameConfig;
fn init(
base_surface: &wl_surface::WlSurface,
compositor: &Attached<wl_compositor::WlCompositor>,
subcompositor: &Attached<wl_subcompositor::WlSubcompositor>,
shm: &Attached<wl_shm::WlShm>,
theme_manager: Option<ThemeManager>,
implementation: Box<dyn FnMut(FrameRequest, u32, DispatchData)>,
) -> Result<AdwaitaFrame, ::std::io::Error> {
let (themer, theme_over_surface) = if let Some(theme_manager) = theme_manager {
(theme_manager, false)
} else {
(
ThemeManager::init(ThemeSpec::System, compositor.clone(), shm.clone()),
true,
)
};
let inner = Rc::new(RefCell::new(Inner {
parts: Parts::default(),
size: (1, 1),
resizable: true,
implem: implementation,
theme_over_surface,
maximized: false,
fullscreened: false,
tiled: false,
}));
let pool = AutoMemPool::new(shm.clone())?;
let colors = ColorTheme::auto();
Ok(AdwaitaFrame {
base_surface: base_surface.clone(),
compositor: compositor.clone(),
subcompositor: subcompositor.clone(),
inner,
pool,
active: WindowState::Inactive,
hidden: true,
pointers: Vec::new(),
themer,
surface_version: compositor.as_ref().version(),
buttons: Default::default(),
title: None,
title_text: TitleText::new(colors.active.font_color),
colors,
})
}
fn new_seat(&mut self, seat: &Attached<wl_seat::WlSeat>) {
let inner = self.inner.clone();
let buttons = self.buttons.clone();
let pointer = self.themer.theme_pointer_with_impl(
seat,
move |event, pointer: ThemedPointer, ddata: DispatchData| {
if let Some(data) = pointer
.as_ref()
.user_data()
.get::<RefCell<PointerUserData>>()
{
let mut data = data.borrow_mut();
let mut inner = inner.borrow_mut();
data.event(event, &mut inner, &buttons.borrow(), &pointer, ddata);
}
},
);
pointer
.as_ref()
.user_data()
.set(|| RefCell::new(PointerUserData::new(seat.detach())));
self.pointers.push(pointer);
}
fn remove_seat(&mut self, seat: &wl_seat::WlSeat) {
self.pointers.retain(|pointer| {
pointer
.as_ref()
.user_data()
.get::<RefCell<PointerUserData>>()
.map(|user_data| {
let guard = user_data.borrow_mut();
if &guard.seat == seat {
pointer.release();
false
} else {
true
}
})
.unwrap_or(false)
});
}
fn set_states(&mut self, states: &[State]) -> bool {
let mut inner = self.inner.borrow_mut();
let mut need_redraw = false;
// Process active.
let new_active = if states.contains(&State::Activated) {
WindowState::Active
} else {
WindowState::Inactive
};
need_redraw |= new_active != self.active;
self.active = new_active;
// Process maximized.
let new_maximized = states.contains(&State::Maximized);
need_redraw |= new_maximized != inner.maximized;
inner.maximized = new_maximized;
// Process fullscreened.
let new_fullscreened = states.contains(&State::Fullscreen);
need_redraw |= new_fullscreened != inner.fullscreened;
inner.fullscreened = new_fullscreened;
let new_tiled = states.contains(&State::TiledLeft)
|| states.contains(&State::TiledRight)
|| states.contains(&State::TiledTop)
|| states.contains(&State::TiledBottom);
need_redraw |= new_tiled != inner.tiled;
inner.tiled = new_tiled;
need_redraw
}
fn set_hidden(&mut self, hidden: bool) {
self.hidden = hidden;
let mut inner = self.inner.borrow_mut();
if !self.hidden {
inner.parts.add_decorations(
&self.base_surface,
&self.compositor,
&self.subcompositor,
self.inner.clone(),
);
} else {
inner.parts.remove_decorations();
}
}
fn set_resizable(&mut self, resizable: bool) {
self.inner.borrow_mut().resizable = resizable;
}
fn resize(&mut self, newsize: (u32, u32)) {
self.inner.borrow_mut().size = newsize;
self.buttons
.borrow_mut()
.arrange(newsize.0 + BORDER_SIZE * 2);
}
fn redraw(&mut self) {
self.redraw_inner();
}
fn subtract_borders(&self, width: i32, height: i32) -> (i32, i32) {
if self.hidden || self.inner.borrow().fullscreened {
(width, height)
} else {
(width, height - HEADER_SIZE as i32)
}
}
fn add_borders(&self, width: i32, height: i32) -> (i32, i32) {
if self.hidden || self.inner.borrow().fullscreened {
(width, height)
} else {
(width, height + HEADER_SIZE as i32)
}
}
fn location(&self) -> (i32, i32) {
if self.hidden || self.inner.borrow().fullscreened {
(0, 0)
} else {
(0, -(HEADER_SIZE as i32))
}
}
fn set_config(&mut self, config: FrameConfig) {
self.colors = config.theme;
}
fn set_title(&mut self, title: String) {
if let Some(title_text) = self.title_text.as_mut() {
title_text.update_title(&title);
}
self.title = Some(title);
}
}
impl AdwaitaFrame {
fn redraw_inner(&mut self) -> SkiaResult {
let inner = self.inner.borrow_mut();
// Don't draw borders if the frame explicitly hidden or fullscreened.
if self.hidden || inner.fullscreened {
inner.parts.hide_decorations();
return Some(());
}
// `parts` can't be empty here, since the initial state for `self.hidden` is true, and
// they will be created once `self.hidden` will become `false`.
let parts = &inner.parts;
let (width, height) = inner.size;
if let Some(decoration) = parts.decoration() {
// Use header scale for all the thing.
let header_scale = decoration.header.scale();
self.buttons.borrow_mut().update_scale(header_scale);
let left_scale = decoration.left.scale();
let right_scale = decoration.right.scale();
let bottom_scale = decoration.bottom.scale();
let (header_width, header_height) = self.buttons.borrow().scaled_size();
let header_height = header_height + BORDER_SIZE * header_scale;
{
// Create the buffers and draw
let colors = if self.active == WindowState::Active {
&self.colors.active
} else {
&self.colors.inactive
};
if let Some(title_text) = self.title_text.as_mut() {
title_text.update_color(colors.font_color);
}
let border_paint = colors.border_paint();
// -> head-subsurface
if let Ok((canvas, buffer)) = self.pool.buffer(
header_width as i32,
header_height as i32,
4 * header_width as i32,
wl_shm::Format::Argb8888,
) {
let mut pixmap = PixmapMut::from_bytes(canvas, header_width, header_height)?;
pixmap.fill(Color::TRANSPARENT);
if let Some(title_text) = self.title_text.as_mut() {
title_text.update_scale(header_scale);
}
draw_headerbar(
&mut pixmap,
self.title_text.as_ref().map(|t| t.pixmap()).unwrap_or(None),
header_scale as f32,
inner.resizable,
inner.maximized,
inner.tiled,
self.active,
&self.colors,
&self.buttons.borrow(),
&self
.pointers
.iter()
.flat_map(|p| {
if p.as_ref().is_alive() {
let data: &RefCell<PointerUserData> =
p.as_ref().user_data().get()?;
Some(data.borrow().location)
} else {
None
}
})
.collect::<Vec<Location>>(),
);
decoration.header.subsurface.set_position(
-(BORDER_SIZE as i32),
-(HEADER_SIZE as i32 + BORDER_SIZE as i32),
);
decoration.header.surface.attach(Some(&buffer), 0, 0);
if self.surface_version >= 4 {
decoration.header.surface.damage_buffer(
0,
0,
header_width as i32,
header_height as i32,
);
} else {
// surface is old and does not support damage_buffer, so we damage
// in surface coordinates and hope it is not rescaled
decoration
.header
.surface
.damage(0, 0, width as i32, HEADER_SIZE as i32);
}
decoration.header.surface.commit();
}
if inner.maximized {
// Don't draw the borders.
decoration.hide_borders();
return Some(());
}
let w = ((width + 2 * BORDER_SIZE) * bottom_scale) as i32;
let h = (BORDER_SIZE * bottom_scale) as i32;
// -> bottom-subsurface
if let Ok((canvas, buffer)) = self.pool.buffer(
w,
h,
(4 * bottom_scale * (width + 2 * BORDER_SIZE)) as i32,
wl_shm::Format::Argb8888,
) {
let mut pixmap = PixmapMut::from_bytes(canvas, w as u32, h as u32)?;
pixmap.fill(Color::TRANSPARENT);
let size = 1.0;
let x = BORDER_SIZE as f32 * bottom_scale as f32 - 1.0;
pixmap.fill_rect(
Rect::from_xywh(
x,
0.0,
w as f32 - BORDER_SIZE as f32 * 2.0 * bottom_scale as f32 + 2.0,
size,
)?,
&border_paint,
Transform::identity(),
None,
);
decoration
.bottom
.subsurface
.set_position(-(BORDER_SIZE as i32), height as i32);
decoration.bottom.surface.attach(Some(&buffer), 0, 0);
if self.surface_version >= 4 {
decoration.bottom.surface.damage_buffer(
0,
0,
((width + 2 * BORDER_SIZE) * bottom_scale) as i32,
(BORDER_SIZE * bottom_scale) as i32,
);
} else {
// surface is old and does not support damage_buffer, so we damage
// in surface coordinates and hope it is not rescaled
decoration.bottom.surface.damage(
0,
0,
(width + 2 * BORDER_SIZE) as i32,
BORDER_SIZE as i32,
);
}
decoration.bottom.surface.commit();
}
let w = (BORDER_SIZE * left_scale) as i32;
let h = (height * left_scale) as i32;
// -> left-subsurface
if let Ok((canvas, buffer)) = self.pool.buffer(
w,
h,
4 * (BORDER_SIZE * left_scale) as i32,
wl_shm::Format::Argb8888,
) {
let mut bg = Paint::default();
bg.set_color_rgba8(255, 0, 0, 255);
let mut pixmap = PixmapMut::from_bytes(canvas, w as u32, h as u32)?;
pixmap.fill(Color::TRANSPARENT);
let size = 1.0;
pixmap.fill_rect(
Rect::from_xywh(w as f32 - size, 0.0, w as f32, h as f32)?,
&border_paint,
Transform::identity(),
None,
);
decoration
.left
.subsurface
.set_position(-(BORDER_SIZE as i32), 0);
decoration.left.surface.attach(Some(&buffer), 0, 0);
if self.surface_version >= 4 {
decoration.left.surface.damage_buffer(0, 0, w, h);
} else {
// surface is old and does not support damage_buffer, so we damage
// in surface coordinates and hope it is not rescaled
decoration.left.surface.damage(
0,
0,
BORDER_SIZE as i32,
(height + HEADER_SIZE) as i32,
);
}
decoration.left.surface.commit();
}
let w = (BORDER_SIZE * right_scale) as i32;
let h = (height * right_scale) as i32;
// -> right-subsurface
if let Ok((canvas, buffer)) = self.pool.buffer(
w,
h,
4 * (BORDER_SIZE * right_scale) as i32,
wl_shm::Format::Argb8888,
) {
let mut bg = Paint::default();
bg.set_color_rgba8(255, 0, 0, 255);
let mut pixmap = PixmapMut::from_bytes(canvas, w as u32, h as u32)?;
pixmap.fill(Color::TRANSPARENT);
let size = 1.0;
pixmap.fill_rect(
Rect::from_xywh(0.0, 0.0, size, h as f32)?,
&border_paint,
Transform::identity(),
None,
);
decoration.right.subsurface.set_position(width as i32, 0);
decoration.right.surface.attach(Some(&buffer), 0, 0);
if self.surface_version >= 4 {
decoration.right.surface.damage_buffer(0, 0, w, h);
} else {
// surface is old and does not support damage_buffer, so we damage
// in surface coordinates and hope it is not rescaled
decoration
.right
.surface
.damage(0, 0, BORDER_SIZE as i32, height as i32);
}
decoration.right.surface.commit();
}
}
}
Some(())
}
}
impl Drop for AdwaitaFrame {
fn drop(&mut self) {
for ptr in self.pointers.drain(..) {
if ptr.as_ref().version() >= 3 {
ptr.release();
}
}
}
}
fn draw_headerbar(
pixmap: &mut PixmapMut,
text_pixmap: Option<&Pixmap>,
scale: f32,
maximizable: bool,
is_maximized: bool,
tiled: bool,
state: WindowState,
colors: &ColorTheme,
buttons: &Buttons,
mouses: &[Location],
) {
let border_size = BORDER_SIZE as f32 * scale;
let margin_h = border_size;
let margin_v = border_size;
let colors = colors.for_state(state);
draw_headerbar_bg(
pixmap,
scale,
margin_h,
margin_v,
colors,
is_maximized,
tiled,
);
if let Some(text_pixmap) = text_pixmap {
let canvas_w = pixmap.width() as f32;
let canvas_h = pixmap.height() as f32;
let header_w = canvas_w - margin_h * 2.0;
let header_h = canvas_h - margin_v;
let text_w = text_pixmap.width() as f32;
let text_h = text_pixmap.height() as f32;
let x = header_w / 2.0 - text_w / 2.0;
let y = header_h / 2.0 - text_h / 2.0;
let x = margin_h + x;
let y = margin_v + y;
let (x, y) = if x + text_w < buttons.minimize.x() - 10.0 {
(x, y)
} else {
let y = header_h / 2.0 - text_h / 2.0;
let x = buttons.minimize.x() - text_w - 10.0;
let y = margin_v + y;
(x, y)
};
let x = x.max(margin_h + 5.0);
if let Some(clip) = Rect::from_xywh(0.0, 0.0, buttons.minimize.x() - 10.0, canvas_h) {
let mut mask = ClipMask::new();
mask.set_path(
canvas_w as u32,
canvas_h as u32,
&PathBuilder::from_rect(clip),
FillRule::Winding,
false,
);
pixmap.draw_pixmap(
x as i32,
y as i32,
text_pixmap.as_ref(),
&PixmapPaint::default(),
Transform::identity(),
Some(&mask),
);
}
}
if buttons.close.x() > margin_h {
buttons.close.draw_close(scale, colors, mouses, pixmap);
}
if buttons.maximize.x() > margin_h {
buttons
.maximize
.draw_maximize(scale, colors, mouses, maximizable, is_maximized, pixmap);
}
if buttons.minimize.x() > margin_h {
buttons
.minimize
.draw_minimize(scale, colors, mouses, pixmap);
}
}
fn draw_headerbar_bg(
pixmap: &mut PixmapMut,
scale: f32,
margin_h: f32,
margin_v: f32,
colors: &ColorMap,
is_maximized: bool,
tiled: bool,
) -> SkiaResult {
let w = pixmap.width() as f32;
let h = pixmap.height() as f32;
let radius = if is_maximized || tiled {
0.0
} else {
10.0 * scale
};
let margin_h = margin_h - 1.0;
let w = w - margin_h * 2.0;
let bg = rounded_headerbar_shape(margin_h, margin_v, w, h, radius)?;
pixmap.fill_path(
&bg,
&colors.headerbar_paint(),
FillRule::Winding,
Transform::identity(),
None,
);
pixmap.fill_rect(
Rect::from_xywh(margin_h, h - 1.0, w, h)?,
&colors.border_paint(),
Transform::identity(),
None,
);
Some(())
}
fn rounded_headerbar_shape(x: f32, y: f32, width: f32, height: f32, radius: f32) -> Option<Path> {
use std::f32::consts::FRAC_1_SQRT_2;
let mut pb = PathBuilder::new();
let mut cursor = Point::from_xy(x, y);
// !!!
// This code is heavily "inspired" by https://gitlab.com/snakedye/snui/
// So technically it should be licensed under MPL-2.0, sorry about that 🥺 👉👈
// !!!
// Positioning the cursor
cursor.y += radius;
pb.move_to(cursor.x, cursor.y);
// Drawing the outline
pb.cubic_to(
cursor.x,
cursor.y,
cursor.x,
cursor.y - FRAC_1_SQRT_2 * radius,
{
cursor.x += radius;
cursor.x
},
{
cursor.y -= radius;
cursor.y
},
);
pb.line_to(
{
cursor.x = x + width - radius;
cursor.x
},
cursor.y,
);
pb.cubic_to(
cursor.x,
cursor.y,
cursor.x + FRAC_1_SQRT_2 * radius,
cursor.y,
{
cursor.x += radius;
cursor.x
},
{
cursor.y += radius;
cursor.y
},
);
pb.line_to(cursor.x, {
cursor.y = y + height;
cursor.y
});
pb.line_to(
{
cursor.x = x;
cursor.x
},
cursor.y,
);
pb.close();
pb.finish()
}

View file

@ -0,0 +1,198 @@
use std::{cell::RefCell, rc::Rc};
use smithay_client_toolkit::{
reexports::client::{
protocol::{
wl_compositor::WlCompositor, wl_subcompositor::WlSubcompositor,
wl_subsurface::WlSubsurface, wl_surface::WlSurface,
},
Attached, DispatchData,
},
window::FrameRequest,
};
use crate::{surface, Inner, Location};
pub enum DecorationPartKind {
Header,
Top,
Left,
Right,
Bottom,
None,
}
#[derive(Debug)]
pub struct Decoration {
pub header: Part,
pub top: Part,
pub left: Part,
pub right: Part,
pub bottom: Part,
}
impl Decoration {
pub fn iter(&self) -> [&Part; 5] {
[
&self.header,
&self.top,
&self.left,
&self.right,
&self.bottom,
]
}
pub fn hide_decoration(&self) {
for p in self.iter() {
p.surface.attach(None, 0, 0);
p.surface.commit();
}
}
pub fn hide_borders(&self) {
for p in self.iter().iter().skip(1) {
p.surface.attach(None, 0, 0);
p.surface.commit();
}
}
}
#[derive(Default, Debug)]
pub(crate) struct Parts {
decoration: Option<Decoration>,
}
impl Parts {
pub fn add_decorations(
&mut self,
parent: &WlSurface,
compositor: &Attached<WlCompositor>,
subcompositor: &Attached<WlSubcompositor>,
inner: Rc<RefCell<Inner>>,
) {
if self.decoration.is_none() {
let header = Part::new(parent, compositor, subcompositor, Some(inner));
let top = Part::new(parent, compositor, subcompositor, None);
let left = Part::new(parent, compositor, subcompositor, None);
let right = Part::new(parent, compositor, subcompositor, None);
let bottom = Part::new(parent, compositor, subcompositor, None);
self.decoration = Some(Decoration {
header,
top,
left,
right,
bottom,
});
}
}
pub fn remove_decorations(&mut self) {
self.decoration = None;
}
pub fn hide_decorations(&self) {
if let Some(decor) = self.decoration.as_ref() {
decor.hide_decoration();
}
}
pub fn decoration(&self) -> Option<&Decoration> {
self.decoration.as_ref()
}
pub fn find_decoration_part(&self, surface: &WlSurface) -> DecorationPartKind {
if let Some(decor) = self.decoration() {
if surface.as_ref().equals(decor.header.surface.as_ref()) {
DecorationPartKind::Header
} else if surface.as_ref().equals(decor.top.surface.as_ref()) {
DecorationPartKind::Top
} else if surface.as_ref().equals(decor.bottom.surface.as_ref()) {
DecorationPartKind::Bottom
} else if surface.as_ref().equals(decor.left.surface.as_ref()) {
DecorationPartKind::Left
} else if surface.as_ref().equals(decor.right.surface.as_ref()) {
DecorationPartKind::Right
} else {
DecorationPartKind::None
}
} else {
DecorationPartKind::None
}
}
pub fn find_surface(&self, surface: &WlSurface) -> Location {
if let Some(decor) = self.decoration() {
if surface.as_ref().equals(decor.header.surface.as_ref()) {
Location::Head
} else if surface.as_ref().equals(decor.top.surface.as_ref()) {
Location::Top
} else if surface.as_ref().equals(decor.bottom.surface.as_ref()) {
Location::Bottom
} else if surface.as_ref().equals(decor.left.surface.as_ref()) {
Location::Left
} else if surface.as_ref().equals(decor.right.surface.as_ref()) {
Location::Right
} else {
Location::None
}
} else {
Location::None
}
}
}
#[derive(Debug)]
pub struct Part {
pub surface: WlSurface,
pub subsurface: WlSubsurface,
}
impl Part {
fn new(
parent: &WlSurface,
compositor: &Attached<WlCompositor>,
subcompositor: &Attached<WlSubcompositor>,
inner: Option<Rc<RefCell<Inner>>>,
) -> Part {
let surface = if let Some(inner) = inner {
surface::setup_surface(
compositor.create_surface(),
Some(move |dpi, surface: WlSurface, ddata: DispatchData| {
surface.set_buffer_scale(dpi);
surface.commit();
(inner.borrow_mut().implem)(FrameRequest::Refresh, 0, ddata);
}),
)
} else {
surface::setup_surface(
compositor.create_surface(),
Some(move |dpi, surface: WlSurface, _ddata: DispatchData| {
surface.set_buffer_scale(dpi);
surface.commit();
}),
)
};
let surface = surface.detach();
let subsurface = subcompositor.get_subsurface(&surface, parent);
Part {
surface,
subsurface: subsurface.detach(),
}
}
pub fn scale(&self) -> u32 {
surface::get_surface_scale_factor(&self.surface) as u32
}
}
impl Drop for Part {
fn drop(&mut self) {
self.subsurface.destroy();
self.surface.destroy();
}
}

View file

@ -0,0 +1,260 @@
use log::error;
use smithay_client_toolkit::{
reexports::{
client::{
protocol::{wl_pointer, wl_seat::WlSeat},
DispatchData,
},
protocols::xdg_shell::client::xdg_toplevel::ResizeEdge,
},
seat::pointer::ThemedPointer,
window::FrameRequest,
};
use crate::{
buttons::{ButtonKind, Buttons},
parts::DecorationPartKind,
precise_location,
theme::{BORDER_SIZE, HEADER_SIZE},
Inner, Location,
};
pub(crate) struct PointerUserData {
pub location: Location,
current_surface: DecorationPartKind,
position: (f64, f64),
pub seat: WlSeat,
last_click: Option<std::time::Instant>,
lpm_grab: Option<ButtonKind>,
}
impl PointerUserData {
pub fn new(seat: WlSeat) -> Self {
Self {
location: Location::None,
current_surface: DecorationPartKind::None,
position: (0.0, 0.0),
seat,
last_click: None,
lpm_grab: None,
}
}
pub fn event(
&mut self,
event: wl_pointer::Event,
inner: &mut Inner,
buttons: &Buttons,
pointer: &ThemedPointer,
ddata: DispatchData<'_>,
) {
use wl_pointer::Event;
match event {
Event::Enter {
serial,
surface,
surface_x,
surface_y,
} => {
self.location = precise_location(
buttons,
inner.parts.find_surface(&surface),
inner.size.0,
surface_x,
surface_y,
);
self.current_surface = inner.parts.find_decoration_part(&surface);
self.position = (surface_x, surface_y);
change_pointer(pointer, inner, self.location, Some(serial))
}
Event::Leave { serial, .. } => {
self.current_surface = DecorationPartKind::None;
self.location = Location::None;
change_pointer(pointer, inner, self.location, Some(serial));
(inner.implem)(FrameRequest::Refresh, 0, ddata);
}
Event::Motion {
surface_x,
surface_y,
..
} => {
self.position = (surface_x, surface_y);
let newpos =
precise_location(buttons, self.location, inner.size.0, surface_x, surface_y);
if newpos != self.location {
match (newpos, self.location) {
(Location::Button(_), _) | (_, Location::Button(_)) => {
// pointer movement involves a button, request refresh
(inner.implem)(FrameRequest::Refresh, 0, ddata);
}
_ => (),
}
// we changed of part of the decoration, pointer image
// may need to be changed
self.location = newpos;
change_pointer(pointer, inner, self.location, None)
}
}
Event::Button {
serial,
button,
state,
..
} => {
let request = if state == wl_pointer::ButtonState::Pressed {
match button {
// Left mouse button.
0x110 => lmb_press(self, inner.maximized, inner.resizable),
// Right mouse button.
0x111 => rmb_press(self),
_ => None,
}
} else {
// Left mouse button.
if button == 0x110 {
lmb_release(self, inner.maximized)
} else {
None
}
};
if let Some(request) = request {
(inner.implem)(request, serial, ddata);
}
}
_ => {}
}
}
}
fn lmb_press(
pointer_data: &mut PointerUserData,
maximized: bool,
resizable: bool,
) -> Option<FrameRequest> {
match pointer_data.location {
Location::Top if resizable => Some(FrameRequest::Resize(
pointer_data.seat.clone(),
ResizeEdge::Top,
)),
Location::TopLeft if resizable => Some(FrameRequest::Resize(
pointer_data.seat.clone(),
ResizeEdge::TopLeft,
)),
Location::Left if resizable => Some(FrameRequest::Resize(
pointer_data.seat.clone(),
ResizeEdge::Left,
)),
Location::BottomLeft if resizable => Some(FrameRequest::Resize(
pointer_data.seat.clone(),
ResizeEdge::BottomLeft,
)),
Location::Bottom if resizable => Some(FrameRequest::Resize(
pointer_data.seat.clone(),
ResizeEdge::Bottom,
)),
Location::BottomRight if resizable => Some(FrameRequest::Resize(
pointer_data.seat.clone(),
ResizeEdge::BottomRight,
)),
Location::Right if resizable => Some(FrameRequest::Resize(
pointer_data.seat.clone(),
ResizeEdge::Right,
)),
Location::TopRight if resizable => Some(FrameRequest::Resize(
pointer_data.seat.clone(),
ResizeEdge::TopRight,
)),
Location::Head => {
let last_click = pointer_data.last_click.replace(std::time::Instant::now());
if let Some(last) = last_click {
if last.elapsed() < std::time::Duration::from_millis(400) {
pointer_data.last_click = None;
if maximized {
Some(FrameRequest::UnMaximize)
} else {
Some(FrameRequest::Maximize)
}
} else {
Some(FrameRequest::Move(pointer_data.seat.clone()))
}
} else {
Some(FrameRequest::Move(pointer_data.seat.clone()))
}
}
Location::Button(btn) => {
pointer_data.lpm_grab = Some(btn);
None
}
_ => None,
}
}
fn lmb_release(pointer_data: &mut PointerUserData, maximized: bool) -> Option<FrameRequest> {
let lpm_grab = pointer_data.lpm_grab.take();
match pointer_data.location {
Location::Button(btn) => {
if lpm_grab == Some(btn) {
let req = match btn {
ButtonKind::Close => FrameRequest::Close,
ButtonKind::Maximize => {
if maximized {
FrameRequest::UnMaximize
} else {
FrameRequest::Maximize
}
}
ButtonKind::Minimize => FrameRequest::Minimize,
};
Some(req)
} else {
None
}
}
_ => None,
}
}
fn rmb_press(pointer_data: &PointerUserData) -> Option<FrameRequest> {
match pointer_data.location {
Location::Head | Location::Button(_) => Some(FrameRequest::ShowMenu(
pointer_data.seat.clone(),
pointer_data.position.0 as i32 - BORDER_SIZE as i32,
// We must offset it by header size for precise position.
pointer_data.position.1 as i32 - (HEADER_SIZE as i32 + BORDER_SIZE as i32),
)),
_ => None,
}
}
fn change_pointer(pointer: &ThemedPointer, inner: &Inner, location: Location, serial: Option<u32>) {
// Prevent theming of the surface if it was requested.
if !inner.theme_over_surface && location == Location::None {
return;
}
let name = match location {
// If we can't resize a frame we shouldn't show resize cursors.
_ if !inner.resizable => "left_ptr",
Location::Top => "top_side",
Location::TopRight => "top_right_corner",
Location::Right => "right_side",
Location::BottomRight => "bottom_right_corner",
Location::Bottom => "bottom_side",
Location::BottomLeft => "bottom_left_corner",
Location::Left => "left_side",
Location::TopLeft => "top_left_corner",
_ => "left_ptr",
};
if pointer.set_cursor(name, serial).is_err() {
error!("Failed to set cursor");
}
}

View file

@ -0,0 +1,154 @@
use std::{cell::RefCell, rc::Rc, sync::Mutex};
use super::client;
use smithay_client_toolkit as sctk;
use client::{
protocol::{wl_output, wl_surface},
Attached, DispatchData, Main,
};
use sctk::output::{add_output_listener, with_output_info, OutputListener};
pub(crate) struct SurfaceUserData {
scale_factor: i32,
outputs: Vec<(wl_output::WlOutput, i32, OutputListener)>,
}
impl SurfaceUserData {
fn new() -> Self {
SurfaceUserData {
scale_factor: 1,
outputs: Vec::new(),
}
}
pub(crate) fn enter<F>(
&mut self,
output: wl_output::WlOutput,
surface: wl_surface::WlSurface,
callback: &Option<Rc<RefCell<F>>>,
) where
F: FnMut(i32, wl_surface::WlSurface, DispatchData) + 'static,
{
let output_scale = with_output_info(&output, |info| info.scale_factor).unwrap_or(1);
let my_surface = surface.clone();
// Use a UserData to safely share the callback with the other thread
let my_callback = client::UserData::new();
if let Some(ref cb) = callback {
my_callback.set(|| cb.clone());
}
let listener = add_output_listener(&output, move |output, info, ddata| {
let mut user_data = my_surface
.as_ref()
.user_data()
.get::<Mutex<SurfaceUserData>>()
.unwrap()
.lock()
.unwrap();
// update the scale factor of the relevant output
for (ref o, ref mut factor, _) in user_data.outputs.iter_mut() {
if o.as_ref().equals(output.as_ref()) {
if info.obsolete {
// an output that no longer exists is marked by a scale factor of -1
*factor = -1;
} else {
*factor = info.scale_factor;
}
break;
}
}
// recompute the scale factor with the new info
let callback = my_callback.get::<Rc<RefCell<F>>>().cloned();
let old_scale_factor = user_data.scale_factor;
let new_scale_factor = user_data.recompute_scale_factor();
drop(user_data);
if let Some(ref cb) = callback {
if old_scale_factor != new_scale_factor {
(*cb.borrow_mut())(new_scale_factor, surface.clone(), ddata);
}
}
});
self.outputs.push((output, output_scale, listener));
}
pub(crate) fn leave(&mut self, output: &wl_output::WlOutput) {
self.outputs
.retain(|(ref output2, _, _)| !output.as_ref().equals(output2.as_ref()));
}
fn recompute_scale_factor(&mut self) -> i32 {
let mut new_scale_factor = 1;
self.outputs.retain(|&(_, output_scale, _)| {
if output_scale > 0 {
new_scale_factor = ::std::cmp::max(new_scale_factor, output_scale);
true
} else {
// cleanup obsolete output
false
}
});
if self.outputs.is_empty() {
// don't update the scale factor if we are not displayed on any output
return self.scale_factor;
}
self.scale_factor = new_scale_factor;
new_scale_factor
}
}
pub fn setup_surface<F>(
surface: Main<wl_surface::WlSurface>,
callback: Option<F>,
) -> Attached<wl_surface::WlSurface>
where
F: FnMut(i32, wl_surface::WlSurface, DispatchData) + 'static,
{
let callback = callback.map(|c| Rc::new(RefCell::new(c)));
surface.quick_assign(move |surface, event, ddata| {
let mut user_data = surface
.as_ref()
.user_data()
.get::<Mutex<SurfaceUserData>>()
.unwrap()
.lock()
.unwrap();
match event {
wl_surface::Event::Enter { output } => {
// Passing the callback to be added to output listener
user_data.enter(output, surface.detach(), &callback);
}
wl_surface::Event::Leave { output } => {
user_data.leave(&output);
}
_ => unreachable!(),
};
let old_scale_factor = user_data.scale_factor;
let new_scale_factor = user_data.recompute_scale_factor();
drop(user_data);
if let Some(ref cb) = callback {
if old_scale_factor != new_scale_factor {
(*cb.borrow_mut())(new_scale_factor, surface.detach(), ddata);
}
}
});
surface
.as_ref()
.user_data()
.set_threadsafe(|| Mutex::new(SurfaceUserData::new()));
surface.into()
}
/// Returns the current suggested scale factor of a surface.
///
/// Panics if the surface was not created using `Environment::create_surface` or
/// `Environment::create_surface_with_dpi_callback`.
pub fn get_surface_scale_factor(surface: &wl_surface::WlSurface) -> i32 {
surface
.as_ref()
.user_data()
.get::<Mutex<SurfaceUserData>>()
.expect("SCTK: Surface was not created by SCTK.")
.lock()
.unwrap()
.scale_factor
}

View file

@ -0,0 +1,133 @@
use smithay_client_toolkit::window::WindowState;
pub use tiny_skia::Color;
use tiny_skia::{Paint, Shader};
pub(crate) const BORDER_SIZE: u32 = 10;
pub(crate) const HEADER_SIZE: u32 = 35;
#[derive(Debug, Clone)]
pub struct ColorMap {
pub headerbar: Color,
pub button_idle: Color,
pub button_hover: Color,
pub button_icon: Color,
pub border_color: Color,
pub font_color: Color,
}
impl ColorMap {
pub(crate) fn headerbar_paint(&self) -> Paint {
Paint {
shader: Shader::SolidColor(self.headerbar),
anti_alias: true,
..Default::default()
}
}
pub(crate) fn button_idle_paint(&self) -> Paint {
Paint {
shader: Shader::SolidColor(self.button_idle),
anti_alias: true,
..Default::default()
}
}
pub(crate) fn button_hover_paint(&self) -> Paint {
Paint {
shader: Shader::SolidColor(self.button_hover),
anti_alias: true,
..Default::default()
}
}
pub(crate) fn button_icon_paint(&self) -> Paint {
Paint {
shader: Shader::SolidColor(self.button_icon),
..Default::default()
}
}
pub(crate) fn border_paint(&self) -> Paint {
Paint {
shader: Shader::SolidColor(self.border_color),
..Default::default()
}
}
}
#[derive(Debug, Clone)]
pub struct ColorTheme {
pub active: ColorMap,
pub inactive: ColorMap,
}
impl Default for ColorTheme {
fn default() -> Self {
Self::light()
}
}
impl ColorTheme {
/// Automatically choose between light & dark themes based on:
/// * dbus org.freedesktop.portal.Settings
/// <https://flatpak.github.io/xdg-desktop-portal/#gdbus-interface-org-freedesktop-portal-Settings>
pub fn auto() -> Self {
match crate::config::prefer_dark() {
true => Self::dark(),
false => Self::light(),
}
}
pub fn light() -> Self {
Self {
active: ColorMap {
headerbar: Color::from_rgba8(235, 235, 235, 255),
button_idle: Color::from_rgba8(216, 216, 216, 255),
button_hover: Color::from_rgba8(207, 207, 207, 255),
button_icon: Color::from_rgba8(42, 42, 42, 255),
border_color: Color::from_rgba8(220, 220, 220, 255),
font_color: Color::from_rgba8(47, 47, 47, 255),
},
inactive: ColorMap {
headerbar: Color::from_rgba8(250, 250, 250, 255),
button_idle: Color::from_rgba8(240, 240, 240, 255),
button_hover: Color::from_rgba8(216, 216, 216, 255),
button_icon: Color::from_rgba8(148, 148, 148, 255),
border_color: Color::from_rgba8(220, 220, 220, 255),
font_color: Color::from_rgba8(150, 150, 150, 255),
},
}
}
pub fn dark() -> Self {
Self {
active: ColorMap {
headerbar: Color::from_rgba8(48, 48, 48, 255),
button_idle: Color::from_rgba8(69, 69, 69, 255),
button_hover: Color::from_rgba8(79, 79, 79, 255),
button_icon: Color::from_rgba8(255, 255, 255, 255),
border_color: Color::from_rgba8(58, 58, 58, 255),
font_color: Color::from_rgba8(255, 255, 255, 255),
},
inactive: ColorMap {
headerbar: Color::from_rgba8(36, 36, 36, 255),
button_idle: Color::from_rgba8(47, 47, 47, 255),
button_hover: Color::from_rgba8(57, 57, 57, 255),
button_icon: Color::from_rgba8(144, 144, 144, 255),
border_color: Color::from_rgba8(58, 58, 58, 255),
font_color: Color::from_rgba8(144, 144, 144, 255),
},
}
}
}
impl ColorTheme {
pub(crate) fn for_state(&self, state: WindowState) -> &ColorMap {
if state == WindowState::Active {
&self.active
} else {
&self.inactive
}
}
}

View file

@ -0,0 +1,61 @@
use tiny_skia::{Color, Pixmap};
#[cfg(any(feature = "crossfont", feature = "ab_glyph"))]
mod config;
#[cfg(any(feature = "crossfont", feature = "ab_glyph"))]
mod font_preference;
#[cfg(feature = "crossfont")]
mod crossfont_renderer;
#[cfg(all(not(feature = "crossfont"), feature = "ab_glyph"))]
mod ab_glyph_renderer;
#[cfg(all(not(feature = "crossfont"), not(feature = "ab_glyph")))]
mod dumb;
#[derive(Debug)]
pub struct TitleText {
#[cfg(feature = "crossfont")]
imp: crossfont_renderer::CrossfontTitleText,
#[cfg(all(not(feature = "crossfont"), feature = "ab_glyph"))]
imp: ab_glyph_renderer::AbGlyphTitleText,
#[cfg(all(not(feature = "crossfont"), not(feature = "ab_glyph")))]
imp: dumb::DumbTitleText,
}
impl TitleText {
pub fn new(color: Color) -> Option<Self> {
#[cfg(feature = "crossfont")]
return crossfont_renderer::CrossfontTitleText::new(color)
.ok()
.map(|imp| Self { imp });
#[cfg(all(not(feature = "crossfont"), feature = "ab_glyph"))]
return Some(Self {
imp: ab_glyph_renderer::AbGlyphTitleText::new(color),
});
#[cfg(all(not(feature = "crossfont"), not(feature = "ab_glyph")))]
{
let _ = color;
return None;
}
}
pub fn update_scale(&mut self, scale: u32) {
self.imp.update_scale(scale)
}
pub fn update_title<S: Into<String>>(&mut self, title: S) {
self.imp.update_title(title)
}
pub fn update_color(&mut self, color: Color) {
self.imp.update_color(color)
}
pub fn pixmap(&self) -> Option<&Pixmap> {
self.imp.pixmap()
}
}

Binary file not shown.

View file

@ -0,0 +1,177 @@
//! Title renderer using ab_glyph.
//!
//! Requires no dynamically linked dependencies.
//!
//! Can fallback to a embedded Cantarell-Regular.ttf font (SIL Open Font Licence v1.1)
//! if the system font doesn't work.
use crate::title::{config, font_preference::FontPreference};
use ab_glyph::{point, Font, FontRef, Glyph, PxScale, PxScaleFont, ScaleFont, VariableFont};
use std::{fs::File, process::Command};
use tiny_skia::{Color, Pixmap, PremultipliedColorU8};
const CANTARELL: &[u8] = include_bytes!("Cantarell-Regular.ttf");
#[derive(Debug)]
pub struct AbGlyphTitleText {
title: String,
font: Option<(memmap2::Mmap, FontPreference)>,
original_px_size: f32,
size: PxScale,
color: Color,
pixmap: Option<Pixmap>,
}
impl AbGlyphTitleText {
pub fn new(color: Color) -> Self {
let font_pref = config::titlebar_font().unwrap_or_default();
let font_pref_pt_size = font_pref.pt_size;
let font = font_file_matching(&font_pref)
.and_then(|f| mmap(&f))
.map(|mmap| (mmap, font_pref));
let size = parse_font(&font)
.pt_to_px_scale(font_pref_pt_size)
.expect("invalid font units_per_em");
Self {
title: <_>::default(),
font,
original_px_size: size.x,
size,
color,
pixmap: None,
}
}
pub fn update_scale(&mut self, scale: u32) {
let new_scale = PxScale::from(self.original_px_size * scale as f32);
if (self.size.x - new_scale.x).abs() > f32::EPSILON {
self.size = new_scale;
self.pixmap = self.render();
}
}
pub fn update_title(&mut self, title: impl Into<String>) {
let new_title = title.into();
if new_title != self.title {
self.title = new_title;
self.pixmap = self.render();
}
}
pub fn update_color(&mut self, color: Color) {
if color != self.color {
self.color = color;
self.pixmap = self.render();
}
}
pub fn pixmap(&self) -> Option<&Pixmap> {
self.pixmap.as_ref()
}
/// Render returning the new `Pixmap`.
fn render(&self) -> Option<Pixmap> {
let font = parse_font(&self.font);
let font = font.as_scaled(self.size);
let glyphs = self.layout(&font);
let last_glyph = glyphs.last()?;
let width = (last_glyph.position.x + font.h_advance(last_glyph.id)).ceil() as u32;
let height = font.height().ceil() as u32;
let mut pixmap = Pixmap::new(width, height)?;
let pixels = pixmap.pixels_mut();
for glyph in glyphs {
if let Some(outline) = font.outline_glyph(glyph) {
let bounds = outline.px_bounds();
let left = bounds.min.x as u32;
let top = bounds.min.y as u32;
outline.draw(|x, y, c| {
let p_idx = (top + y) * width + (left + x);
let old_alpha_u8 = pixels[p_idx as usize].alpha();
let new_alpha = c + (old_alpha_u8 as f32 / 255.0);
if let Some(px) = PremultipliedColorU8::from_rgba(
(self.color.red() * new_alpha * 255.0) as _,
(self.color.green() * new_alpha * 255.0) as _,
(self.color.blue() * new_alpha * 255.0) as _,
(new_alpha * 255.0) as _,
) {
pixels[p_idx as usize] = px;
}
})
}
}
Some(pixmap)
}
/// Simple single-line glyph layout.
fn layout(&self, font: &PxScaleFont<impl Font>) -> Vec<Glyph> {
let mut caret = point(0.0, font.ascent());
let mut last_glyph: Option<Glyph> = None;
let mut target = Vec::new();
for c in self.title.chars() {
if c.is_control() {
continue;
}
let mut glyph = font.scaled_glyph(c);
if let Some(previous) = last_glyph.take() {
caret.x += font.kern(previous.id, glyph.id);
}
glyph.position = caret;
last_glyph = Some(glyph.clone());
caret.x += font.h_advance(glyph.id);
target.push(glyph);
}
target
}
}
/// Parse the memmapped system font or fallback to built-in cantarell.
fn parse_font(sys_font: &Option<(memmap2::Mmap, FontPreference)>) -> FontRef<'_> {
match sys_font {
Some((mmap, font_pref)) => {
FontRef::try_from_slice(mmap)
.map(|mut f| {
// basic "bold" handling for variable fonts
if font_pref
.style
.as_deref()
.map_or(false, |s| s.eq_ignore_ascii_case("bold"))
{
f.set_variation(b"wght", 700.0);
}
f
})
.unwrap_or_else(|_| FontRef::try_from_slice(CANTARELL).unwrap())
}
_ => FontRef::try_from_slice(CANTARELL).unwrap(),
}
}
/// Font-config without dynamically linked dependencies
fn font_file_matching(pref: &FontPreference) -> Option<File> {
let mut pattern = pref.name.clone();
if let Some(style) = &pref.style {
pattern.push(':');
pattern.push_str(style);
}
Command::new("fc-match")
.arg("-f")
.arg("%{file}")
.arg(&pattern)
.output()
.ok()
.and_then(|out| String::from_utf8(out.stdout).ok())
.and_then(|path| File::open(path.trim()).ok())
}
fn mmap(file: &File) -> Option<memmap2::Mmap> {
// Safety: System font files are not expected to be mutated during use
unsafe { memmap2::Mmap::map(file).ok() }
}

View file

@ -0,0 +1,20 @@
//! System font configuration.
use crate::title::font_preference::FontPreference;
use std::process::Command;
/// Query system for which font to use for window titles.
pub(crate) fn titlebar_font() -> Option<FontPreference> {
// outputs something like: `'Cantarell Bold 12'`
let stdout = Command::new("gsettings")
.args(["get", "org.gnome.desktop.wm.preferences", "titlebar-font"])
.output()
.ok()
.and_then(|out| String::from_utf8(out.stdout).ok())?;
FontPreference::from_name_style_size(
stdout
.trim()
.trim_end_matches('\'')
.trim_start_matches('\''),
)
}

View file

@ -0,0 +1,227 @@
use crate::title::config;
use crossfont::{GlyphKey, Rasterize, RasterizedGlyph};
use tiny_skia::{Color, Pixmap, PixmapPaint, PixmapRef, Transform};
pub struct CrossfontTitleText {
title: String,
font_desc: crossfont::FontDesc,
font_key: crossfont::FontKey,
size: crossfont::Size,
scale: u32,
metrics: crossfont::Metrics,
rasterizer: crossfont::Rasterizer,
color: Color,
pixmap: Option<Pixmap>,
}
impl std::fmt::Debug for CrossfontTitleText {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TitleText")
.field("title", &self.title)
.field("font_desc", &self.font_desc)
.field("font_key", &self.font_key)
.field("size", &self.size)
.field("scale", &self.scale)
.field("pixmap", &self.pixmap)
.finish()
}
}
impl CrossfontTitleText {
pub fn new(color: Color) -> Result<Self, crossfont::Error> {
let title = "".into();
let scale = 1;
let font_pref = config::titlebar_font().unwrap_or_default();
let font_style = font_pref
.style
.map(crossfont::Style::Specific)
.unwrap_or_else(|| crossfont::Style::Description {
slant: crossfont::Slant::Normal,
weight: crossfont::Weight::Normal,
});
let font_desc = crossfont::FontDesc::new(&font_pref.name, font_style);
let mut rasterizer = crossfont::Rasterizer::new(scale as f32)?;
let size = crossfont::Size::new(font_pref.pt_size);
let font_key = rasterizer.load_font(&font_desc, size)?;
// Need to load at least one glyph for the face before calling metrics.
// The glyph requested here ('m' at the time of writing) has no special
// meaning.
rasterizer.get_glyph(GlyphKey {
font_key,
character: 'm',
size,
})?;
let metrics = rasterizer.metrics(font_key, size)?;
let mut this = Self {
title,
font_desc,
font_key,
size,
scale,
metrics,
rasterizer,
color,
pixmap: None,
};
this.rerender();
Ok(this)
}
fn update_metrics(&mut self) -> Result<(), crossfont::Error> {
self.rasterizer.get_glyph(GlyphKey {
font_key: self.font_key,
character: 'm',
size: self.size,
})?;
self.metrics = self.rasterizer.metrics(self.font_key, self.size)?;
Ok(())
}
pub fn update_scale(&mut self, scale: u32) {
if self.scale != scale {
self.rasterizer.update_dpr(scale as f32);
self.scale = scale;
self.update_metrics().ok();
self.rerender();
}
}
pub fn update_title<S: Into<String>>(&mut self, title: S) {
let title = title.into();
if self.title != title {
self.title = title;
self.rerender();
}
}
pub fn update_color(&mut self, color: Color) {
if self.color != color {
self.color = color;
self.rerender();
}
}
fn rerender(&mut self) {
let glyphs: Vec<_> = self
.title
.chars()
.filter_map(|character| {
let key = GlyphKey {
character,
font_key: self.font_key,
size: self.size,
};
self.rasterizer
.get_glyph(key)
.map(|glyph| (key, glyph))
.ok()
})
.collect();
if glyphs.is_empty() {
self.pixmap = None;
return;
}
let width = self.calc_width(&glyphs);
let height = self.metrics.line_height.round() as i32;
let mut pixmap = if let Some(p) = Pixmap::new(width as u32, height as u32) {
p
} else {
self.pixmap = None;
return;
};
// pixmap.fill(Color::from_rgba8(255, 0, 0, 55));
let mut caret = 0;
let mut last_glyph = None;
for (key, glyph) in glyphs {
let mut buffer = Vec::with_capacity(glyph.width as usize * 4);
let glyph_buffer = match &glyph.buffer {
crossfont::BitmapBuffer::Rgb(v) => v.chunks(3),
crossfont::BitmapBuffer::Rgba(v) => v.chunks(4),
};
for px in glyph_buffer {
let alpha = if let Some(alpha) = px.get(3) {
*alpha as f32 / 255.0
} else {
let r = px[0] as f32 / 255.0;
let g = px[1] as f32 / 255.0;
let b = px[2] as f32 / 255.0;
(r + g + b) / 3.0
};
let mut color = self.color;
color.set_alpha(alpha);
let color = color.premultiply().to_color_u8();
buffer.push(color.red());
buffer.push(color.green());
buffer.push(color.blue());
buffer.push(color.alpha());
}
if let Some(last) = last_glyph {
let (x, _) = self.rasterizer.kerning(last, key);
caret += x as i32;
}
if let Some(pixmap_glyph) =
PixmapRef::from_bytes(&buffer, glyph.width as _, glyph.height as _)
{
pixmap.draw_pixmap(
glyph.left + caret,
height - glyph.top + self.metrics.descent.round() as i32,
pixmap_glyph,
&PixmapPaint::default(),
Transform::identity(),
None,
);
}
caret += glyph.advance.0;
last_glyph = Some(key);
}
self.pixmap = Some(pixmap);
}
pub fn pixmap(&self) -> Option<&Pixmap> {
self.pixmap.as_ref()
}
fn calc_width(&mut self, glyphs: &[(GlyphKey, RasterizedGlyph)]) -> i32 {
let mut caret = 0;
let mut last_glyph: Option<&GlyphKey> = None;
for (key, glyph) in glyphs.iter() {
if let Some(last) = last_glyph {
let (x, _) = self.rasterizer.kerning(*last, *key);
caret += x as i32;
}
caret += glyph.advance.0;
last_glyph = Some(key);
}
caret
}
}

View file

@ -0,0 +1,16 @@
use tiny_skia::{Color, Pixmap};
#[derive(Debug)]
pub struct DumbTitleText {}
impl DumbTitleText {
pub fn update_scale(&mut self, _scale: u32) {}
pub fn update_title<S: Into<String>>(&mut self, _title: S) {}
pub fn update_color(&mut self, _color: Color) {}
pub fn pixmap(&self) -> Option<&Pixmap> {
None
}
}

View file

@ -0,0 +1,91 @@
#[derive(Debug)]
pub(crate) struct FontPreference {
pub name: String,
pub style: Option<String>,
pub pt_size: f32,
}
impl Default for FontPreference {
fn default() -> Self {
Self {
name: "sans-serif".into(),
style: None,
pt_size: 10.0,
}
}
}
impl FontPreference {
/// Parse config string like `Cantarell 12`, `Cantarell Bold 11`, `Noto Serif CJK HK Bold 12`.
pub fn from_name_style_size(conf: &str) -> Option<Self> {
// assume last is size, 2nd last is style and the rest is name.
match conf.rsplit_once(' ') {
Some((head, tail)) if tail.chars().all(|c| c.is_numeric()) => {
let pt_size: f32 = tail.parse().unwrap_or(10.0);
match head.rsplit_once(' ') {
Some((name, style)) if !name.is_empty() => Some(Self {
name: name.into(),
style: Some(style.into()),
pt_size,
}),
None if !head.is_empty() => Some(Self {
name: head.into(),
style: None,
pt_size,
}),
_ => None,
}
}
Some((head, tail)) if !head.is_empty() => Some(Self {
name: head.into(),
style: Some(tail.into()),
pt_size: 10.0,
}),
None if !conf.is_empty() => Some(Self {
name: conf.into(),
style: None,
pt_size: 10.0,
}),
_ => None,
}
}
}
#[test]
fn pref_from_multi_name_variant_size() {
let pref = FontPreference::from_name_style_size("Noto Serif CJK HK Bold 12").unwrap();
assert_eq!(pref.name, "Noto Serif CJK HK");
assert_eq!(pref.style, Some("Bold".into()));
assert!((pref.pt_size - 12.0).abs() < f32::EPSILON);
}
#[test]
fn pref_from_name_variant_size() {
let pref = FontPreference::from_name_style_size("Cantarell Bold 12").unwrap();
assert_eq!(pref.name, "Cantarell");
assert_eq!(pref.style, Some("Bold".into()));
assert!((pref.pt_size - 12.0).abs() < f32::EPSILON);
}
#[test]
fn pref_from_name_size() {
let pref = FontPreference::from_name_style_size("Cantarell 12").unwrap();
assert_eq!(pref.name, "Cantarell");
assert_eq!(pref.style, None);
assert!((pref.pt_size - 12.0).abs() < f32::EPSILON);
}
#[test]
fn pref_from_name() {
let pref = FontPreference::from_name_style_size("Cantarell").unwrap();
assert_eq!(pref.name, "Cantarell");
assert_eq!(pref.style, None);
assert!((pref.pt_size - 10.0).abs() < f32::EPSILON);
}
#[test]
fn pref_from_multi_name_style() {
let pref = FontPreference::from_name_style_size("Foo Bar Baz Bold").unwrap();
assert_eq!(pref.name, "Foo Bar Baz");
assert_eq!(pref.style, Some("Bold".into()));
assert!((pref.pt_size - 10.0).abs() < f32::EPSILON);
}