diff --git a/crates/pdfs-gui/build.rs b/crates/pdfs-gui/build.rs
index 2362531..dd7440a 100644
--- a/crates/pdfs-gui/build.rs
+++ b/crates/pdfs-gui/build.rs
@@ -27,5 +27,6 @@ fn main() {
println!("cargo:rerun-if-changed=resources/icons");
println!("cargo:rerun-if-changed=resources/style.css");
println!("cargo:rerun-if-changed=resources/proton-theme.css");
+ println!("cargo:rerun-if-changed=resources/prompt.css");
println!("cargo:rerun-if-changed=../../packaging/io.narl.proton-drive-linux.svg");
}
diff --git a/crates/pdfs-gui/resources/pdfs.gresource.xml b/crates/pdfs-gui/resources/pdfs.gresource.xml
index 73120cb..1026a5a 100644
--- a/crates/pdfs-gui/resources/pdfs.gresource.xml
+++ b/crates/pdfs-gui/resources/pdfs.gresource.xml
@@ -33,5 +33,7 @@
style.css
proton-theme.css
+
+ prompt.css
diff --git a/crates/pdfs-gui/resources/prompt.css b/crates/pdfs-gui/resources/prompt.css
new file mode 100644
index 0000000..7f769d1
--- /dev/null
+++ b/crates/pdfs-gui/resources/prompt.css
@@ -0,0 +1,224 @@
+/* The search launcher (pdfs-prompt). Colours come from the libadwaita
+ palette, so the launcher follows the system accent, light/dark and high
+ contrast like the app; the opt-in Proton theme recolours both alike. */
+
+window.launcher-window {
+ background: transparent;
+}
+
+.launcher-card {
+ background-color: var(--view-bg-color);
+ color: var(--view-fg-color);
+ border: 1px solid var(--border-color);
+ border-radius: 14px;
+ box-shadow: 0 16px 48px rgb(0 0 0 / 35%);
+}
+
+.search-bar {
+ padding: 14px 18px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.search-icon {
+ -gtk-icon-size: 20px;
+ color: color-mix(in srgb, currentColor 55%, transparent);
+}
+
+.search-entry {
+ background: none;
+ border: none;
+ box-shadow: none;
+ outline: none;
+ padding: 2px 0;
+ font-size: 1.15rem;
+}
+
+.key-hint {
+ padding: 2px 8px;
+ border-radius: 6px;
+ background-color: color-mix(in srgb, currentColor 8%, transparent);
+ font-size: 0.75rem;
+ opacity: 0.55;
+}
+
+.chip-row {
+ padding: 10px 14px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.chip {
+ padding: 4px 14px;
+ min-height: 0;
+ border-radius: 999px;
+ background: none;
+ border: 1px solid var(--border-color);
+ box-shadow: none;
+ font-size: 0.86rem;
+ font-weight: 500;
+}
+
+.chip:hover {
+ background-color: color-mix(in srgb, currentColor 6%, transparent);
+}
+
+.chip:checked {
+ background-color: var(--accent-bg-color);
+ color: var(--accent-fg-color);
+ border-color: var(--accent-bg-color);
+}
+
+/* --- results ------------------------------------------------------------ */
+
+.results {
+ padding: 2px 8px 10px 8px;
+}
+
+.section-header {
+ padding: 12px 10px 6px 10px;
+}
+
+.section-title {
+ font-size: 0.78rem;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ opacity: 0.55;
+}
+
+.section-icon {
+ -gtk-icon-size: 14px;
+ opacity: 0.55;
+}
+
+.result-list {
+ background: none;
+}
+
+.result-row {
+ padding: 6px 10px;
+ border-radius: 10px;
+}
+
+.result-row:hover {
+ background-color: color-mix(in srgb, currentColor 5%, transparent);
+}
+
+/* A soft tint rather than a solid accent bar: the text keeps its colour, so
+ the highlighted match stays readable on the selected row. */
+.result-row:selected,
+.result-row:selected:hover {
+ background-color: color-mix(in srgb, var(--accent-bg-color) 18%, transparent);
+ color: inherit;
+}
+
+.result-name {
+ font-weight: 500;
+}
+
+.result-location,
+.result-meta {
+ font-size: 0.8rem;
+ opacity: 0.55;
+}
+
+.result-row:selected .result-location,
+.result-row:selected .result-meta {
+ opacity: 0.7;
+}
+
+.thumb {
+ border-radius: 6px;
+}
+
+.result-badge,
+.preview-badge {
+ -gtk-icon-size: 14px;
+}
+
+.badge-pinned {
+ color: var(--success-color);
+}
+
+.badge-cached {
+ color: var(--accent-color);
+}
+
+.badge-cloud,
+.badge-local {
+ opacity: 0.55;
+}
+
+/* --- preview ------------------------------------------------------------ */
+
+.preview {
+ padding: 16px;
+ background-color: color-mix(in srgb, currentColor 3%, transparent);
+}
+
+.preview-thumb {
+ margin-bottom: 12px;
+}
+
+.preview-thumb .thumb {
+ border-radius: 10px;
+}
+
+.preview-name {
+ font-weight: 700;
+ font-size: 1.05rem;
+}
+
+.preview-kind {
+ font-size: 0.85rem;
+ opacity: 0.6;
+ margin-bottom: 12px;
+}
+
+.preview-key {
+ font-size: 0.8rem;
+ opacity: 0.55;
+}
+
+.preview-value {
+ font-size: 0.85rem;
+}
+
+.preview-actions {
+ margin-top: 12px;
+}
+
+.preview-action {
+ padding: 6px 8px;
+ border-radius: 8px;
+}
+
+.preview-shortcut {
+ font-size: 0.75rem;
+ opacity: 0.6;
+}
+
+.preview-feedback {
+ margin-top: 6px;
+ font-size: 0.8rem;
+ color: var(--accent-color);
+}
+
+.preview-empty {
+ opacity: 0.2;
+}
+
+/* --- footer ------------------------------------------------------------- */
+
+.footer {
+ padding: 8px 16px;
+ border-top: 1px solid var(--border-color);
+}
+
+.footer-text {
+ font-size: 0.78rem;
+ opacity: 0.5;
+}
+
+statuspage.compact > scrolledwindow > viewport > box {
+ margin: 24px 12px;
+}
diff --git a/crates/pdfs-gui/src/app/main.rs b/crates/pdfs-gui/src/app/main.rs
index 643366f..9e334bb 100644
--- a/crates/pdfs-gui/src/app/main.rs
+++ b/crates/pdfs-gui/src/app/main.rs
@@ -8,6 +8,8 @@ pub(crate) mod events;
pub(crate) mod i18n;
pub(crate) mod pages;
pub(crate) mod reload;
+#[path = "../theme.rs"]
+pub(crate) mod theme;
pub(crate) mod widgets;
use compat::*;
@@ -30,6 +32,7 @@ use pages::takeout::*;
use pages::trash::*;
use pages::verify::*;
use reload::{LoadTicket, Loader, replace_items};
+use theme::{load_resources, proton_theme_active, set_proton_theme};
use widgets::compat::*;
use widgets::details::*;
use widgets::file_list::*;
@@ -210,7 +213,7 @@ fn main() -> glib::ExitCode {
.flags(gio::ApplicationFlags::HANDLES_COMMAND_LINE)
.build();
app.connect_startup(|_| {
- load_proton_theme();
+ load_resources("/de/nils/protondrivelinux/style.css");
if let Ok(dirs) = AppDirs::new() {
set_proton_theme(dirs.load_config().proton_theme.unwrap_or(false));
}
@@ -287,157 +290,6 @@ pub(crate) fn spawn_tray() {
}
}
-/// Register the bundled GResources (custom icons, the stylesheet) and load the
-/// app stylesheet. The Proton theme is a separate provider, applied by
-/// [`set_proton_theme`] from the saved preference.
-fn load_proton_theme() {
- let bytes = include_bytes!(concat!(env!("OUT_DIR"), "/pdfs.gresource"));
- let resource_data = glib::Bytes::from_static(bytes);
- if let Ok(resource) = gio::Resource::from_data(&resource_data) {
- gio::resources_register(&resource);
- } else {
- tracing::error!("failed to load gresource bundle");
- }
-
- let Some(display) = gtk4::gdk::Display::default() else {
- return;
- };
- let provider = gtk4::CssProvider::new();
- provider.load_from_resource("/de/nils/protondrivelinux/style.css");
- // Above the user's own gtk.css, which often imports a whole theme: its
- // `button { padding: 6px 10px; min-width: 16px }` would otherwise pad every
- // photo tile and push the grid past the window. Every rule in the sheet is
- // scoped to the app's own classes, so nothing else the user styled changes.
- // One above the Proton theme too, so the app's rules refine it.
- gtk4::style_context_add_provider_for_display(
- &display,
- &provider,
- gtk4::STYLE_PROVIDER_PRIORITY_USER + 2,
- );
- gtk4::IconTheme::for_display(&display).add_resource_path("/de/nils/protondrivelinux/icons");
-}
-
-thread_local! {
- /// The Proton theme provider while it is installed. Kept so turning the
- /// preference off can remove exactly what turning it on added.
- static PROTON_THEME: RefCell