use gtk4::prelude::*;
pub struct ThemeColors {
pub view_bg: String,
pub selection: String,
pub foreground: String,
pub error: String,
}
impl ThemeColors {
#[allow(deprecated)]
pub fn resolve() -> Self {
let probe = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
let ctx = probe.style_context();
let rgba_to_hex = |c: >k4::gdk::RGBA| -> String {
format!(
"#{:02x}{:02x}{:02x}",
(c.red() * 255.0).round() as u8,
(c.green() * 255.0).round() as u8,
(c.blue() * 255.0).round() as u8,
)
};
let view_bg = if let Some(base) = ctx.lookup_color("theme_base_color") {
let luma = 0.299 * base.red() + 0.587 * base.green() + 0.114 * base.blue();
let factor: f32 = if luma < 0.5 { 0.77 } else { 0.98 };
let to_u8 =
|v: f32| -> u8 { ((v * factor * 255.0).round() as i32).clamp(0, 255) as u8 };
format!(
"#{:02x}{:02x}{:02x}",
to_u8(base.red()),
to_u8(base.green()),
to_u8(base.blue()),
)
} else {
"@theme_base_color".to_string()
};
let error = ctx
.lookup_color("error_color")
.map(|c| rgba_to_hex(&c))
.unwrap_or_else(|| "@error_color".to_string());
let selection = ctx
.lookup_color("theme_selected_bg_color")
.map(|c| {
format!(
"rgba({},{},{},0.32)",
(c.red() * 255.0).round() as u8,
(c.green() * 255.0).round() as u8,
(c.blue() * 255.0).round() as u8,
)
})
.unwrap_or_else(|| "alpha(@theme_selected_bg_color, 0.32)".to_string());
let foreground = ctx
.lookup_color("theme_fg_color")
.map(|c| rgba_to_hex(&c))
.unwrap_or_else(|| "@theme_fg_color".to_string());
Self {
view_bg,
selection,
foreground,
error,
}
}
pub fn base_css(&self, extra_rules: &str) -> String {
format!(
"* {{ font-size: 13px; }} \
window, window:backdrop {{ border: 1px solid alpha(white, 0.15); border-radius: 12px; background-color: {bg}; }} \
scrolledwindow, viewport, list, \
scrolledwindow:backdrop, viewport:backdrop, list:backdrop {{ background-color: transparent; background-image: none; border-radius: 10px; }} \
row:not(:selected), row:not(:selected):backdrop {{ background-color: transparent; background-image: none; }} \
row:selected, row:selected:backdrop {{ background-color: {sel}; color: {fg}; }} \
.dim {{ opacity: 0.55; }} \
row {{ border-radius: 8px; }} \
.close-btn {{ min-width: 24px; min-height: 24px; padding: 0; opacity: 0.35; }} \
.close-btn:hover {{ opacity: 0.9; }} \
{extra}",
bg = self.view_bg,
fg = self.foreground,
sel = self.selection,
extra = extra_rules,
)
}
}
pub fn load_css(css_text: &str) {
let css = gtk4::CssProvider::new();
css.load_from_data(css_text);
if let Some(display) = gtk4::gdk::Display::default() {
gtk4::style_context_add_provider_for_display(
&display,
&css,
gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
}
pub fn build_title_bar() -> (gtk4::Box, gtk4::Button) {
let title_bar = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
let spacer = gtk4::Box::new(gtk4::Orientation::Horizontal, 0);
spacer.set_hexpand(true);
title_bar.append(&spacer);
let close_btn = gtk4::Button::from_icon_name("window-close-symbolic");
close_btn.set_tooltip_text(Some("Close"));
close_btn.add_css_class("close-btn");
title_bar.append(&close_btn);
(title_bar, close_btn)
}
fn detect_edge(x: f64, y: f64, w: f64, h: f64, threshold: f64) -> Option<gtk4::gdk::SurfaceEdge> {
let top = y < threshold;
let bottom = y > h - threshold;
let left = x < threshold;
let right = x > w - threshold;
if top && left {
Some(gtk4::gdk::SurfaceEdge::NorthWest)
} else if top && right {
Some(gtk4::gdk::SurfaceEdge::NorthEast)
} else if bottom && left {
Some(gtk4::gdk::SurfaceEdge::SouthWest)
} else if bottom && right {
Some(gtk4::gdk::SurfaceEdge::SouthEast)
} else if top {
Some(gtk4::gdk::SurfaceEdge::North)
} else if bottom {
Some(gtk4::gdk::SurfaceEdge::South)
} else if left {
Some(gtk4::gdk::SurfaceEdge::West)
} else if right {
Some(gtk4::gdk::SurfaceEdge::East)
} else {
None
}
}
fn edge_cursor(edge: Option<gtk4::gdk::SurfaceEdge>) -> Option<&'static str> {
match edge {
Some(gtk4::gdk::SurfaceEdge::North) | Some(gtk4::gdk::SurfaceEdge::South) => {
Some("ns-resize")
}
Some(gtk4::gdk::SurfaceEdge::East) | Some(gtk4::gdk::SurfaceEdge::West) => {
Some("ew-resize")
}
Some(gtk4::gdk::SurfaceEdge::NorthEast) | Some(gtk4::gdk::SurfaceEdge::SouthWest) => {
Some("nesw-resize")
}
Some(gtk4::gdk::SurfaceEdge::NorthWest) | Some(gtk4::gdk::SurfaceEdge::SouthEast) => {
Some("nwse-resize")
}
_ => None,
}
}
pub fn install_edge_resize(window: >k4::Window) {
use std::cell::Cell;
use std::rc::Rc;
const CURSOR_THRESHOLD: f64 = 6.0;
const CLICK_THRESHOLD: f64 = 10.0;
{
let prev_edge: Rc<Cell<Option<gtk4::gdk::SurfaceEdge>>> = Rc::new(Cell::new(None));
let motion = gtk4::EventControllerMotion::new();
let win = window.clone();
let prev = Rc::clone(&prev_edge);
motion.connect_motion(move |_ctrl, x, y| {
let edge = detect_edge(
x,
y,
win.width() as f64,
win.height() as f64,
CURSOR_THRESHOLD,
);
if edge != prev.get() {
prev.set(edge);
win.set_cursor_from_name(edge_cursor(edge));
}
});
let prev_leave = Rc::clone(&prev_edge);
let win_leave = window.clone();
motion.connect_leave(move |_ctrl| {
prev_leave.set(None);
win_leave.set_cursor_from_name(None);
});
window.add_controller(motion);
}
{
let click = gtk4::GestureClick::new();
click.set_propagation_phase(gtk4::PropagationPhase::Capture);
let win = window.clone();
click.connect_pressed(move |gesture, _n_press, x, y| {
let edge = detect_edge(
x,
y,
win.width() as f64,
win.height() as f64,
CLICK_THRESHOLD,
);
if let Some(edge) = edge {
gesture.set_state(gtk4::EventSequenceState::Claimed);
let (sx, sy) = if let Some(native) = win.native() {
let (nx, ny) = native.surface_transform();
(x + nx, y + ny)
} else {
(x, y)
};
if let Some(native) = win.native() {
if let Some(surface) = native.surface() {
use gtk4::gdk::prelude::ToplevelExt;
if let Ok(toplevel) = surface.dynamic_cast::<gtk4::gdk::Toplevel>() {
let device = gesture.current_event().and_then(|e| e.device());
let timestamp = gesture.current_event().map_or(0, |e| e.time());
toplevel.begin_resize(edge, device.as_ref(), 1, sx, sy, timestamp);
}
}
}
} else {
gesture.set_state(gtk4::EventSequenceState::Denied);
}
});
window.add_controller(click);
}
}
pub fn present_centred(window: >k4::Window) {
window.set_opacity(0.0);
let window_reveal = window.clone();
window.connect_map(move |w| {
let xid = w
.surface()
.and_then(|s| s.downcast_ref::<gdk4_x11::X11Surface>().map(|xs| xs.xid()));
if let Some(xid) = xid {
#[allow(clippy::cast_possible_truncation)]
let xid32 = xid as u32;
crate::x11::x11_centre_and_raise_xid(xid32);
}
window_reveal.set_opacity(1.0);
});
window.present();
}