gartk
UI Toolkit Library
Shared UI toolkit providing X11 window management, Cairo/Pango rendering, theming, and input handling for gardesk GUI components.
Features
- X11 window management with x11rb
- Cairo/Pango rendering for graphics and text
- Type-safe color and geometry primitives
- Event loop with input handling
- Monitor detection via RandR
- Keyboard and cursor management
- Clipboard manager with X11 selection protocol
- Dialog window support with transient/modal
- Window activation API (EWMH _NET_ACTIVE_WINDOW)
- Motion event coalescing and idle events
- Theme system for consistent styling
- Transparency and compositing support
- XDND drag-and-drop with file path extraction
- Modular crate architecture
- Double-buffering for flicker-free rendering
Quick Start
What is gartk?
gartk is a modular UI toolkit library that provides X11 window management, Cairo/Pango rendering, and input handling primitives for gardesk components. It's designed as a foundation for building X11 GUI applications with Rust, offering a clean, type-safe API over x11rb, Cairo, and Pango.
The toolkit is split into three crates for modularity: gartk-core
(platform-independent types), gartk-x11 (X11 integration),
and gartk-render (Cairo/Pango rendering).
Installation
Add gartk crates to your Cargo.toml:
[dependencies]
gartk-core = { path = "path/to/gartk/gartk-core" }
gartk-x11 = { path = "path/to/gartk/gartk-x11" }
gartk-render = { path = "path/to/gartk/gartk-render" } System Dependencies
gartk requires X11 development libraries and Cairo/Pango:
Hello World Example
A minimal example showing window creation, rendering, and event handling:
use gartk_core::{Color, InputEvent, Key, Rect, Theme};
use gartk_render::{Renderer, TextStyle};
use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig};
fn main() -> anyhow::Result<()> {
// Connect to X11
let conn = Connection::connect(None)?;
// Create a window
let window = Window::create(
conn.clone(),
WindowConfig::new()
.title("Hello gartk")
.class("hello")
.size(400, 300)
.centered(),
)?;
// Create renderer with theme
let theme = Theme::dark();
let mut renderer = Renderer::with_theme(400, 300, theme.clone())?;
// Create event loop
let config = EventLoopConfig::default();
let mut event_loop = EventLoop::new(&window, config)?;
// Main loop
event_loop.run(|el, event| {
match event {
InputEvent::Expose => {
// Clear and render
renderer.clear()?;
renderer.text_centered(
"Hello, gartk!",
Rect::new(0, 0, 400, 300).center(),
&TextStyle::new()
.font_size(24.0)
.color(theme.foreground),
)?;
// Copy to window (simplified - see docs for GC creation)
renderer.flush();
}
InputEvent::Key(key_event) if key_event.key == Key::Escape => {
el.quit();
return Ok(false);
}
InputEvent::CloseRequested => {
el.quit();
return Ok(false);
}
_ => {}
}
Ok(true)
})?;
Ok(())
} Key Features
Type-Safe X11
Built on x11rb for zero-copy event parsing and type-safe protocol handling
High-Quality Rendering
Cairo/Pango integration for antialiased graphics and advanced text layout
Built-in Theming
Dark, light, and high-contrast themes with full customization support
Modular Design
Three-crate structure allows using only what you need
ARGB Transparency
Automatic ARGB visual selection for composited transparency
RandR Support
Multi-monitor detection with DPI calculation and positioning
🚀 Who Should Use gartk?
- • Developers building X11 GUI applications in Rust
- • Projects needing lightweight window management without full GUI frameworks
- • Tools requiring custom launchers, popups, or overlay UIs
- • Applications targeting Linux desktop environments with X11
Configuration
gartk is a library, not a standalone application. Configuration happens in your Rust code through builders and APIs.
Theme Configuration
Create custom themes using the Theme builder:
use gartk_core::{Color, Theme, ThemeBuilder};
// Use a preset theme
let theme = Theme::dark(); // Default Catppuccin-inspired
let theme = Theme::light(); // Light theme
let theme = Theme::high_contrast(); // Accessibility
// Customize a theme
let custom = ThemeBuilder::from(Theme::dark())
.background(Color::from_hex("#1a1b26")?)
.foreground(Color::from_hex("#c0caf5")?)
.border(Color::from_hex("#565f89")?)
.border_radius(12.0)
.font_family("JetBrains Mono")
.font_size(13.0)
.build();
// Build from scratch
let theme = Theme::builder()
.background(Color::from_hex("#282a36")?)
.foreground(Color::from_hex("#f8f8f2")?)
.selection_background(Color::from_hex("#44475a")?)
.input_background(Color::from_hex("#44475a")?)
.item_selected_background(Color::from_hex("#6272a4")?)
.padding(16)
.build(); Window Configuration
WindowConfig provides a builder API for window creation:
use gartk_x11::{WindowConfig, WindowType};
// Popup window (override-redirect, bypasses WM)
let config = WindowConfig::popup()
.title("Menu")
.position(100, 100)
.size(400, 300)
.transparent(true);
// Dialog window (managed by WM)
let config = WindowConfig::dialog()
.title("Settings")
.class("myapp-settings")
.size(600, 400)
.centered();
// Normal application window
let config = WindowConfig::new()
.title("My App")
.class("myapp")
.size(800, 600)
.window_type(WindowType::Normal)
.background(0xFF1e1e2e); // ARGB color
// Utility window
let config = WindowConfig::new()
.window_type(WindowType::Utility)
.title("Tool Palette")
.size(200, 400)
.map_on_create(false); // Don't show immediately Color Formats
Color supports multiple parsing formats:
use gartk_core::Color;
// Hex colors
let color = Color::from_hex("#ff0000")?; // Red
let color = Color::from_hex("#f00")?; // Short form
let color = Color::from_hex("#ff000080")?; // With alpha
// RGB/RGBA
let color = Color::from_rgb_str("rgb(255, 0, 0)")?;
let color = Color::from_rgb_str("rgba(255, 0, 0, 0.5)")?;
// Named colors
let color = Color::from_name("red")?;
let color = Color::from_name("transparent")?;
// Parse any format
let color = Color::parse("#ff0000")?;
let color = "rgba(255, 0, 0, 0.5)".parse::<Color>()?;
// From u8 components (0-255)
let color = Color::from_u8(255, 128, 64, 255);
// Programmatic creation (0.0-1.0)
let color = Color::new(1.0, 0.5, 0.25, 1.0);
let color = Color::rgb(1.0, 0.0, 0.0);
// Manipulation
let lighter = color.lighten(0.2);
let darker = color.darken(0.3);
let transparent = color.with_alpha(0.5); Event Loop Configuration
use gartk_x11::EventLoopConfig;
// Default: 60 FPS, expose-only redraw
let config = EventLoopConfig::default();
// Custom FPS and continuous redraw
let config = EventLoopConfig {
fps: 120,
continuous_redraw: true, // Redraw every frame
}; Text Styling
use gartk_render::{TextStyle, TextAlign};
let style = TextStyle::new()
.font_family("JetBrains Mono")
.font_size(14.0)
.color(Color::from_hex("#cdd6f4")?)
.align(TextAlign::Center)
.ellipsize(true) // Add "..." if text too long
.wrap(false) // Don't wrap lines
.max_width(300); // Pixel width constraint Overview
gartk is a modular UI toolkit for building X11 GUI applications in Rust. It provides:
- • Type-safe X11 window management via x11rb
- • High-quality rendering with Cairo and Pango
- • Platform-independent geometry and color types
- • Built-in theming with light/dark/high-contrast presets
- • Input event abstraction (keyboard, mouse, window events)
- • RandR multi-monitor support with DPI calculation
- • ARGB visual support for composited transparency
The library is battle-tested as the foundation for garlaunch.
Architecture
gartk is split into three independent crates to allow selective dependency. Applications can depend on just gartk-core for types, add gartk-x11 for windowing, or use the full stack with gartk-render for Cairo/Pango graphics.
gartk-render (Cairo/Pango rendering)
↓
gartk-x11 (X11 connection, windows, events)
↓
gartk-core (colors, geometry, themes, input events) Crate Structure
gartk is split into three independent crates for modularity:
gartk-core
Platform-independent core types with no X11 or rendering dependencies.
Exports:
- •
Color- RGBA color with parsing (hex, rgb, named) - •
Rect,Point,Size,Edges- Geometry primitives - •
Theme,ThemeBuilder- UI theming - •
InputEvent,KeyEvent,MouseEvent,ScrollEvent- Input abstraction - •
Key,Modifiers,MouseButton- Input primitives
gartk-x11
X11 integration layer built on x11rb. Handles connections, windows, events, and monitors.
Exports:
- •
Connection- X11 connection wrapper with cached screen info - •
Window,WindowConfig- Window creation and management - •
EventLoop,EventLoopConfig- Blocking event loop with FPS control - •
Monitor- RandR monitor detection with DPI - •
CursorManager,CursorShape- Cursor management - •
Atoms- Pre-interned EWMH and ICCCM atoms - •
key_from_keycode,modifiers_from_x11- Keyboard utilities
gartk-render
Cairo/Pango rendering layer with high-level abstractions and shape primitives.
Exports:
- •
Renderer- High-level renderer combining shapes and text - •
Surface,DoubleBufferedSurface- Cairo image surfaces - •
TextRenderer,TextStyle,TextAlign- Pango text rendering - •
fill_rect,fill_rounded_rect,fill_circle- Shape fills - •
stroke_rect,stroke_rounded_rect- Shape strokes - •
line,hline,vline- Line drawing - •
copy_surface_to_window- X11 PutImage helper
Design Principles
Modularity
Three-crate split allows cherry-picking components. Use gartk-core alone for color/geometry types.
Type Safety
Built on x11rb for compile-time protocol safety. Strong typing for events, colors, and geometry.
Builder APIs
Fluent builder pattern for WindowConfig, Theme, and TextStyle enables readable configuration.
Performance
Zero-copy event parsing, double-buffered rendering, Arc-based connection sharing.
Error Handling
thiserror-based error types with context. Result types throughout the API.
gartk-core
Platform-independent core types with zero X11 or rendering dependencies. Provides the foundational types used throughout the gardesk ecosystem: colors with multiple parse formats, geometry primitives, a theme system, and input event abstractions.
- Color - RGBA with hex, rgb(), rgba(), and named color parsing
- Geometry - Rect, Point, Size, Edges for layout math
- Theme - Semantic color roles with builder pattern and presets
- Events - InputEvent, KeyEvent, MouseEvent, ScrollEvent
Color
The Color type represents RGBA colors with components in 0.0-1.0 range:
pub struct Color {
pub r: f64,
pub g: f64,
pub b: f64,
pub a: f64,
} Parsing
- •
from_hex: #RGB, #RRGGBB, #RRGGBBAA - •
from_rgb_str: rgb(255, 0, 0), rgba(255, 0, 0, 0.5) - •
from_name: "red", "blue", "transparent", "darkgray" - •
parse: Auto-detects format
Manipulation
- •
with_alpha(a): Returns new color with alpha - •
lighten(factor): Lightens by 0.0-1.0 - •
darken(factor): Darkens by 0.0-1.0 - •
to_argb_u32(): Convert to X11 ARGB format - •
to_rgb_u32(): Convert to 24-bit RGB - •
to_hex(): Convert to hex string (#RRGGBB)
HSV Color Space
- •
to_hsv(): Convert RGB to (hue, saturation, value) tuple - •
from_hsv(h, s, v): Create color from HSV (hue 0-360, sat/val 0.0-1.0) - •
from_hsva(h, s, v, a): Create color from HSVA - •
with_hue(h): Returns new color with modified hue - •
with_saturation(s): Returns new color with modified saturation - •
with_value(v): Returns new color with modified brightness
Constants
BLACK, WHITE, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA, TRANSPARENT, GRAY, DARK_GRAY, LIGHT_GRAY
Geometry Types
Point
2D point with i32 coordinates:
let p = Point::new(100, 50); let p = Point::from((100, 50)); let p = Point::ORIGIN; // (0, 0)
Size
2D size with u32 dimensions:
let s = Size::new(800, 600); let s = Size::ZERO; // (0, 0) let area = s.area(); // Returns u64
Rect
Rectangle with position and size:
let r = Rect::new(10, 10, 200, 100);
let r = Rect::from_point_size(Point::new(10, 10), Size::new(200, 100));
// Queries
let pos = r.position(); // Point
let size = r.size(); // Size
let center = r.center(); // Point
let right = r.right(); // i32
let bottom = r.bottom(); // i32
// Tests
if r.contains_point(Point::new(50, 50)) { }
if r.intersects(other_rect) { }
let intersection = r.intersection(other_rect)?;
// Insets (shrinking)
let inset = r.inset_uniform(10); // 10px on all sides
let inset = r.inset(5, 10, 5, 10); // top, right, bottom, left Edges
Padding/margin specification:
let e = Edges::uniform(10); // All sides let e = Edges::symmetric(8, 12); // vertical, horizontal let e = Edges::new(5, 10, 5, 10); // TRBL let total_h = e.horizontal(); // left + right let total_v = e.vertical(); // top + bottom
Input Events
Platform-independent event abstraction (converted from X11 events by EventLoop):
pub enum InputEvent {
Key(KeyEvent),
MousePress(MouseEvent),
MouseRelease(MouseEvent),
MouseMove(MouseEvent),
MouseEnter(Point),
MouseLeave,
Scroll(ScrollEvent),
Expose,
Resize { width: u32, height: u32 },
FocusIn,
FocusOut,
CloseRequested, // WM_DELETE_WINDOW
Idle, // No pending events (v0.3.0)
SelectionRequest(SelectionRequestEvent),
SelectionNotify(SelectionNotifyEvent),
SelectionClear,
FileDrop(Vec<PathBuf>), // XDND file drop
} KeyEvent
pub struct KeyEvent {
pub key: Key,
pub keycode: u8,
pub modifiers: Modifiers,
pub pressed: bool,
}
// Key enum
pub enum Key {
Escape, Return, Tab, Backspace, Delete,
Home, End, PageUp, PageDown, Insert,
Left, Right, Up, Down,
F1..F12,
Space,
Char(char), // Printable characters
Unknown(u8),
} KeyEvent Helpers
key_event.is_printable() // true for Char(c) and Space
key_event.char() // Some('a') for printable keys Modifiers
pub struct Modifiers {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub super_key: bool,
pub caps_lock: bool,
pub num_lock: bool,
} MouseButton
pub enum MouseButton {
Left, Middle, Right,
ScrollUp, ScrollDown, ScrollLeft, ScrollRight,
Back, Forward,
Unknown(u8),
} Theme System
Theme provides comprehensive UI styling configuration:
pub struct Theme {
// Window
pub background: Color,
pub foreground: Color,
pub border: Color,
pub border_width: u32,
pub border_radius: f64,
// Text
pub font_family: String,
pub font_size: f64,
// Selection
pub selection_background: Color,
pub selection_foreground: Color,
// Input field
pub input_background: Color,
pub input_foreground: Color,
pub input_border: Color,
pub input_placeholder: Color,
pub input_cursor: Color,
// Items/list
pub item_background: Color,
pub item_foreground: Color,
pub item_hover_background: Color,
pub item_hover_foreground: Color,
pub item_selected_background: Color,
pub item_selected_foreground: Color,
pub item_description: Color,
// Scrollbar
pub scrollbar_track: Color,
pub scrollbar_thumb: Color,
pub scrollbar_width: u32,
// Spacing
pub padding: u32,
pub item_spacing: u32,
pub item_padding: u32,
} Built-in Themes
- •
Theme::dark(): Catppuccin Mocha-inspired (#1e1e2e background) - •
Theme::light(): Catppuccin Latte (#eff1f5 background) - •
Theme::high_contrast(): Black/white for accessibility
Use ThemeBuilder to customize (see Config tab).
gartk-x11
X11 integration layer built on x11rb. Wraps raw X11 protocol into safe, ergonomic Rust types for connection management, window creation, event handling, monitor detection, keyboard translation, and cursor control. Used by all gardesk GUI applications that need X11 windowing.
- Connection - Cached screen info, visual detection, atom interning
- Window - Creation with ARGB support, properties, map/unmap
- EventLoop - Blocking loop with FPS control and redraw requests
- Monitor - RandR-based detection with DPI calculation
- Keyboard - Keycode-to-Key translation, modifier tracking
- Cursor - Shape management via X cursor font
- Atoms - Pre-interned EWMH/ICCCM atoms for clipboard and window hints
X11 Connection
Connection wraps x11rb's RustConnection with cached screen information:
let conn = Connection::connect(None)?; // Connect to $DISPLAY
let conn = Connection::connect(Some(":1"))?; // Specific display
// Screen queries
let root = conn.root();
let width = conn.screen_width();
let height = conn.screen_height();
let visual = conn.default_visual();
let colormap = conn.default_colormap();
// ARGB visual for transparency
if let Some(argb_visual) = conn.find_argb_visual() {
let colormap = conn.create_colormap(argb_visual)?;
}
// Atom interning
let atom = conn.intern_atom("_NET_WM_NAME", false)?;
// ID generation
let window_id = conn.generate_id()?;
// Access underlying x11rb connection
conn.inner().map_window(window_id)?;
conn.flush()?; // Flush requests
conn.sync()?; // Flush and wait for reply
// Query pointer position
let (x, y) = conn.query_pointer()?;
// Non-blocking event polling
let event = conn.poll_event()?; // Returns None if no events Connection uses Arc internally and is Clone-able for sharing across threads.
Window Management
Window provides a high-level API for X11 window creation and management:
Creation
let window = Window::create(conn.clone(), config)?; // Window is automatically destroyed on Drop // Manual cleanup: window.destroy()?;
Visibility
window.map()?; // Show window.unmap()?; // Hide
Geometry
window.move_to(100, 100)?; window.resize(800, 600)?; window.set_geometry(Rect::new(100, 100, 800, 600))?; let rect = window.rect(); let size = window.size();
Stacking and Focus
window.raise()?; // Raise to top window.focus()?; // Request input focus
Properties
window.set_title("New Title")?;
let id = window.id(); // X11 Window ID
let visual = window.visual(); // Visual ID
let depth = window.depth(); // Depth (24 or 32)
let colormap = window.colormap(); // Colormap ID Input Grabbing
// For screen lockers or modal dialogs window.grab_keyboard()?; window.grab_keyboard_with_retry(5)?; // Retry up to 5 times window.grab_pointer()?; // Release grabs window.ungrab_keyboard()?; window.ungrab_pointer()?;
Dialog Windows (v0.3.0)
// Create a dialog window that belongs to a parent
let config = WindowConfig::dialog()
.title("Save changes?")
.size(400, 200);
let dialog = Window::create(conn.clone(), config)?;
// Set transient relationship (dialog belongs to parent)
dialog.set_transient_for(parent.id())?;
// Make it modal (blocks interaction with parent)
dialog.add_state(atoms.net_wm_state_modal)?;
// Clear modal state when done
dialog.clear_state(atoms.net_wm_state_modal)?; Window Activation (v0.3.0)
// Request window activation via _NET_ACTIVE_WINDOW // Proper way for applications to request focus window.activate()?; // Resize window window.set_size(800, 600)?;
Note: Window automatically registers for common event masks (Exposure, KeyPress, ButtonPress, StructureNotify, etc.) and sets WM_PROTOCOLS for proper close handling.
Event Loop
EventLoop provides a blocking event loop with FPS control and event translation:
let mut event_loop = EventLoop::new(&window, config)?;
event_loop.run(|el, event| {
match event {
InputEvent::Expose => {
// Redraw window
el.request_redraw(); // Request another redraw
}
InputEvent::Key(key_event) => {
if key_event.pressed && key_event.key == Key::Escape {
el.quit();
return Ok(false); // Stop loop
}
}
InputEvent::Resize { width, height } => {
// Handle resize
}
InputEvent::CloseRequested => {
el.quit();
return Ok(false);
}
_ => {}
}
Ok(true) // Continue loop
})?; Drag and Drop
// Enable XDND drag-and-drop support
event_loop.enable_xdnd();
// Handle file drops in the event callback
InputEvent::FileDrop(paths) => {
for path in paths {
println!("Dropped: {}", path.display());
}
} Frame Timing
EventLoop maintains target FPS by sleeping between frames. With continuous_redraw: false,
it only redraws on Expose or when request_redraw() is called.
Event Translation
EventLoop automatically translates X11 events to InputEvent:
- • KeyPress/KeyRelease → InputEvent::Key
- • ButtonPress → InputEvent::MousePress or Scroll (for scroll buttons)
- • MotionNotify → InputEvent::MouseMove
- • EnterNotify/LeaveNotify → MouseEnter/MouseLeave
- • Expose → InputEvent::Expose (also sets needs_redraw flag)
- • ConfigureNotify → InputEvent::Resize
- • ClientMessage (WM_DELETE_WINDOW) → CloseRequested
- • (v0.3.0) InputEvent::Idle - emitted once per frame when no events
- • (v0.3.0) SelectionRequest/SelectionClear - clipboard events
Motion Event Coalescing (v0.3.0)
EventLoop automatically coalesces consecutive MotionNotify events into a single MouseMove event, reducing lag during fast mouse movement. Only the final position is delivered.
Idle Events (v0.3.0)
InputEvent::Idle => {
// Emitted once per frame when there are no pending events
// Useful for animations, timers, or background tasks
animation_tick();
} Monitor Detection
RandR-based multi-monitor detection with DPI calculation:
use gartk_x11::{detect_monitors, primary_monitor, monitor_at_point};
// Detect all monitors
let monitors = detect_monitors(&conn)?;
for monitor in &monitors {
println!("{}: {}x{} @ {}x{} (DPI: {:.1})",
monitor.name,
monitor.rect.width,
monitor.rect.height,
monitor.rect.x,
monitor.rect.y,
monitor.dpi()
);
}
// Get primary monitor
let primary = primary_monitor(&conn)?;
// Find monitor at coordinates or at current pointer
let monitor = monitor_at_point(&conn, 1000, 500)?;
let monitor = monitor_at_pointer(&conn)?;
// Monitor properties
let dpi = monitor.dpi(); // Average DPI
let dpi_x = monitor.dpi_x(); // Horizontal DPI
let dpi_y = monitor.dpi_y(); // Vertical DPI
let center = monitor.center(); // Center point If RandR is unavailable or no monitors detected, falls back to screen dimensions.
Keyboard Handling
gartk-x11 provides basic keyboard handling using evdev keycodes (offset by 8):
use gartk_x11::{key_from_keycode, modifiers_from_x11, key_event_from_x11};
// Convert X11 keycode to Key enum
let modifiers = Modifiers::NONE;
let key = key_from_keycode(36, &modifiers); // 36 = Return
// Extract modifiers from X11 state
let mods = modifiers_from_x11(event.state);
// Create KeyEvent from X11 event (used internally by EventLoop)
let key_event = key_event_from_x11(&x11_key_press_event, true); Limitation: Keyboard handling uses hardcoded evdev mappings. For proper layout support (e.g., AZERTY, Dvorak), integrate xkbcommon or similar. Shift/CapsLock handling for alphabetic keys is implemented via XOR for toggling.
Cursor Management
CursorManager creates and caches X11 cursors from the standard cursor font:
use gartk_x11::{CursorManager, CursorShape};
let mut cursor_mgr = CursorManager::new(conn.clone())?;
// Get a cursor (cached after first call)
let cursor = cursor_mgr.get(CursorShape::Pointer)?;
// Set window cursor
cursor_mgr.set_window_cursor(window.id(), CursorShape::Text)?; Available Shapes
X11 Atoms
Atoms struct pre-interns commonly used EWMH and ICCCM atoms:
use gartk_x11::Atoms; let atoms = Atoms::new(&conn)?; // WM protocols atoms.wm_protocols atoms.wm_delete_window atoms.wm_take_focus // EWMH window types atoms.net_wm_window_type atoms.net_wm_window_type_normal atoms.net_wm_window_type_dialog atoms.net_wm_window_type_utility atoms.net_wm_window_type_popup_menu // EWMH state atoms.net_wm_state atoms.net_wm_state_above atoms.net_wm_state_fullscreen // Clipboard atoms.clipboard atoms.primary atoms.utf8_string atoms.text_uri_list // (v0.3.0) file:// URIs atoms.gnome_copied_files // (v0.3.0) GNOME format
Atoms is used internally by Window for setting properties. It's Clone-able via Arc.
Clipboard Manager (v0.3.0)
ClipboardManager handles X11 clipboard operations for file paths and text:
use gartk_x11::ClipboardManager;
let mut clipboard = ClipboardManager::new(conn.clone(), atoms.clone())?;
// Copy file paths (for file managers)
clipboard.copy_files(&["/home/user/file.txt"], false)?; // Copy
clipboard.copy_files(&["/home/user/file.txt"], true)?; // Cut
// Handle clipboard events in event loop
InputEvent::SelectionRequest(req) => {
clipboard.handle_selection_request(&req)?;
}
InputEvent::SelectionClear => {
clipboard.handle_selection_clear();
}
Supports GNOME (x-special/gnome-copied-files) and KDE (application/x-kde-cutselection) formats for file operations.
gartk-render
Cairo/Pango rendering layer providing high-level abstractions for 2D graphics and text. Manages Cairo image surfaces (including double-buffered), provides a Renderer combining shape and text drawing, and handles the final blit to X11 windows via PutImage.
- Surface - Cairo ImageSurface creation, double-buffered variant
- Renderer - High-level draw API combining shapes, text, and clipping
- Text - Pango-backed text rendering with style, alignment, ellipsis
- Shapes - fill/stroke for rect, rounded rect, circle, lines
Cairo Surfaces
Surface
Wrapper around Cairo ImageSurface (ARGB32 format):
let surface = Surface::new(800, 600)?; let ctx = surface.context()?; // Get Cairo context surface.clear(Color::BLACK)?; // Clear to color surface.flush(); // Flush Cairo operations // Resize (creates new surface) surface.resize(1024, 768)?; // Access raw data let data = surface.data()?; // Vec<u8> of ARGB pixels let stride = surface.stride(); // Bytes per row let w = surface.width(); // Width let h = surface.height(); // Height // Create from/export to RGBA pixel data let surface = Surface::from_rgba(&rgba_bytes, width, height)?; let rgba_bytes = surface.to_rgba()?;
DoubleBufferedSurface
Flicker-free rendering with front/back buffers:
let mut db_surface = DoubleBufferedSurface::new(800, 600)?; // Draw to back buffer let back = db_surface.back(); let ctx = back.context()?; // ... render ... // Swap buffers (copies back to front) db_surface.swap()?; // Display front buffer let front = db_surface.front(); copy_surface_to_window(front, &window, gc, 0, 0)?;
Copy to Window
Helper function to copy surface to X11 window via PutImage:
use gartk_render::copy_surface_to_window; // Create graphics context let gc = conn.generate_id()?; conn.inner().create_gc(gc, window.id(), &Default::default())?; // Copy surface copy_surface_to_window(&mut surface, &window, gc, 0, 0)?;
High-Level Renderer
Renderer combines Surface, shape primitives, and TextRenderer:
let renderer = Renderer::new(800, 600)?;
let renderer = Renderer::with_theme(800, 600, Theme::dark())?;
// Clear
renderer.clear()?; // Uses theme background
renderer.clear_color(Color::from_hex("#1e1e2e")?)?;
// Shapes
renderer.fill_rect(Rect::new(10, 10, 100, 50), Color::RED)?;
renderer.fill_rounded_rect(
Rect::new(10, 70, 100, 50),
8.0, // radius
Color::BLUE,
)?;
renderer.stroke_rect(
Rect::new(10, 130, 100, 50),
Color::GREEN,
2.0, // line width
)?;
renderer.fill_circle(200.0, 100.0, 30.0, Color::YELLOW)?;
renderer.line(50.0, 200.0, 150.0, 250.0, Color::CYAN, 1.0)?;
// Text
let style = TextStyle::new()
.font_size(16.0)
.color(Color::WHITE);
renderer.text("Hello", 10.0, 300.0, &style)?;
renderer.text_centered("Centered", Point::new(400, 400), &style)?;
renderer.text_in_rect("In Rect", Rect::new(10, 450, 200, 50), &style)?;
// Measure text
let size = renderer.measure_text("Test", &style)?;
// Access internals
let ctx = renderer.context()?; // Cairo context
let surface = renderer.surface();
let theme = renderer.theme();
renderer.flush(); Text Rendering
Pango-based text rendering with layout support:
let text_renderer = TextRenderer::new(); let text_renderer = TextRenderer::with_style(default_style); let ctx = surface.context()?; // Draw text text_renderer.draw(&ctx, "Text", 10.0, 10.0, &style); // Draw in rectangle (vertical centering) text_renderer.draw_in_rect(&ctx, "Text", rect, &style); // Draw centered at point text_renderer.draw_centered(&ctx, "Text", center_point, &style); // Measure let size = text_renderer.measure(&ctx, "Text", &style); // Create Pango layout directly let layout = text_renderer.create_layout(&ctx, "Text", &style);
TextStyle Options
- •
align: Left, Center, Right (Pango alignment) - •
ellipsize: Add "..." if text exceeds max_width - •
wrap: Wrap lines at word boundaries - •
max_width: Pixel width constraint (enables ellipsize/wrap)
Shape Primitives
Low-level shape drawing functions operating on Cairo Context:
use gartk_render::{fill_rect, fill_rounded_rect, fill_circle};
use gartk_render::{stroke_rect, stroke_rounded_rect, stroke_circle};
use gartk_render::{line, hline, vline, set_color};
let ctx = surface.context()?;
// Filled shapes
fill_rect(&ctx, rect, color);
fill_rounded_rect(&ctx, rect, radius, color);
fill_circle(&ctx, cx, cy, radius, color);
// Stroked shapes
stroke_rect(&ctx, rect, color, line_width);
stroke_rounded_rect(&ctx, rect, radius, color, line_width);
stroke_circle(&ctx, cx, cy, radius, color, line_width);
// Lines
line(&ctx, x1, y1, x2, y2, color, line_width);
hline(&ctx, y, x1, x2, color, line_width);
vline(&ctx, x, y1, y2, color, line_width);
// Path creation (no fill/stroke)
rect_path(&ctx, rect);
rounded_rect_path(&ctx, rect, radius);
circle_path(&ctx, cx, cy, radius);
// Set color
set_color(&ctx, color);
ctx.fill()?; These primitives are also available through the high-level Renderer API.
Integration
Patterns and techniques for building gardesk applications with gartk. Covers the standard application setup, transparency support for composited environments, and double-buffered rendering to eliminate flicker.
Common Patterns
Complete Application
use gartk_core::{Color, InputEvent, Key, Theme};
use gartk_render::Renderer;
use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig};
fn main() -> anyhow::Result<()> {
// Setup
let conn = Connection::connect(None)?;
let window = Window::create(conn.clone(), WindowConfig::new()
.title("App")
.size(800, 600))?;
let theme = Theme::dark();
let mut renderer = Renderer::with_theme(800, 600, theme)?;
// Graphics context for copying
let gc = conn.generate_id()?;
conn.inner().create_gc(gc, window.id(), &Default::default())?;
// Event loop
let mut event_loop = EventLoop::new(&window, EventLoopConfig::default())?;
event_loop.run(|el, event| {
match event {
InputEvent::Expose => {
renderer.clear()?;
// ... draw ...
renderer.flush();
copy_surface_to_window(
&mut renderer.surface().clone(),
&window,
gc,
0, 0
)?;
}
InputEvent::Key(k) if k.key == Key::Escape => {
el.quit();
return Ok(false);
}
_ => {}
}
Ok(true)
})?;
Ok(())
} Popup Window
// Override-redirect window for menus/launchers
let window = Window::create(conn.clone(), WindowConfig::popup()
.position(100, 100)
.size(400, 300)
.transparent(true))?;
window.raise()?; // Ensure on top
window.focus()?; // Grab focus Monitor-Aware Positioning
use gartk_x11::primary_monitor;
let monitor = primary_monitor(&conn)?;
let center = monitor.center();
// Center on primary monitor
let window_size = Size::new(400, 300);
let pos = Point::new(
center.x - window_size.width as i32 / 2,
center.y - window_size.height as i32 / 2,
);
let window = Window::create(conn.clone(), WindowConfig::new()
.position(pos.x, pos.y)
.size(window_size.width, window_size.height))?; Transparency Support
ARGB transparency requires a compositor (picom, compton, xcompmgr):
// Request ARGB visual
let window = Window::create(conn.clone(), WindowConfig::popup()
.transparent(true) // Finds 32-bit ARGB visual
.size(400, 300))?;
// Window will have depth 32 if ARGB visual found
if window.depth() == 32 {
println!("Transparency supported");
}
// Render with alpha channel
let renderer = Renderer::new(400, 300)?;
renderer.clear_color(Color::from_hex("#1e1e2e80")?)?; // 50% transparent
renderer.fill_rounded_rect(
Rect::new(10, 10, 380, 280),
8.0,
Color::from_hex("#313244e0")?, // Semi-transparent
)?; Note: If no ARGB visual is found (no compositor or unsupported), gartk falls back to default 24-bit visual. Alpha channel is ignored in this case.
Double Buffering
Use DoubleBufferedSurface to eliminate flicker:
let mut db_surface = DoubleBufferedSurface::new(800, 600)?;
event_loop.run(|el, event| {
match event {
InputEvent::Expose | InputEvent::Resize { .. } => {
// Draw to back buffer
let back = db_surface.back();
let ctx = back.context()?;
// Clear
ctx.set_source_rgba(0.12, 0.12, 0.18, 1.0);
ctx.set_operator(cairo::Operator::Source);
ctx.paint()?;
ctx.set_operator(cairo::Operator::Over);
// Render content
// ...
// Swap buffers (copy back to front)
db_surface.swap()?;
// Copy front buffer to window
let front = db_surface.front();
copy_surface_to_window(front, &window, gc, 0, 0)?;
}
_ => {}
}
Ok(true)
})?; Workflows
Building a Custom Launcher
Example of building a simple application launcher with fuzzy search:
use gartk_core::{Color, InputEvent, Key, Rect, Theme};
use gartk_render::{Renderer, TextStyle};
use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig};
struct Launcher {
query: String,
selected: usize,
items: Vec<String>,
}
impl Launcher {
fn new() -> Self {
Self {
query: String::new(),
selected: 0,
items: vec![
"Firefox".into(),
"Terminal".into(),
"Files".into(),
],
}
}
fn handle_key(&mut self, key: Key) -> bool {
match key {
Key::Escape => return false,
Key::Return => {
// Launch selected item
println!("Launch: {}", self.items[self.selected]);
return false;
}
Key::Up if self.selected > 0 => self.selected -= 1,
Key::Down if self.selected < self.items.len() - 1 => self.selected += 1,
Key::Backspace => { self.query.pop(); }
Key::Char(c) | Key::Space if c == ' ' => {
self.query.push(c);
}
_ => {}
}
true
}
fn render(&self, renderer: &Renderer, theme: &Theme) -> anyhow::Result<()> {
renderer.clear()?;
// Input box
let input_rect = Rect::new(10, 10, 380, 40);
renderer.fill_rounded_rect(input_rect, 8.0, theme.input_background)?;
let input_style = TextStyle::new()
.font_size(16.0)
.color(theme.input_foreground);
renderer.text(&self.query, 20.0, 25.0, &input_style)?;
// Items
let mut y = 60;
for (i, item) in self.items.iter().enumerate() {
let item_rect = Rect::new(10, y, 380, 30);
if i == self.selected {
renderer.fill_rect(item_rect, theme.item_selected_background)?;
}
let item_style = TextStyle::new()
.font_size(14.0)
.color(if i == self.selected {
theme.item_selected_foreground
} else {
theme.item_foreground
});
renderer.text(item, 20.0, y as f64 + 8.0, &item_style)?;
y += 35;
}
renderer.flush();
Ok(())
}
}
fn main() -> anyhow::Result<()> {
let conn = Connection::connect(None)?;
let window = Window::create(
conn.clone(),
WindowConfig::popup()
.title("Launcher")
.size(400, 300)
.centered(),
)?;
let theme = Theme::dark();
let renderer = Renderer::with_theme(400, 300, theme.clone())?;
let mut launcher = Launcher::new();
let gc = conn.generate_id()?;
conn.inner().create_gc(gc, window.id(), &Default::default())?;
let mut event_loop = EventLoop::new(&window, EventLoopConfig::default())?;
event_loop.run(|el, event| {
match event {
InputEvent::Expose => {
launcher.render(&renderer, &theme)?;
let mut surface_clone = renderer.surface().clone();
gartk_render::copy_surface_to_window(
&mut surface_clone,
&window,
gc,
0, 0
)?;
}
InputEvent::Key(key_event) if key_event.pressed => {
if !launcher.handle_key(key_event.key) {
el.quit();
return Ok(false);
}
el.request_redraw();
}
_ => {}
}
Ok(true)
})?;
Ok(())
} Creating a Status Widget
Example of a persistent status widget with transparency:
use gartk_core::{Color, InputEvent, Rect};
use gartk_render::{Renderer, TextStyle};
use gartk_x11::{Connection, EventLoop, EventLoopConfig, Window, WindowConfig, WindowType};
use std::time::{Duration, Instant};
struct StatusWidget {
last_update: Instant,
text: String,
}
impl StatusWidget {
fn new() -> Self {
Self {
last_update: Instant::now(),
text: "Status".into(),
}
}
fn update(&mut self) {
if self.last_update.elapsed() > Duration::from_secs(1) {
self.text = format!("Time: {}", chrono::Local::now().format("%H:%M:%S"));
self.last_update = Instant::now();
}
}
fn render(&self, renderer: &Renderer) -> anyhow::Result<()> {
// Transparent background
renderer.clear_color(Color::from_hex("#00000000")?)?;
// Semi-transparent panel
renderer.fill_rounded_rect(
Rect::new(5, 5, 190, 40),
8.0,
Color::from_hex("#1e1e2ee0")?,
)?;
let style = TextStyle::new()
.font_size(14.0)
.color(Color::from_hex("#cdd6f4")?);
renderer.text(&self.text, 15.0, 20.0, &style)?;
renderer.flush();
Ok(())
}
}
fn main() -> anyhow::Result<()> {
let conn = Connection::connect(None)?;
// Utility window, always on top
let window = Window::create(
conn.clone(),
WindowConfig::new()
.title("Widget")
.window_type(WindowType::Utility)
.position(10, 10)
.size(200, 50)
.transparent(true),
)?;
window.raise()?;
let renderer = Renderer::new(200, 50)?;
let mut widget = StatusWidget::new();
let gc = conn.generate_id()?;
conn.inner().create_gc(gc, window.id(), &Default::default())?;
// Continuous redraw for updates
let config = EventLoopConfig {
fps: 60,
continuous_redraw: true,
};
let mut event_loop = EventLoop::new(&window, config)?;
event_loop.run(|el, event| {
match event {
InputEvent::Expose => {
widget.update();
widget.render(&renderer)?;
let mut surface = renderer.surface().clone();
gartk_render::copy_surface_to_window(&mut surface, &window, gc, 0, 0)?;
}
InputEvent::CloseRequested => {
el.quit();
return Ok(false);
}
_ => {}
}
Ok(true)
})?;
Ok(())
} Multi-Monitor Layout
use gartk_x11::{Connection, detect_monitors};
fn main() -> anyhow::Result<()> {
let conn = Connection::connect(None)?;
let monitors = detect_monitors(&conn)?;
println!("Detected {} monitors:", monitors.len());
for (i, monitor) in monitors.iter().enumerate() {
println!(" [{}] {}", i, monitor.name);
println!(" Position: {},{}",monitor.rect.x, monitor.rect.y);
println!(" Size: {}x{}", monitor.rect.width, monitor.rect.height);
println!(" DPI: {:.1}", monitor.dpi());
println!(" Primary: {}", monitor.primary);
}
// Create window on each monitor
for monitor in &monitors {
let pos = monitor.center();
// ... create window at pos ...
}
Ok(())
} 💡 Integration Examples
See garlaunch source for a real-world example of gartk usage:
- • Fuzzy search UI with item list rendering
- • Input field with cursor blinking
- • Keyboard navigation and text input
- • Monitor-aware window positioning
- • Theme integration
Troubleshooting
Cannot connect to X11 display
Symptom: ConnectionFailed error
Solutions:
Transparency not working
Symptom: Windows appear opaque despite using ARGB colors
Causes:
- • No compositor running (picom, compton, xcompmgr)
- • No ARGB visual available (check
window.depth() == 32)
Solutions:
Window flickers during rendering
Symptom: Visible tearing or flashing during redraws
Solution: Use DoubleBufferedSurface instead of Surface
// Before (flickers) let surface = Surface::new(800, 600)?; // After (smooth) let db_surface = DoubleBufferedSurface::new(800, 600)?; // Draw to db_surface.back() // Then db_surface.swap()?
Keyboard input not working correctly
Symptom: Wrong characters or missing keys on non-QWERTY layouts
Cause: gartk uses hardcoded evdev keymaps (QWERTY-centric)
Workaround: For proper layout support, integrate xkbcommon:
# Add xkbcommon to Cargo.toml [dependencies] xkbcommon = "0.7" # Use xkb state to map keycodes to keysyms # See xkbcommon docs for integration details
Text rendering looks blurry or wrong size
Symptoms:
- • Text appears too small or large
- • Blurry text on high-DPI displays
Solutions:
- • Scale font size based on monitor DPI:
let monitor = primary_monitor(&conn)?; let scale = monitor.dpi() / 96.0; // 96 is baseline DPI let font_size = 14.0 * scale; let style = TextStyle::new().font_size(font_size);
RandR monitor detection fails
Symptom: RandRNotAvailable error or empty monitor list
Solutions:
Fallback: gartk automatically falls back to screen dimensions if RandR unavailable
Cursor not changing
Symptom: CursorManager::set_window_cursor has no effect
Cause: Cursor font not available or WM overriding cursor
Solutions:
- • Ensure cursor font is installed (usually in xorg-fonts-misc)
- • For override-redirect windows, cursor should work
- • For normal windows, WM may set its own cursor
Compilation errors with Cairo/Pango
Symptom: Linker errors or missing -lcairo, -lpango
Cause: Missing system libraries
Solutions:
Event loop high CPU usage
Symptom: 100% CPU usage during event loop
Cause: Continuous redraw mode with high FPS
Solution: Adjust EventLoopConfig:
// Low CPU (redraw only on events)
let config = EventLoopConfig {
fps: 60,
continuous_redraw: false, // Default
};
// Or lower FPS if continuous redraw needed
let config = EventLoopConfig {
fps: 30,
continuous_redraw: true,
}; 📋 Debug Checklist
For general gartk issues, verify:
- • X11 is running and DISPLAY is set
- • All system dependencies installed (libx11, cairo, pango)
- • Using latest gartk from trunk branch
- • Enable RUST_LOG=debug for detailed traces
- • Test in Xephyr for isolated environment
Releases
gartk is a library crate - no binary RPMs available. Install via cargo or build from source.