| Crates.io | egui-desktop |
| lib.rs | egui-desktop |
| version | 0.2.1 |
| created_at | 2025-09-30 03:17:49.425315+00 |
| updated_at | 2025-12-30 11:10:56.220087+00 |
| description | Cross-platform desktop UI components for egui applications |
| homepage | https://github.com/PxlSyl/egui-desktop |
| repository | https://github.com/PxlSyl/egui-desktop |
| max_upload_size | |
| id | 1860517 |
| size | 2,473,549 |
A comprehensive desktop UI framework for egui applications with native window decorations, advanced theming, and cross-platform desktop integration.
This project is currently in alpha version and under active development. It may not be fully tested on all platforms. While we strive for cross-platform compatibility, some features may behave differently or have limitations on certain operating systems. Please test thoroughly in your target environments before using in production applications.
Known limitations:
We welcome feedback, bug reports, and contributions to help improve platform compatibility! Feel free to open pull requests or report issues - your input helps make this framework better for everyone.
As a developer who uses this framework to build my own desktop applications, I'm passionate about rust and egui and want to make desktop development easier for Rust programmers. This framework addresses the common pain points when building native-feeling desktop apps with egui and custom title bars:
Not WebAssembly-oriented, but designed to give egui desktop apps professional look and features (custom title bar, menus, system icons, Windows/macOS/Linux themes).
The goal is to provide a solid foundation so you can focus on building your application logic instead of wrestling with platform-specific UI details.
The easiest way to get started is using our CLI tool that generates a complete starter project:
# Install the CLI globally from crates.io
cargo install egui-desktop-cli
# Or install from local development:
# cargo install --path cli
# Generate a new project
egui-desktop my-super-project
# This creates a complete project with:
# - Modular structure (main.rs, app.rs, theme_provider.rs, etc.)
# - All dependencies configured
# - Ready-to-run example with themes, sidebar, and custom UI
The CLI generates a professional project structure with:
For more details about the CLI and generated project structure, see the CLI README.
use egui_desktop::{TitleBar, apply_rounded_corners, render_resize_handles};
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
// Apply native rounded corners (call once)
apply_rounded_corners(frame);
// Render title bar with default light theme
TitleBar::new("My App").show(ctx);
// Add manual resize handles
render_resize_handles(ctx);
// Your app content here
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Hello World!");
});
}
}
The framework completely replaces native window control buttons with custom egui-drawn buttons:
// Custom control button colors
let title_bar = TitleBar::new("My App")
.with_custom_theme(TitleBarTheme {
close_hover_color: egui::Color32::RED, // Red close button on hover
minimize_icon_color: egui::Color32::BLUE, // Blue minimize icon
maximize_icon_color: egui::Color32::GREEN, // Green maximize icon
restore_icon_color: egui::Color32::YELLOW, // Yellow restore icon
..Default::default()
});
use egui_desktop::{TitleBar, ThemeMode, TitleBarTheme};
// Light theme
TitleBar::new("My App")
.with_theme_mode(ThemeMode::Light)
.show(ctx);
// Dark theme
TitleBar::new("My App")
.with_theme_mode(ThemeMode::Dark)
.show(ctx);
// System theme (follows OS)
TitleBar::new("My App")
.with_theme_mode(ThemeMode::System)
.sync_with_system_theme()
.show(ctx);
// Custom theme
TitleBar::new("My App")
.with_theme(TitleBarTheme {
background_color: Color32::from_rgb(45, 45, 65),
hover_color: Color32::from_rgb(65, 65, 85),
close_hover_color: Color32::from_rgb(220, 20, 40),
close_icon_color: Color32::from_rgb(180, 180, 180),
title_color: Color32::from_rgb(200, 200, 255),
})
.show(ctx);
Add custom icons to the title bar with optional keyboard shortcuts:
use egui_desktop::{TitleBar, CustomIcon, KeyboardShortcut};
TitleBar::new("My App")
// Custom app icon (supports SVG, PNG, JPEG, etc.)
.with_custom_app_icon(include_bytes!("icon.svg"), "app-icon.svg")
// Add custom icon with callback and tooltip
.add_icon(
CustomIcon::Image(ImageSource::from_bytes("settings.svg", include_bytes!("settings.svg"))),
Some(Box::new(|| println!("Settings clicked!"))),
Some("Settings".to_string()),
None // No keyboard shortcut
)
// Add custom icon with keyboard shortcut
.add_icon(
CustomIcon::Drawn(Box::new(|painter, rect, color| {
// Custom drawing code for notification bell
painter.circle_filled(rect.center(), rect.width() * 0.4, color);
})),
Some(Box::new(|| println!("Notifications clicked!"))),
Some("Notifications".to_string()),
Some(KeyboardShortcut::parse("ctrl+n")) // Ctrl+N shortcut
)
.show(ctx);
// Don't forget to handle shortcuts in your app's update loop
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.title_bar.handle_icon_shortcuts(ctx);
// ... rest of your app logic
}
}
CustomIcon::Image() with SVG, PNG, JPEG, etc.CustomIcon::Drawn() with custom drawing functionsIcons can have keyboard shortcuts that trigger their callbacks:
KeyboardShortcut::parse() for simple string-based shortcutshandle_icon_shortcuts(ctx) in your app's update loopSee examples/multi_window.rs for a complete egui 0.32 / eframe sample that opens additional native windows (viewports) with their own TitleBar instances:
cargo run --example multi_window
Highlights of the example:
TitleBar objects per window (main, Settings, About) with different button sets.ctx.show_viewport_deferred(...) so they are actual OS-level windows, not embedded panels.Arc<Mutex<...>>, ensuring every window sees the same data.ViewportBuilder::with_position with ctx.input(|i| i.viewport().inner_rect).ViewportCommand::Close back to eframe.Use this example as a starting point when you need multiple windows that stay visually consistent with egui-desktopβs desktop chrome.
use egui_desktop::{CustomIcon, ImageSource};
// Add custom icons to the title bar (automatically positioned by platform)
TitleBar::new("My App")
// Image-based icon (SVG, PNG, JPEG, etc.)
.add_icon(
CustomIcon::Image(ImageSource::from_bytes("settings.svg", include_bytes!("settings.svg"))),
Some(Box::new(|| println!("Settings clicked!")))
)
// Custom drawn icon
.add_icon(
CustomIcon::Drawn(Box::new(|painter, rect, color| {
// Draw a custom notification bell
let center = rect.center();
let radius = rect.width().min(rect.height()) * 0.4;
painter.circle_stroke(center, radius, egui::Stroke::new(2.0, color));
})),
Some(Box::new(|| println!("Notifications!")))
)
.show(ctx);
You can add animated icons that the framework will drive every frame with timing, hover/press state, and theme colors.
API overview:
CustomIcon::Animated(Box<dyn Fn(&Painter, Rect, Color32, &mut IconAnimationState, AnimationCtx) + Send + Sync>)CustomIcon::AnimatedUi(Box<dyn Fn(&mut Ui, Rect, Color32, &mut IconAnimationState, AnimationCtx) + Send + Sync>)TitleBar::add_animated_icon(...) and TitleBar::add_animated_ui_icon(...)IconAnimationState { hover_t, press_t, progress, last_time }AnimationCtx { time, delta_seconds, hovered, pressed }Notes:
TitleBar persistent (store it in your app struct) so per-icon animation state is preserved across frames.icon_color; hover backgrounds use the title bar theme. Theme changes update these automatically.set_custom_icon_color(index, Some(color)); pass None to return to theme-driven color.Painter-based example (minimal):
use egui_desktop::{TitleBar, TitleBarOptions};
// Build once and store in your app struct
let mut title_bar = TitleBar::new(TitleBarOptions::new().with_title("Animated"));
title_bar = title_bar.add_animated_icon(
Box::new(|painter, rect, icon_color, state, actx| {
// Simple pulse driven by hover
let target = if actx.hovered { 1.0 } else { 0.0 };
state.progress += (target - state.progress) * (actx.delta_seconds * 6.0);
let r = rect.width().min(rect.height()) * (0.25 + 0.15 * state.progress);
painter.circle_filled(rect.center(), r, icon_color);
}),
Some(Box::new(|| println!("Animated icon clicked"))),
Some("Animated".to_string()),
None,
);
Ui-based example (no Painter required) β sunβmoon style:
title_bar = title_bar.add_animated_ui_icon(
Box::new(|ui, rect, icon_color, state, actx| {
let speed = 6.0;
let target = if actx.hovered { 1.0 } else { 0.0 };
state.progress += (target - state.progress) * (actx.delta_seconds * speed);
let mut child = ui.child_ui(rect, egui::Layout::default(), None);
let center = rect.center();
let size = rect.height().min(rect.width());
let radius = size * 0.35;
if state.progress < 0.5 {
let sun_p = 1.0 - (state.progress * 2.0);
child.painter().circle(center, radius * 0.8 * sun_p, icon_color, egui::Stroke::NONE);
} else {
let moon_p = (state.progress - 0.5) * 2.0;
child.painter().circle(center, radius, icon_color, egui::Stroke::NONE);
let offset = radius * 0.6 * moon_p;
let angle = std::f32::consts::FRAC_PI_4;
let mask_center = egui::Pos2::new(center.x + angle.cos() * offset, center.y - angle.sin() * offset);
child.painter().circle(mask_center, radius, ui.visuals().widgets.noninteractive.bg_fill, egui::Stroke::NONE);
}
}),
None,
Some("Theme".to_string()),
None,
);
Theme-aware coloring:
title_bar.set_custom_icon_color(0, None); // use theme color
// Or override:
title_bar.set_custom_icon_color(0, Some(egui::Color32::from_rgb(255, 200, 0)));
Platform-specific positioning:
TitleBar::new("My App")
.add_menu_item("File", Some(Box::new(|| println!("File clicked"))))
.add_menu_item("Edit", Some(Box::new(|| println!("Edit clicked"))))
.add_menu_item("Help", Some(Box::new(|| println!("Help clicked"))))
.show(ctx);
use egui_desktop::{TitleBar, MenuItem, SubMenuItem, KeyboardShortcut};
// Create complex menu structures with submenus and shortcuts
let file_menu = MenuItem::new("File")
.add_subitem(
SubMenuItem::new("New")
.with_shortcut(KeyboardShortcut::parse("ctrl+n"))
.with_callback(Box::new(|| println!("New file!")))
)
.add_subitem(
SubMenuItem::new("Open")
.with_shortcut(KeyboardShortcut::parse("ctrl+o"))
.with_callback(Box::new(|| println!("Open file!")))
)
.add_subitem(
SubMenuItem::new("Save")
.with_shortcut(KeyboardShortcut::parse("ctrl+s"))
.with_callback(Box::new(|| println!("Save file!")))
.with_separator() // Add separator after this item
)
.add_subitem(
SubMenuItem::new("Exit")
.with_shortcut(KeyboardShortcut::parse("ctrl+q"))
.with_callback(Box::new(|| std::process::exit(0)))
);
// Add submenus with nested sidemenus (cascading menus)
let edit_menu = MenuItem::new("Edit")
.add_subitem(
SubMenuItem::new("Undo")
.with_shortcut(KeyboardShortcut::parse("ctrl+z"))
.with_callback(Box::new(|| println!("Undo!")))
)
.add_subitem(
SubMenuItem::new("Cut")
.with_shortcut(KeyboardShortcut::parse("ctrl+x"))
.with_callback(Box::new(|| println!("Cut!")))
)
.add_subitem(
SubMenuItem::new("Copy")
.with_shortcut(KeyboardShortcut::parse("ctrl+c"))
.with_callback(Box::new(|| println!("Copy!")))
)
.add_subitem(
SubMenuItem::new("Paste")
.with_shortcut(KeyboardShortcut::parse("ctrl+v"))
.with_callback(Box::new(|| println!("Paste!")))
.with_separator()
)
.add_subitem(
SubMenuItem::new("Find")
.with_shortcut(KeyboardShortcut::parse("ctrl+f"))
.with_callback(Box::new(|| println!("Find!")))
.add_child_subitem(
SubMenuItem::new("Find Next")
.with_shortcut(KeyboardShortcut::parse("f3"))
.with_callback(Box::new(|| println!("Find next!")))
)
.add_child_subitem(
SubMenuItem::new("Find Previous")
.with_shortcut(KeyboardShortcut::parse("shift+f3"))
.with_callback(Box::new(|| println!("Find previous!")))
)
);
TitleBar::new("My App")
.add_menu_with_submenu(file_menu)
.add_menu_with_submenu(edit_menu)
.show(ctx);
The framework provides comprehensive keyboard navigation that follows platform standards:
The navigation system intelligently handles different contexts:
The keyboard navigation follows platform conventions:
The framework supports simple, string-based keyboard shortcuts:
use egui_desktop::KeyboardShortcut;
// Simple shortcuts with string parsing
KeyboardShortcut::parse("s") // Single key
KeyboardShortcut::parse("ctrl+s") // Ctrl+S
KeyboardShortcut::parse("alt+s") // Alt+S
KeyboardShortcut::parse("shift+s") // Shift+S
KeyboardShortcut::parse("cmd+s") // Cmd+S (macOS)
// Complex combinations
KeyboardShortcut::parse("f3") // F3
KeyboardShortcut::parse("shift+f3") // Shift+F3
KeyboardShortcut::parse("ctrl+shift+f3") // Ctrl+Shift+F3
// Special keys
KeyboardShortcut::parse("enter") // Enter
KeyboardShortcut::parse("space") // Space
KeyboardShortcut::parse("escape") // Escape
KeyboardShortcut::parse("tab") // Tab
KeyboardShortcut::parse("delete") // Delete
// Numbers and punctuation
KeyboardShortcut::parse("1") // Number 1
KeyboardShortcut::parse("ctrl+=") // Ctrl+=
KeyboardShortcut::parse("ctrl+-") // Ctrl+-
Supported modifiers: ctrl, alt, shift, cmd (macOS)
Supported keys: All letters (a-z), numbers (0-9), function keys (f1-f12), special keys, and punctuation.
use egui_desktop::{TitleBar, TitleBarOptions};
// Control title visibility per platform
TitleBar::new(
TitleBarOptions::new()
.with_title("My App")
.with_title_visibility(
false, // macOS: hide title (follows macOS conventions)
true, // Windows: show title
true, // Linux: show title
)
)
.show(ctx);
// Or use the new unified API
TitleBar::new(
TitleBarOptions::new()
.with_title("My App")
.with_title_visibility(true, true, false)
)
.show(ctx);
TitleBar::new(
TitleBarOptions::new()
.with_title("My App")
.with_background_color(Color32::from_rgb(30, 30, 30))
.with_hover_color(Color32::from_rgb(60, 60, 60))
.with_close_hover_color(Color32::from_rgb(232, 17, 35))
.with_title_color(Color32::from_rgb(200, 200, 200))
.with_title_font_size(14.0)
.with_title_visibility(true, true, false) // Platform-specific title visibility
.with_custom_app_icon(include_bytes!("icon.svg"), "app-icon.svg")
)
.show(ctx);
You can customize the highlight color for keyboard navigation:
TitleBar::new(
TitleBarOptions::new()
.with_title("My App")
.with_keyboard_selection_color(Color32::from_rgba_premultiplied(255, 100, 100, 120)) // Custom red highlight
)
.show(ctx);
Default colors:
rgb(0, 120, 215)) - Universal blue that works everywherergb(30, 144, 255)) - Brighter blue for dark backgroundsThe easiest way to see all features in action is to generate a complete project:
# Install CLI from crates.io (if not already installed)
cargo install egui-desktop-cli
# Generate and run demo project
egui-desktop mon-demo-projet
cd mon-demo-projet
cargo run
This creates a fully-featured demo with:
| Example | Description |
|---|---|
basic_app.rs |
Simple app with default light theme and title bar |
custom_title_bar.rs |
Customized title bar with dark theme and menu items |
multi_platform.rs |
Cross-platform demo showing OS-specific features |
no_title_app.rs |
Title bar without title text (macOS: traffic lights only, Windows/Linux: icon + controls) |
To test the keyboard navigation features:
cargo run --example theme_demoAlt (Windows/Linux) or Ctrl+F2 (macOS)Right arrow on items with submenusEnter or Space to activate menu itemsEscape or click outside the menu areaRun examples with:
cargo run --example basic_app
cargo run --example theme_demo
cargo run --example custom_title_bar
The framework automatically detects system themes on:
AppsUseLightThemedefaults read -g AppleInterfaceStylegsettings (GNOME) or GTK_THEME environment variablewith_theme_mode(mode) - Set theme mode (Light/Dark/System)with_theme(theme) - Use custom themesync_with_egui_theme(ctx) - Sync with egui's themesync_with_system_theme() - Sync with system themeControl whether the app title is displayed on each platform:
with_title_visibility(macos, windows, linux) - Set visibility per platformDwmSetWindowAttribute APINSWindow layer corner radiusInteractive resize handles around window edges and corners:
ViewportCommand::BeginResize to eguiThe framework uses minimal dependencies:
egui - UI frameworkegui_extras - Image loaders and utilitiesraw-window-handle - Window handle abstractionlazy_static - Global state management for keyboard shortcutsPlatform-specific dependencies are included automatically:
windows crate for native APIscocoa and objc for native APIsx11, wayland-client for native APIslazy_staticThe framework uses lazy_static to manage global state for keyboard shortcuts, ensuring proper "just pressed" detection across the entire application. This is essential for reliable keyboard shortcut handling.
Why lazy_static is needed:
key_pressed() method returns true for the entire duration a key is held, not just on the first pressImplementation details:
lazy_static! {
static ref SHORTCUT_STATES: Mutex<HashMap<String, bool>> = Mutex::new(HashMap::new());
}
This global state allows the framework to:
Benefits:
Cargo.toml:[dependencies]
egui-desktop = "0.1.0"
egui_extras = { version = "0.32", features = ["all_loaders"] }
eframe = "0.32"
egui = "0.32"
use egui_extras::install_image_loaders;
fn main() -> Result<(), eframe::Error> {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_decorations(false), // Disable native decorations
..Default::default()
};
eframe::run_native(
"My App",
options,
Box::new(|cc| {
install_image_loaders(&cc.egui_ctx); // Enable image loading
Ok(Box::new(MyApp::default()))
}),
)
}
apply_native_rounded_corners_to_window once in your update loopThemeMode::System for the best user experienceContributions are welcome! Please feel free to submit pull requests or open issues for bugs and feature requests.
Particularly needed:
We'd love to see Linux users contribute their expertise to make this framework work great on all Linux distributions and desktop environments!
MIT License - see LICENSE file for details.