Skip to main content

mimick/library/
style.rs

1//! Library view CSS: subtle pulsing placeholder for loading thumbnails,
2//! a muted error tile, and a rounded transfer progress treatment.
3//!
4//! Registered exactly once per process via `std::sync::OnceLock`.
5
6use std::sync::OnceLock;
7
8use gtk::CssProvider;
9use gtk::gdk;
10use gtk::style_context_add_provider_for_display;
11
12const LIBRARY_CSS: &str = r#"
13@keyframes mimick-pulse {
14    0%   { background-color: alpha(@view_fg_color, 0.06); }
15    50%  { background-color: alpha(@view_fg_color, 0.14); }
16    100% { background-color: alpha(@view_fg_color, 0.06); }
17}
18
19picture.mimick-thumbnail-loading {
20    background-color: alpha(@view_fg_color, 0.08);
21    animation: mimick-pulse 1.4s ease-in-out infinite;
22}
23
24picture.mimick-thumbnail-loaded {
25}
26
27picture.mimick-thumbnail-error {
28    background-color: alpha(@error_color, 0.18);
29}
30
31overlay.mimick-cell {
32}
33
34box.mimick-empty {
35    padding: 32px;
36}
37
38label.mimick-empty-title {
39    font-size: 1.2em;
40    font-weight: bold;
41}
42
43label.mimick-empty-subtitle {
44    opacity: 0.65;
45}
46
47label.mimick-timeline-banner {
48    font-size: 1.05em;
49    font-weight: 600;
50    padding: 4px 8px;
51    background-color: alpha(@accent_bg_color, 0.10);
52    border-bottom: 1px solid alpha(@view_fg_color, 0.10);
53}
54
55image.mimick-status-badge {
56    opacity: 0.90;
57    color: white;
58    -gtk-icon-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
59}
60
61image.mimick-video-badge {
62    opacity: 0.92;
63    color: white;
64    -gtk-icon-shadow: 0 2px 4px rgba(0, 0, 0, 0.8);
65}
66
67picture.mimick-person-avatar {
68    border-radius: 9999px;
69    background-color: alpha(@view_fg_color, 0.08);
70}
71
72picture.mimick-explore-tile,
73overlay.mimick-explore-tile {
74    border-radius: 6px;
75    background-color: alpha(@view_fg_color, 0.08);
76}
77
78box.mimick-explore-spacer {
79    min-width: 130px;
80    min-height: 100px;
81}
82
83window.mimick-wide box.mimick-explore-spacer {
84    min-width: 220px;
85    min-height: 150px;
86}
87
88overlay.mimick-see-more-tile {
89    border-radius: 6px;
90    background-color: alpha(@view_fg_color, 0.06);
91    border: 1px dashed alpha(@view_fg_color, 0.2);
92}
93
94box.mimick-stat-card {
95    border-radius: 12px;
96    padding: 14px;
97    background: alpha(@accent_bg_color, 0.08);
98}
99
100box.mimick-stat-card.photo-card {
101    background: alpha(#6a9fc7, 0.14);
102}
103
104box.mimick-stat-card.video-card {
105    background: alpha(#d16c96, 0.14);
106}
107
108box.mimick-stat-card.storage-card {
109    background: alpha(#7bb876, 0.14);
110}
111
112label.mimick-stat-value {
113    font-size: 1.6em;
114    font-weight: 800;
115}
116
117label.mimick-stat-label {
118    font-size: 0.85em;
119    font-weight: 600;
120    opacity: 0.60;
121}
122
123progressbar.mimick-quota-bar trough {
124    min-height: 6px;
125    border-radius: 999px;
126    background: alpha(@window_fg_color, 0.10);
127}
128
129progressbar.mimick-quota-bar progress {
130    min-height: 6px;
131    border-radius: 999px;
132    background: mix(@accent_bg_color, #d16c96, 0.45);
133}
134
135box.mimick-version-badge {
136    border-radius: 999px;
137    padding: 4px 12px;
138    background: alpha(@accent_bg_color, 0.12);
139}
140
141.mimick-masonry-canvas {
142    background-color: @view_bg_color;
143}
144
145/* Hide the horizontal scrollbar on the search form.  hscrollbar_policy
146   is Automatic (not Never) so the scrolled window's minimum width is
147   decoupled from the wide AdwEntryRow children (~434px).  The scrollbar
148   itself is never needed visually. */
149scrolledwindow.mimick-search-scroll scrollbar.horizontal {
150    min-height: 0;
151    opacity: 0;
152}
153
154picture.mimick-grid-thumb {
155    min-width: 80px;
156    min-height: 60px;
157}
158
159window.mimick-wide picture.mimick-grid-thumb {
160    min-width: 356px;
161    min-height: 200px;
162}
163
164picture.mimick-thumbnail-square,
165picture.mimick-thumbnail-square.mimick-thumbnail-loading,
166picture.mimick-thumbnail-square.mimick-thumbnail-loaded,
167picture.mimick-thumbnail-square.mimick-thumbnail-error {
168    border-radius: 0;
169}
170
171box.mimick-transfer-shell {
172    min-height: 36px;
173    border-top: 1px solid alpha(@window_fg_color, 0.08);
174    background: alpha(@window_fg_color, 0.03);
175}
176
177box.mimick-transfer-shell.active {
178    background: alpha(@accent_bg_color, 0.04);
179}
180
181progressbar.mimick-transfer-progress {
182    min-width: 100px;
183    min-height: 18px;
184}
185
186progressbar.mimick-transfer-progress trough {
187    min-height: 18px;
188    border-radius: 999px;
189    background: alpha(@window_fg_color, 0.12);
190}
191
192progressbar.mimick-transfer-progress progress {
193    min-height: 18px;
194    border-radius: 999px;
195    background: mix(@accent_bg_color, #d16c96, 0.45);
196}
197
198/* ── Press feedback ───────────────────────────────────────────────
199   Subtle scale-down on press for all interactive elements.
200   GTK4 natively skips CSS transitions when gtk-enable-animations
201   is off, so accessibility is respected automatically.
202
203   Covers: header-bar buttons, dialog buttons, sidebar rows,
204   settings switch/action/spin rows, lightbox actions, grid tiles,
205   explore/album tiles, and any widget with .mimick-pressable.  */
206
207button,
208.mimick-pressable,
209menubutton > button,
210row.activatable {
211    transition: all 150ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
212}
213
214button:active,
215.mimick-pressable:active,
216menubutton > button:active,
217row.activatable:active {
218    transform: scale(0.97);
219    opacity: 0.88;
220}
221
222overlay.mimick-cell {
223    transition: transform 80ms ease-out, opacity 80ms ease-out;
224}
225
226overlay.mimick-cell:active {
227    transform: scale(0.96);
228    opacity: 0.90;
229}
230
231picture.mimick-explore-tile,
232overlay.mimick-explore-tile,
233picture.mimick-person-avatar {
234    transition: transform 80ms ease-out, opacity 80ms ease-out;
235}
236
237picture.mimick-explore-tile:active,
238overlay.mimick-explore-tile:active,
239picture.mimick-person-avatar:active {
240    transform: scale(0.95);
241    opacity: 0.88;
242}
243
244/* Details-pane group accents. The icon classes paint just the prefix icon,
245   keeping the row text in the standard foreground for legibility. */
246.mimick-detail-icon {
247    min-width: 28px;
248    min-height: 28px;
249    padding: 6px;
250    border-radius: 10px;
251    margin: 2px 0;
252    -gtk-icon-size: 16px;
253}
254
255.mimick-accent-camera {
256    color: @accent_color;
257    background-color: alpha(@accent_bg_color, 0.18);
258}
259
260.mimick-accent-image {
261    color: @success_color;
262    background-color: alpha(@success_bg_color, 0.20);
263}
264
265.mimick-accent-location {
266    color: @warning_color;
267    background-color: alpha(@warning_bg_color, 0.22);
268}
269
270scrolledwindow.mimick-details-pane {
271    background: alpha(@window_bg_color, 0.30);
272}
273
274scrolledwindow.mimick-details-pane > viewport > box {
275    padding: 4px;
276}
277
278/* Lightbox image-load spinner. The Mimick app icon eases in from slow
279   rotation, accelerates through the middle of the cycle, then eases back
280   out — giving the breathing rhythm requested for "ramp up / ramp down". */
281@keyframes mimick-icon-spin {
282    0%   { transform: rotate(0deg); }
283    20%  { transform: rotate(40deg); }
284    50%  { transform: rotate(220deg); }
285    80%  { transform: rotate(320deg); }
286    100% { transform: rotate(360deg); }
287}
288
289image.mimick-loader-icon {
290    opacity: 0.85;
291    -gtk-icon-shadow: 0 2px 12px alpha(black, 0.45);
292    animation: mimick-icon-spin 2.4s ease-in-out infinite;
293}
294
295picture.mimick-lightbox-picture {
296    background-color: alpha(@view_fg_color, 0.05);
297}
298
299box.mimick-preview-unavailable {
300    min-width: 240px;
301    padding: 20px;
302    border-radius: 8px;
303    background-color: alpha(@window_bg_color, 0.92);
304    border: 1px solid alpha(@view_fg_color, 0.16);
305}
306
307/* ── Drag-and-drop overlay ───────────────────────────────────────
308   Shown when files are dragged over the window.  */
309
310box.mimick-drop-overlay {
311    background: alpha(@accent_bg_color, 0.50);
312    border: none;
313    border-radius: 0;
314    margin: 0;
315    transition: opacity 200ms ease-in-out;
316}
317
318box.mimick-drop-overlay image {
319    -gtk-icon-size: 48px;
320    opacity: 0.85;
321    color: @accent_fg_color;
322}
323
324box.mimick-drop-overlay label {
325    font-size: 1.1em;
326    font-weight: 600;
327    opacity: 0.9;
328    color: @accent_fg_color;
329}
330
331/* ── Drag badge (multi-file count) ─────────────────────────────── */
332
333box.mimick-drag-badge {
334    background: @accent_bg_color;
335    color: @accent_fg_color;
336    border-radius: 999px;
337    padding: 2px 8px;
338    font-weight: bold;
339    font-size: 11px;
340    min-width: 20px;
341    min-height: 20px;
342}
343
344/* ── Album sidebar row drop-hover ──────────────────────────────── */
345
346row.mimick-album-drop-hover {
347    background: alpha(@accent_bg_color, 0.25);
348    border-radius: 6px;
349    transition: background 150ms ease-in-out, transform 150ms ease-in-out;
350    transform: scale(1.03);
351}
352"#;
353
354static REGISTERED: OnceLock<()> = OnceLock::new();
355
356/// Install the library-view stylesheet on the default display. Idempotent.
357pub fn ensure_registered() {
358    REGISTERED.get_or_init(|| {
359        let Some(display) = gdk::Display::default() else {
360            log::warn!("No default GDK display; library CSS not registered");
361            return;
362        };
363
364        let provider = CssProvider::new();
365        provider.load_from_string(LIBRARY_CSS);
366        style_context_add_provider_for_display(
367            &display,
368            &provider,
369            gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
370        );
371    });
372}