diff --git a/src/main.rs b/src/main.rs index bd1b419..085ae11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,17 @@ fn main() { // Create and show the GTK UI let ui = GtkLauncherUI::new(app, launcher); + + // Apply built-in default styles + if let Err(e) = ui.apply_default_css() { + eprintln!("Warning: Could not apply default styles: {}", e); + } + + // Optionally apply user CSS overrides + // if let Err(_) = ui.apply_css("waycast.css") { + // // Silently ignore if user hasn't provided custom CSS + // } + ui.show(); }); diff --git a/src/ui/gtk/default.css b/src/ui/gtk/default.css new file mode 100644 index 0000000..5db1283 --- /dev/null +++ b/src/ui/gtk/default.css @@ -0,0 +1,111 @@ +/* Default CSS styles for Waycast launcher */ +/* Not too much so that colors follow system theme */ + +/* Main window */ +#launcher-window { + /* background-color: rgba(0, 0, 0, 0.9); + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); */ +} + +/* Main container */ +#main-container { + padding: 16px; +} + +/* Search input */ +#search-input { + font-size: 16px; + padding: 12px; + border-radius: 8px; + margin-bottom: 12px; + /* border: 1px solid rgba(255, 255, 255, 0.2); + background-color: rgba(255, 255, 255, 0.1); + color: white; */ +} + +#search-input:focus { + /* background-color: rgba(255, 255, 255, 0.15); + border-color: rgba(74, 144, 226, 0.6); */ + box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.3); +} + +/* Results container */ +#results-container { + border-radius: 8px; + /* background-color: rgba(255, 255, 255, 0.05); */ +} + +/* Results list */ +#results-list { + /* background-color: transparent; */ +} + +/* Individual list items */ +#list-item { + padding: 8px 12px; + border-radius: 6px; + margin: 2px; +} + +#list-item:hover { + /* background-color: rgba(255, 255, 255, 0.1); */ +} + +#list-item:selected { + /* background-color: rgba(74, 144, 226, 0.4); */ +} + +/* Item icon */ +#item-icon { + margin-right: 12px; +} + +/* Item text container */ +#item-text { + /* Text styling handled by individual elements */ +} + +/* Item title */ +#item-title { + font-weight: 600; + font-size: 14px; + /* color: white; */ +} + +/* Item description */ +#item-description { + font-size: 11px; + /* color: rgba(255, 255, 255, 0.7); */ + margin-top: 2px; +} + +/* CSS Classes (alternative to IDs) */ + +.launcher-window { + /* Alternative window styling */ +} + +.launcher-search { + /* Alternative search styling */ +} + +.launcher-item { + /* Alternative item styling */ +} + +.launcher-item:hover { + /* Alternative hover state */ +} + +.launcher-title { + /* Alternative title styling */ +} + +.launcher-description { + /* Alternative description styling */ +} + +.launcher-icon { + /* Alternative icon styling */ +} \ No newline at end of file diff --git a/src/ui/gtk/mod.rs b/src/ui/gtk/mod.rs index 3d1f62c..83733d5 100644 --- a/src/ui/gtk/mod.rs +++ b/src/ui/gtk/mod.rs @@ -10,7 +10,7 @@ use gio::ListStore; use gtk::subclass::prelude::ObjectSubclassIsExt; use gtk4_layer_shell as layerShell; use layerShell::LayerShell; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::rc::Rc; use std::cell::RefCell; use gio::prelude::ApplicationExt; @@ -99,6 +99,8 @@ impl GtkLauncherUI { let search_input = Entry::new(); search_input.set_placeholder_text(Some("Search...")); + search_input.set_widget_name("search-input"); + search_input.add_css_class("launcher-search"); let scrolled_window = ScrolledWindow::new(); scrolled_window.set_min_content_height(300); @@ -113,6 +115,8 @@ impl GtkLauncherUI { // Setup factory to create widgets factory.connect_setup(move |_, list_item| { let container = GtkBox::new(Orientation::Horizontal, 10); + container.set_widget_name("list-item"); + container.add_css_class("launcher-item"); list_item.set_child(Some(&container)); }); @@ -151,16 +155,22 @@ impl GtkLauncherUI { } } image.set_pixel_size(icon_size); + image.set_widget_name("item-icon"); + image.add_css_class("launcher-icon"); // Create text container (vertical box for title + description) let text_box = GtkBox::new(Orientation::Vertical, 2); text_box.set_hexpand(true); text_box.set_valign(gtk::Align::Center); + text_box.set_widget_name("item-text"); + text_box.add_css_class("launcher-text"); // Create title label let title_label = Label::new(Some(&item_obj.title())); title_label.set_xalign(0.0); title_label.set_ellipsize(gtk::pango::EllipsizeMode::End); + title_label.set_widget_name("item-title"); + title_label.add_css_class("launcher-title"); text_box.append(&title_label); // Create description label if description exists @@ -168,7 +178,8 @@ impl GtkLauncherUI { let desc_label = Label::new(Some(&description)); desc_label.set_xalign(0.0); desc_label.set_ellipsize(gtk::pango::EllipsizeMode::Middle); - desc_label.add_css_class("dim-label"); // Make it visually secondary + desc_label.set_widget_name("item-description"); + desc_label.add_css_class("launcher-description"); desc_label.set_opacity(0.7); text_box.append(&desc_label); } @@ -181,11 +192,21 @@ impl GtkLauncherUI { let list_view = ListView::new(Some(selection.clone()), Some(factory)); list_view.set_vexpand(true); list_view.set_can_focus(true); + list_view.set_widget_name("results-list"); + list_view.add_css_class("launcher-list"); scrolled_window.set_child(Some(&list_view)); + scrolled_window.set_widget_name("results-container"); + scrolled_window.add_css_class("launcher-results-container"); + main_box.append(&search_input); main_box.append(&scrolled_window); + main_box.set_widget_name("main-container"); + main_box.add_css_class("launcher-main"); + window.set_child(Some(&main_box)); + window.set_widget_name("launcher-window"); + window.add_css_class("launcher-window"); // Set up layer shell so the launcher can float window.init_layer_shell(); @@ -330,6 +351,57 @@ impl GtkLauncherUI { pub fn show(&self) { self.window.present(); } + + /// Apply default built-in CSS styles + pub fn apply_default_css(&self) -> Result<(), String> { + const DEFAULT_CSS: &str = include_str!("default.css"); + + let css_provider = gtk::CssProvider::new(); + css_provider.load_from_data(DEFAULT_CSS); + + if let Some(display) = gtk::gdk::Display::default() { + gtk::style_context_add_provider_for_display( + &display, + &css_provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + Ok(()) + } else { + Err("Could not get default display".to_string()) + } + } + + pub fn apply_css>(&self, css_path: P) -> Result<(), String> { + let css_provider = gtk::CssProvider::new(); + + // Check if file exists first + if !css_path.as_ref().exists() { + return Err(format!("CSS file does not exist: {}", css_path.as_ref().display())); + } + + // Try to load the CSS file + // Note: load_from_path doesn't return a Result, it panics on error + // So we'll use a different approach with error handling + use std::fs; + match fs::read_to_string(css_path.as_ref()) { + Ok(css_content) => { + css_provider.load_from_data(&css_content); + + // Apply the CSS to the display + if let Some(display) = gtk::gdk::Display::default() { + gtk::style_context_add_provider_for_display( + &display, + &css_provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + Ok(()) + } else { + Err("Could not get default display".to_string()) + } + } + Err(e) => Err(format!("Failed to read CSS file: {}", e)) + } + } } fn find_icon_file(