X11/Cairo UI toolkit for gardesk components
gartk is a shared X11/Cairo toolkit providing common UI functionality for gardesk components. It includes core types (Color, Rect, Theme), Cairo/Pango rendering with shape primitives and text layout, and an X11 backend with window management, event handling, and monitor detection. Used by garlaunch, garlock, gardm-greeter, garshot, and other gar components.
gartk is a Rust library providing common UI functionality for the gardesk desktop environment. It extracts shared patterns from garlaunch, garbar, gardm-greeter, and other components into reusable crates.
| Option | Type | Default | Description |
|---|---|---|---|
gartk-core | - | - | Fundamental types: Color, Rect, Theme, InputEvent |
gartk-render | - | - | Cairo/Pango rendering: surfaces, shapes, text |
gartk-x11 | - | - | X11 integration: windows, event loop, monitors |
Add to your Cargo.toml:
[dependencies]
gartk-core = { path = "../gartk/gartk-core" }
gartk-x11 = { path = "../gartk/gartk-x11" }
gartk-render = { path = "../gartk/gartk-render" } Minimal window example:
use gartk_core::{Color, Theme, InputEvent, Key};
use gartk_x11::{Connection, Window, WindowConfig, EventLoop, EventLoopConfig};
use gartk_render::Renderer;
fn main() -> anyhow::Result<()> {
// Connect to X11
let conn = Connection::connect(None)?;
// Create window
let window = Window::create(
conn.clone(),
WindowConfig::new()
.title("Hello gartk")
.class("hello-gartk")
.size(400, 300),
)?;
// Create renderer with theme
let theme = Theme::dark();
let mut renderer = Renderer::with_theme(400, 300, theme)?;
// Event loop
let mut event_loop = EventLoop::new(&window, EventLoopConfig::default())?;
event_loop.run(|ev_loop, event| {
match event {
InputEvent::Expose | InputEvent::Idle => {
if ev_loop.needs_redraw() {
renderer.clear()?;
renderer.text_default("Hello, World!", 50.0, 150.0, Color::WHITE)?;
renderer.flush();
ev_loop.redraw_done();
}
}
InputEvent::Key(e) if e.pressed && e.key == Key::Escape => {
return Ok(false); // Exit
}
InputEvent::CloseRequested => return Ok(false),
_ => {}
}
Ok(true) // Continue
})?;
Ok(())
} gartk is used by these components:
Core types used throughout gartk. These are platform-independent abstractions that can be shared across different backends.
use gartk_core::{
Color, ColorError,
Point, Size, Rect, Edges,
InputEvent, Key, KeyEvent, Modifiers,
MouseButton, MouseEvent, ScrollEvent,
Theme, ThemeBuilder,
}; RGBA color with components in 0.0-1.0 range. Supports parsing from multiple formats and color space conversions.
| Option | Type | Default | Description |
|---|---|---|---|
new(r, g, b, a) | - | - | Create from 0.0-1.0 float components |
rgb(r, g, b) | - | - | Create opaque color from floats |
from_u8(r, g, b, a) | - | - | Create from 0-255 integer components |
from_hex(s) | - | - | Parse #RGB, #RRGGBB, or #RRGGBBAA |
from_rgb_str(s) | - | - | Parse rgb(r,g,b) or rgba(r,g,b,a) |
from_name(s) | - | - | Get named color (black, white, red, etc.) |
parse(s) | - | - | Auto-detect format and parse |
// Various parsing methods
let c1 = Color::from_hex("#ff6600")?;
let c2 = Color::from_hex("#f60")?; // Short form
let c3 = Color::from_hex("#ff660080")?; // With alpha
let c4 = Color::from_rgb_str("rgb(255, 102, 0)")?;
let c5 = Color::from_rgb_str("rgba(255, 102, 0, 0.5)")?;
let c6 = Color::from_name("red")?;
// Auto-detect format
let c7 = Color::parse("#ff6600")?;
let c8 = Color::parse("rgb(255, 0, 0)")?;
let c9 = Color::parse("blue")?; Convert between RGB and HSV color spaces for intuitive color manipulation:
// RGB to HSV
let color = Color::from_hex("#ff6600")?;
let (hue, sat, val) = color.to_hsv();
// hue: 0-360, sat: 0-1, val: 0-1
// HSV to RGB
let orange = Color::from_hsv(30.0, 1.0, 1.0);
let orange_alpha = Color::from_hsva(30.0, 1.0, 1.0, 0.5);
// Modify individual components
let shifted = color.with_hue(180.0); // Shift hue
let muted = color.with_saturation(0.5); // Reduce saturation
let darker = color.with_value(0.5); // Reduce brightness | Option | Type | Default | Description |
|---|---|---|---|
with_alpha(a) | - | - | Return color with modified alpha |
lighten(factor) | - | - | Lighten by factor (0.0-1.0) |
darken(factor) | - | - | Darken by factor (0.0-1.0) |
to_hsv() | - | - | Convert to (hue, saturation, value) |
from_hsv(h, s, v) | - | - | Create from HSV values |
with_hue(h) | - | - | Return color with modified hue |
with_saturation(s) | - | - | Return color with modified saturation |
with_value(v) | - | - | Return color with modified brightness |
to_hex() | - | - | Convert to hex string (#rrggbb or #rrggbbaa) |
to_argb_u32() | - | - | Convert to ARGB u32 for X11 |
let base = Color::from_hex("#3498db")?;
// Adjust alpha
let translucent = base.with_alpha(0.5);
// Lighten/darken
let lighter = base.lighten(0.2); // 20% lighter
let darker = base.darken(0.2); // 20% darker
// Convert for X11
let argb = base.to_argb_u32(); // For window background
let hex = base.to_hex(); // "#3498db" | Option | Type | Default | Description |
|---|---|---|---|
BLACK | - | - | #000000 |
WHITE | - | - | #ffffff |
RED | - | - | #ff0000 |
GREEN | - | - | #00ff00 |
BLUE | - | - | #0000ff |
YELLOW | - | - | #ffff00 |
CYAN | - | - | #00ffff |
MAGENTA | - | - | #ff00ff |
GRAY | - | - | #808080 |
DARK_GRAY | - | - | #404040 |
LIGHT_GRAY | - | - | #c0c0c0 |
TRANSPARENT | - | - | rgba(0,0,0,0) |
2D point with i32 coordinates:
let p = Point::new(100, 200);
let origin = Point::ORIGIN; // (0, 0)
// Access
let x = p.x;
let y = p.y; 2D size with u32 dimensions:
let s = Size::new(800, 600);
// Access
let w = s.width;
let h = s.height;
// Check if empty
if s.is_empty() { /* ... */ } Rectangle with position and size:
let r = Rect::new(10, 20, 100, 50); // x, y, width, height
let r2 = Rect::from_size(Size::new(100, 50)); // At origin
// Bounds
let left = r.left(); // x
let top = r.top(); // y
let right = r.right(); // x + width
let bottom = r.bottom();// y + height
// Position and size
let pos = r.position(); // Point
let size = r.size(); // Size
// Hit testing
if r.contains_point(Point::new(50, 30)) {
// Point is inside rectangle
}
// Intersection
if r.intersects(&other_rect) {
let overlap = r.intersection(&other_rect);
} Edge values for padding/margin:
let edges = Edges::new(10, 20, 10, 20); // top, right, bottom, left
let uniform = Edges::all(8);
let zero = Edges::ZERO;
// Access individual edges
let top = edges.top;
let horizontal = edges.left + edges.right; Platform-agnostic input event abstraction:
| Option | Type | Default | Description |
|---|---|---|---|
Key(KeyEvent) | - | - | Keyboard press or release |
MousePress(MouseEvent) | - | - | Mouse button pressed |
MouseRelease(MouseEvent) | - | - | Mouse button released |
MouseMove(MouseEvent) | - | - | Mouse cursor moved |
MouseEnter(Point) | - | - | Mouse entered window |
MouseLeave | - | - | Mouse left window |
Scroll(ScrollEvent) | - | - | Scroll wheel event |
Expose | - | - | Window needs redraw |
Resize { width, height } | - | - | Window resized |
FocusIn | - | - | Window gained focus |
FocusOut | - | - | Window lost focus |
CloseRequested | - | - | Window close button clicked |
SelectionRequest | - | - | Clipboard request from another app |
SelectionClear | - | - | Lost clipboard ownership |
Idle | - | - | Sent every frame when no other events |
match event {
InputEvent::Key(e) if e.pressed => {
match e.key {
Key::Escape => quit(),
Key::Return => submit(),
Key::Char(c) => input.push(c),
Key::Backspace => input.pop(),
Key::Up => select_prev(),
Key::Down => select_next(),
_ => {}
}
// Check modifiers
if e.modifiers.ctrl {
// Ctrl+key
}
}
_ => {}
} match event {
InputEvent::MousePress(e) => {
let pos = e.position;
if let Some(MouseButton::Left) = e.button {
handle_click(pos);
}
}
InputEvent::MouseMove(e) => {
update_hover(e.position);
}
_ => {}
} match event {
InputEvent::Scroll(e) => {
// delta_y: -1 for up, 1 for down
// delta_x: -1 for left, 1 for right
scroll_offset += e.delta_y * 20; // 20px per scroll step
}
_ => {}
} Cairo/Pango rendering layer providing surfaces, shape primitives, and text layout.
use gartk_render::{
Surface, DoubleBufferedSurface,
Renderer,
TextRenderer, TextStyle, TextAlign,
fill_rect, fill_rounded_rect, stroke_rect,
fill_circle, line, set_color,
};
// Re-exports for direct Cairo access
use gartk_render::{cairo, pango, pangocairo}; // Create an image surface
let surface = Surface::new(800, 600)?;
// Get Cairo context for drawing
let ctx = surface.context()?;
// Clear with a color
surface.clear(Color::from_hex("#1e1e2e")?)?; // Handle window resize
match event {
InputEvent::Resize { width, height } => {
surface.resize(width, height)?;
}
_ => {}
} // For flicker-free rendering
use gartk_render::copy_surface_to_window;
// After drawing, copy to window
surface.flush();
copy_surface_to_window(&surface, &window)?; High-level renderer combining surface, shapes, and text:
// Basic renderer
let renderer = Renderer::new(800, 600)?;
// With theme (recommended)
let theme = Theme::dark();
let renderer = Renderer::with_theme(800, 600, theme)?;
// Clear with theme background
renderer.clear()?;
// Or specific color
renderer.clear_color(Color::BLACK)?; | Option | Type | Default | Description |
|---|---|---|---|
fill_rect(ctx, rect, color) | - | - | Fill a rectangle |
fill_rounded_rect(ctx, rect, radius, color) | - | - | Fill rounded rectangle |
stroke_rect(ctx, rect, color, width) | - | - | Stroke rectangle outline |
stroke_rounded_rect(ctx, rect, radius, color, width) | - | - | Stroke rounded rect outline |
fill_circle(ctx, cx, cy, radius, color) | - | - | Fill a circle |
stroke_circle(ctx, cx, cy, radius, color, width) | - | - | Stroke circle outline |
line(ctx, x1, y1, x2, y2, color, width) | - | - | Draw a line |
hline(ctx, x, y, length, color, width) | - | - | Draw horizontal line |
vline(ctx, x, y, length, color, width) | - | - | Draw vertical line |
// Via Renderer
renderer.fill_rect(Rect::new(10, 10, 100, 50), Color::BLUE)?;
renderer.fill_rounded_rect(Rect::new(10, 70, 100, 50), 8.0, Color::GREEN)?;
renderer.stroke_rect(Rect::new(10, 130, 100, 50), Color::RED, 2.0)?;
renderer.fill_circle(160.0, 35.0, 20.0, Color::YELLOW)?;
renderer.line(10.0, 200.0, 110.0, 200.0, Color::WHITE, 2.0)?;
// Via shape functions with context
let ctx = renderer.context()?;
fill_rounded_rect(&ctx, Rect::new(0, 0, 100, 30), 4.0, Color::BLUE);
stroke_circle(&ctx, 50.0, 50.0, 25.0, Color::WHITE, 2.0); // Simple text with default style
renderer.text_default("Hello!", 10.0, 50.0, Color::WHITE)?;
// Custom style
let style = TextStyle::new()
.font_family("JetBrains Mono")
.font_size(16.0)
.color(Color::from_hex("#cdd6f4")?)
.bold(true);
renderer.text("Styled text", 10.0, 80.0, &style)?;
// Text in rectangle (with alignment)
let rect = Rect::new(10, 100, 200, 30);
let centered_style = style.align(TextAlign::Center);
renderer.text_in_rect("Centered", rect, ¢ered_style)?;
// Centered at point
renderer.text_centered("Center", Point::new(100, 150), &style)?;
// Measure text
let size = renderer.measure_text("Some text", &style)?; let style = TextStyle::new()
.font_family("sans-serif") // Font family
.font_size(14.0) // Size in points
.color(Color::WHITE) // Text color
.bold(true) // Bold weight
.italic(true) // Italic style
.underline(true) // Underline
.strikethrough(true) // Strikethrough
.align(TextAlign::Left) // Left, Center, Right
.line_height(1.5); // Line height multiplier X11 integration layer providing window management, event handling, and system integration.
use gartk_x11::{
Connection,
Window, WindowConfig, WindowType,
EventLoop, EventLoopConfig,
Monitor, detect_monitors, primary_monitor,
CursorManager, CursorShape,
ClipboardManager, ClipboardContent,
}; // Connect to default display
let conn = Connection::connect(None)?;
// Connect to specific display
let conn = Connection::connect(Some(":1"))?;
// Get screen info
let width = conn.screen_width();
let height = conn.screen_height();
let root = conn.root();
let depth = conn.default_depth();
// Check for ARGB visual (transparency)
if let Some(visual) = conn.find_argb_visual() {
// Transparency supported
}
// Flush pending requests
conn.flush()?;
// Poll for events
while let Some(event) = conn.poll_event()? {
// Handle event
} | Option | Type | Default | Description |
|---|---|---|---|
title | String | - | Window title displayed in title bar |
class | String | - | Window class for WM_CLASS property |
position | Option<Point> | - | Initial position (None = centered) |
size | Size | - | Window dimensions (default 400x300) |
override_redirect | bool | - | Bypass window manager (popups) |
window_type | WindowType | - | EWMH window type hint |
background | Option<u32> | - | Background color (ARGB) |
border_width | u32 | - | Window border width |
map_on_create | bool | - | Show window immediately (default true) |
transparent | bool | - | Request ARGB visual for transparency |
parent_window | Option<Window> | - | Parent for dialogs (WM_TRANSIENT_FOR) |
modal | bool | - | Set modal and above states |
// Builder pattern
let window = Window::create(
conn.clone(),
WindowConfig::new()
.title("My App")
.class("my-app")
.size(800, 600)
.position(100, 100) // Or omit for centered
.background(0xff1e1e2e) // ARGB
.transparent(true) // ARGB visual
.border_width(0),
)?;
// Presets
let popup = WindowConfig::popup(); // override_redirect + PopupMenu
let dialog = WindowConfig::dialog(); // Dialog type
// Modal dialog
let dialog = Window::create(
conn.clone(),
WindowConfig::dialog()
.title("Confirm")
.size(300, 150)
.parent_window(main_window.id())
.modal(true),
)?; | Option | Type | Default | Description |
|---|---|---|---|
Normal | - | - | Regular application window (default) |
Dialog | - | - | Dialog window, typically centered over parent |
Utility | - | - | Utility window (toolbox, palette) |
PopupMenu | - | - | Popup menu, no decorations |
Splash | - | - | Splash screen, no decorations |
| Option | Type | Default | Description |
|---|---|---|---|
id() | - | - | Get X11 window ID |
map() / unmap() | - | - | Show or hide the window |
move_to(x, y) | - | - | Move window to position |
resize(w, h) | - | - | Resize window |
set_geometry(rect) | - | - | Move and resize in one call |
raise() | - | - | Raise window to top |
focus() | - | - | Request input focus |
activate() | - | - | Request activation via EWMH |
clear() | - | - | Clear window with background color |
set_title(s) | - | - | Change window title |
grab_keyboard() | - | - | Grab exclusive keyboard input |
ungrab_keyboard() | - | - | Release keyboard grab |
grab_pointer() | - | - | Grab mouse input |
ungrab_pointer() | - | - | Release mouse grab |
let config = EventLoopConfig {
fps: 60, // Target frame rate
continuous_redraw: false, // Redraw only on expose
};
let mut event_loop = EventLoop::new(&window, config)?;
event_loop.run(|ev_loop, event| {
match event {
InputEvent::Expose => {
ev_loop.request_redraw();
}
InputEvent::Idle => {
if ev_loop.needs_redraw() {
// Draw frame
render()?;
ev_loop.redraw_done();
}
}
InputEvent::CloseRequested => {
return Ok(false); // Exit loop
}
_ => {}
}
Ok(true) // Continue
})?; // Detect all monitors via XRandR
let monitors = detect_monitors(&conn)?;
for mon in &monitors {
println!("{}: {}x{}+{}+{} {}",
mon.name, mon.width, mon.height,
mon.x, mon.y,
if mon.primary { "(primary)" } else { "" }
);
}
// Get primary monitor
let primary = primary_monitor(&conn)?;
// Find monitor at point
let mon = monitor_at_point(&conn, 500, 300)?;
// Find monitor at pointer
let mon = monitor_at_pointer(&conn)?; let cursor_mgr = CursorManager::new(&conn)?;
// Set cursor shape
cursor_mgr.set_cursor(&window, CursorShape::Arrow)?;
cursor_mgr.set_cursor(&window, CursorShape::IBeam)?;
cursor_mgr.set_cursor(&window, CursorShape::Crosshair)?;
cursor_mgr.set_cursor(&window, CursorShape::Hand)?;
cursor_mgr.set_cursor(&window, CursorShape::ResizeNS)?;
cursor_mgr.set_cursor(&window, CursorShape::ResizeEW)?; let clipboard = ClipboardManager::new(&conn)?;
// Set clipboard content
clipboard.set_text("Hello clipboard")?;
// For file paths (drag-and-drop, file managers)
clipboard.set_uris(&["/path/to/file.txt"])?; gartk includes a comprehensive theming system with presets and builder pattern for custom themes.
// Dark theme (default) - Catppuccin Mocha inspired
let theme = Theme::dark();
// Light theme - Catppuccin Latte inspired
let theme = Theme::light();
// High contrast for accessibility
let theme = Theme::high_contrast(); | Option | Type | Default | Description |
|---|---|---|---|
background | Color | - | Window background color |
foreground | Color | - | Default text color |
border | Color | - | Border color |
border_width | u32 | - | Border width in pixels |
border_radius | f64 | - | Corner radius for rounded elements |
font_family | String | - | Font family name |
font_size | f64 | - | Font size in points |
selection_background | Color | - | Selected text background |
selection_foreground | Color | - | Selected text color |
input_background | Color | - | Input field background |
input_foreground | Color | - | Input field text color |
input_border | Color | - | Input field border |
input_placeholder | Color | - | Placeholder text color |
input_cursor | Color | - | Text cursor color |
| Option | Type | Default | Description |
|---|---|---|---|
item_background | Color | - | List item background |
item_foreground | Color | - | List item text color |
item_hover_background | Color | - | Hovered item background |
item_hover_foreground | Color | - | Hovered item text |
item_selected_background | Color | - | Selected item background |
item_selected_foreground | Color | - | Selected item text |
item_description | Color | - | Item description text color |
| Option | Type | Default | Description |
|---|---|---|---|
scrollbar_track | Color | - | Scrollbar track color |
scrollbar_thumb | Color | - | Scrollbar thumb color |
scrollbar_width | u32 | - | Scrollbar width in pixels |
| Option | Type | Default | Description |
|---|---|---|---|
padding | u32 | - | General padding |
item_spacing | u32 | - | Spacing between list items |
item_padding | u32 | - | Padding inside list items |
// Builder pattern
let theme = Theme::builder()
.background(Color::from_hex("#282a36")?)
.foreground(Color::from_hex("#f8f8f2")?)
.border(Color::from_hex("#44475a")?)
.border_radius(6.0)
.font_family("JetBrains Mono")
.font_size(13.0)
.padding(16)
.build();
// Modify existing theme
let custom = ThemeBuilder::from(Theme::dark())
.font_size(16.0)
.border_radius(0.0) // Sharp corners
.build(); let theme = Theme::dark();
let renderer = Renderer::with_theme(800, 600, theme.clone())?;
// Access theme in rendering
renderer.clear()?; // Uses theme.background
// Use theme colors
renderer.fill_rect(rect, renderer.theme().selection_background)?;
renderer.text_default("Text", x, y, renderer.theme().foreground)?;
// Draw themed input field
let input_rect = Rect::new(10, 10, 200, 30);
renderer.fill_rounded_rect(
input_rect,
theme.border_radius,
theme.input_background
)?;
renderer.stroke_rounded_rect(
input_rect,
theme.border_radius,
theme.input_border,
1.0
)?; Symptom: "No display connection" error.
Solutions:
Symptom: Window background is black instead of transparent.
Causes:
transparent(true) in WindowConfigSymptom: KeyboardGrabFailed error when spawned from WM keybinding.
Solution: Use retry with delay:
// WM may still hold keyboard when spawning
window.grab_keyboard_with_retry(10, 20)?; // 10 attempts, 20ms delay Symptom: Window appears but doesn't respond to keyboard.
Solutions:
window.focus() after mappingwindow.activate() (EWMH)Symptom: Surface::new returns error.
Possible causes:
Symptom: Text renders with fallback font.
Solutions:
fc-cache -f to rebuild font cachefc-list | grep "FontName"When reporting issues, please include:
rustc --versioncat /etc/os-releasexdpyinfo | head -5Report issues at GitHub Issues.