From 83fa22d84cee6c55df9dfa054a6e98f5b6f54d99 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Sat, 28 Mar 2026 11:17:45 +0100 Subject: [PATCH] feat(ui): add result highlighting and remove window shadow Highlighting: - Dynamic plugin results (calculator, converter, websearch, filesearch) get a subtle accent left-border + background tint when auto-detected - Exact name matches (case-insensitive) are highlighted the same way - Exact match on apps gets a higher score boost (50k) than other providers (30k), so apps rank first when names match exactly Shadow: - Removed hardcoded box-shadow from all theme CSS files - Added --owlry-shadow variable in base.css (defaults to none) - Themes can opt into shadow via --owlry-shadow if desired CSS class: .owlry-result-highlight on ResultRow --- crates/owlry-core/src/providers/mod.rs | 13 +++++++++- crates/owlry/src/resources/base.css | 11 +++++++++ crates/owlry/src/resources/owl-theme.css | 2 -- crates/owlry/src/ui/main_window.rs | 21 +++++++++++------ crates/owlry/src/ui/result_row.rs | 30 ++++++++++++++++++++++-- data/themes/apex-neon.css | 2 -- data/themes/catppuccin-mocha.css | 2 -- data/themes/dracula.css | 2 -- data/themes/gruvbox-dark.css | 2 -- data/themes/nord.css | 2 -- data/themes/one-dark.css | 2 -- data/themes/owl.css | 2 -- data/themes/rose-pine.css | 2 -- data/themes/solarized-dark.css | 2 -- data/themes/tokyo-night.css | 2 -- 15 files changed, 65 insertions(+), 32 deletions(-) diff --git a/crates/owlry-core/src/providers/mod.rs b/crates/owlry-core/src/providers/mod.rs index 8fc716f..9fc9025 100644 --- a/crates/owlry-core/src/providers/mod.rs +++ b/crates/owlry-core/src/providers/mod.rs @@ -686,7 +686,18 @@ impl ProviderManager { base_score.map(|s| { let frecency_score = frecency.get_score_at(&item.id, now); let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64; - (item.clone(), s + frecency_boost) + + // Exact name match bonus — apps get a higher boost + let exact_match_boost = if item.name.eq_ignore_ascii_case(query) { + match &item.provider { + ProviderType::Application => 50_000, + _ => 30_000, + } + } else { + 0 + }; + + (item.clone(), s + frecency_boost + exact_match_boost) }) }; diff --git a/crates/owlry/src/resources/base.css b/crates/owlry/src/resources/base.css index f1f1869..18d1ef3 100644 --- a/crates/owlry/src/resources/base.css +++ b/crates/owlry/src/resources/base.css @@ -14,6 +14,7 @@ background-color: var(--owlry-bg, @theme_bg_color); border-radius: var(--owlry-border-radius, 12px); border: 1px solid var(--owlry-border, @borders); + box-shadow: var(--owlry-shadow, none); padding: 12px; } @@ -56,6 +57,16 @@ color: var(--owlry-accent-bright, @theme_selected_fg_color); } +/* Highlighted result row (exact match or auto-detected plugin result) */ +.owlry-result-highlight { + background-color: alpha(var(--owlry-accent, @theme_selected_bg_color), 0.08); + border-left: 3px solid var(--owlry-accent, @theme_selected_bg_color); +} + +.owlry-result-highlight:selected { + border-left: 3px solid var(--owlry-accent-bright, @theme_selected_fg_color); +} + /* Result icon */ .owlry-result-icon { color: var(--owlry-text, @theme_fg_color); diff --git a/crates/owlry/src/resources/owl-theme.css b/crates/owlry/src/resources/owl-theme.css index 23244ba..1b34529 100644 --- a/crates/owlry/src/resources/owl-theme.css +++ b/crates/owlry/src/resources/owl-theme.css @@ -31,8 +31,6 @@ .owlry-main { background-color: rgba(26, 27, 38, 0.95); border: 1px solid rgba(65, 72, 104, 0.6); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(224, 175, 104, 0.1); } /* Search entry */ diff --git a/crates/owlry/src/ui/main_window.rs b/crates/owlry/src/ui/main_window.rs index 21a3efc..0cb0157 100644 --- a/crates/owlry/src/ui/main_window.rs +++ b/crates/owlry/src/ui/main_window.rs @@ -42,6 +42,8 @@ struct LazyLoadState { all_results: Vec, /// Number of items currently displayed displayed_count: usize, + /// The query that produced these results (for highlighting in lazy-loaded batches) + query: String, } /// Number of items to display initially and per batch @@ -528,7 +530,7 @@ impl MainWindow { } for item in &actions { - let row = ResultRow::new(item); + let row = ResultRow::new(item, ""); results_list.append(&row); } @@ -610,7 +612,7 @@ impl MainWindow { } for item in &filtered { - let row = ResultRow::new(item); + let row = ResultRow::new(item, ""); results_list.append(&row); } @@ -709,6 +711,7 @@ impl MainWindow { let results_list_cb = results_list.clone(); let current_results_cb = current_results.clone(); let lazy_state_cb = lazy_state.clone(); + let query_for_highlight = query_str.clone(); gtk4::glib::spawn_future_local(async move { if let Ok(result) = rx.await { @@ -726,7 +729,7 @@ impl MainWindow { INITIAL_RESULTS.min(items.len()); for item in items.iter().take(initial_count) { - let row = ResultRow::new(item); + let row = ResultRow::new(item, &query_for_highlight); results_list_cb.append(&row); } @@ -741,6 +744,7 @@ impl MainWindow { let mut lazy = lazy_state_cb.borrow_mut(); lazy.all_results = items; lazy.displayed_count = initial_count; + lazy.query = query_for_highlight; } }); } else { @@ -760,7 +764,7 @@ impl MainWindow { let initial_count = INITIAL_RESULTS.min(results.len()); for item in results.iter().take(initial_count) { - let row = ResultRow::new(item); + let row = ResultRow::new(item, &query_str); results_list.append(&row); } @@ -772,6 +776,7 @@ impl MainWindow { results[..initial_count].to_vec(); let mut lazy = lazy_state.borrow_mut(); lazy.all_results = results; + lazy.query = query_str; lazy.displayed_count = initial_count; } }, @@ -1267,7 +1272,7 @@ impl MainWindow { let initial_count = INITIAL_RESULTS.min(results.len()); for item in results.iter().take(initial_count) { - let row = ResultRow::new(item); + let row = ResultRow::new(item, ""); results_list.append(&row); } @@ -1303,7 +1308,7 @@ impl MainWindow { let initial_count = INITIAL_RESULTS.min(results.len()); for item in results.iter().take(initial_count) { - let row = ResultRow::new(item); + let row = ResultRow::new(item, query); self.results_list.append(&row); } @@ -1316,6 +1321,7 @@ impl MainWindow { let mut lazy = self.lazy_state.borrow_mut(); lazy.all_results = results; lazy.displayed_count = initial_count; + lazy.query = query.to_string(); } /// Set up lazy loading scroll detection @@ -1372,8 +1378,9 @@ impl MainWindow { if displayed < all_count { // Load next batch let new_end = (displayed + LOAD_MORE_BATCH).min(all_count); + let query = lazy.query.clone(); for item in lazy.all_results[displayed..new_end].iter() { - let row = ResultRow::new(item); + let row = ResultRow::new(item, &query); results_list.append(&row); } lazy.displayed_count = new_end; diff --git a/crates/owlry/src/ui/result_row.rs b/crates/owlry/src/ui/result_row.rs index f4366b9..7be5019 100644 --- a/crates/owlry/src/ui/result_row.rs +++ b/crates/owlry/src/ui/result_row.rs @@ -1,6 +1,6 @@ use gtk4::prelude::*; use gtk4::{Box as GtkBox, Image, Label, ListBoxRow, Orientation, Widget}; -use owlry_core::providers::LaunchItem; +use owlry_core::providers::{LaunchItem, ProviderType}; #[allow(dead_code)] pub struct ResultRow { @@ -18,9 +18,31 @@ fn is_emoji_icon(s: &str) -> bool { !first_char.is_ascii() && s.chars().count() <= 8 } +/// Check if this item should be highlighted based on the query. +/// Highlighted when: +/// - Item is from a dynamic plugin (calculator, converter, websearch, filesearch) +/// and the query is non-empty (auto-detect triggered) +/// - Item name exactly matches the query (case-insensitive) +fn should_highlight(item: &LaunchItem, query: &str) -> bool { + if query.is_empty() { + return false; + } + + // Exact name match (case-insensitive) + if item.name.eq_ignore_ascii_case(query) { + return true; + } + + // Dynamic plugin auto-detect results + matches!( + &item.provider, + ProviderType::Plugin(id) if matches!(id.as_str(), "calc" | "conv" | "websearch" | "filesearch") + ) +} + impl ResultRow { #[allow(clippy::new_ret_no_self)] - pub fn new(item: &LaunchItem) -> ListBoxRow { + pub fn new(item: &LaunchItem, query: &str) -> ListBoxRow { let row = ListBoxRow::builder() .selectable(true) .activatable(true) @@ -28,6 +50,10 @@ impl ResultRow { row.add_css_class("owlry-result-row"); + if should_highlight(item, query) { + row.add_css_class("owlry-result-highlight"); + } + let hbox = GtkBox::builder() .orientation(Orientation::Horizontal) .spacing(12) diff --git a/data/themes/apex-neon.css b/data/themes/apex-neon.css index 90505a9..9570110 100644 --- a/data/themes/apex-neon.css +++ b/data/themes/apex-neon.css @@ -77,8 +77,6 @@ .owlry-main { background-color: rgba(5, 5, 5, 0.98); border: 1px solid rgba(38, 38, 38, 0.8); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8), - 0 0 0 1px rgba(255, 0, 68, 0.1); } .owlry-search { diff --git a/data/themes/catppuccin-mocha.css b/data/themes/catppuccin-mocha.css index 4d0defd..0aa3340 100644 --- a/data/themes/catppuccin-mocha.css +++ b/data/themes/catppuccin-mocha.css @@ -24,8 +24,6 @@ .owlry-main { background-color: rgba(30, 30, 46, 0.95); border: 1px solid rgba(69, 71, 90, 0.6); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(203, 166, 247, 0.1); } .owlry-search { diff --git a/data/themes/dracula.css b/data/themes/dracula.css index f525d59..6aafb66 100644 --- a/data/themes/dracula.css +++ b/data/themes/dracula.css @@ -24,8 +24,6 @@ .owlry-main { background-color: rgba(40, 42, 54, 0.95); border: 1px solid rgba(98, 114, 164, 0.6); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(189, 147, 249, 0.1); } .owlry-search { diff --git a/data/themes/gruvbox-dark.css b/data/themes/gruvbox-dark.css index 5e917ff..08b713f 100644 --- a/data/themes/gruvbox-dark.css +++ b/data/themes/gruvbox-dark.css @@ -24,8 +24,6 @@ .owlry-main { background-color: rgba(40, 40, 40, 0.95); border: 1px solid rgba(80, 73, 69, 0.6); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(254, 128, 25, 0.1); } .owlry-search { diff --git a/data/themes/nord.css b/data/themes/nord.css index 602d56c..08b7362 100644 --- a/data/themes/nord.css +++ b/data/themes/nord.css @@ -24,8 +24,6 @@ .owlry-main { background-color: rgba(46, 52, 64, 0.95); border: 1px solid rgba(76, 86, 106, 0.6); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), - 0 0 0 1px rgba(136, 192, 208, 0.1); } .owlry-search { diff --git a/data/themes/one-dark.css b/data/themes/one-dark.css index 40c1f50..d77e3c0 100644 --- a/data/themes/one-dark.css +++ b/data/themes/one-dark.css @@ -24,8 +24,6 @@ .owlry-main { background-color: rgba(40, 44, 52, 0.95); border: 1px solid rgba(24, 26, 31, 0.6); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(97, 175, 239, 0.1); } .owlry-search { diff --git a/data/themes/owl.css b/data/themes/owl.css index d6c962b..ccfe587 100644 --- a/data/themes/owl.css +++ b/data/themes/owl.css @@ -33,8 +33,6 @@ .owlry-main { background-color: rgba(26, 27, 38, 0.95); border: 1px solid rgba(65, 72, 104, 0.6); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(224, 175, 104, 0.1); } .owlry-search { diff --git a/data/themes/rose-pine.css b/data/themes/rose-pine.css index d9e5762..40e5d71 100644 --- a/data/themes/rose-pine.css +++ b/data/themes/rose-pine.css @@ -24,8 +24,6 @@ .owlry-main { background-color: rgba(25, 23, 36, 0.95); border: 1px solid rgba(38, 35, 58, 0.6); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(196, 167, 231, 0.1); } .owlry-search { diff --git a/data/themes/solarized-dark.css b/data/themes/solarized-dark.css index 1de0a6f..f9e18e3 100644 --- a/data/themes/solarized-dark.css +++ b/data/themes/solarized-dark.css @@ -24,8 +24,6 @@ .owlry-main { background-color: rgba(0, 43, 54, 0.95); border: 1px solid rgba(88, 110, 117, 0.6); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(38, 139, 210, 0.1); } .owlry-search { diff --git a/data/themes/tokyo-night.css b/data/themes/tokyo-night.css index ee6c1a0..d7874ab 100644 --- a/data/themes/tokyo-night.css +++ b/data/themes/tokyo-night.css @@ -24,8 +24,6 @@ .owlry-main { background-color: rgba(26, 27, 38, 0.95); border: 1px solid rgba(65, 72, 104, 0.6); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(122, 162, 247, 0.1); } .owlry-search {