Krolyxon
Commit f2bd31a

Branches

Tags

Refs →

remagesrcuimod.rs

221 lines · 6.4 KB · Rust

1//! UI surfaces: status bar, grid view, palette, which-key, help and info panel.
2//!
3//! Each frame the UI turns state into a [`Scene`]; the renderer only draws it.
4
5use std::path::Path;
6use std::time::Instant;
7
8use crate::config::theme::{Rgba, Theme};
9use crate::render::image::AlphaStyle;
10use crate::render::prims::Prim;
11use crate::render::text::{Align, FontRole, TextItem};
12use crate::render::{ImageDraw, Scene};
13use crate::state::{AlphaBg, AppState, MsgKind};
14
15/// Space Mono label tracking.
16const LABEL_TRACKING: f32 = 0.08;
17
18pub fn file_name(path: &Path) -> String {
19 path.file_name().map_or_else(
20 || path.display().to_string(),
21 |n| n.to_string_lossy().into(),
22 )
23}
24
25fn label(text: &str, color: Rgba, sf: f32, x: f32, y: f32, align: Align) -> TextItem {
26 TextItem {
27 spans: vec![(text.to_uppercase(), color)],
28 role: FontRole::Mono,
29 size: (11.0 * sf).round(),
30 weight: 400,
31 tracking: LABEL_TRACKING,
32 x,
33 y,
34 align,
35 max_width: None,
36 }
37}
38
39/// Longest message shown before truncation, in characters.
40const MAX_MESSAGE: usize = 72;
41
42/// `measure` returns the laid-out width of a text item, for layouts that depend on it.
43pub fn scene<'a>(
44 state: &'a AppState,
45 theme: &Theme,
46 now: Instant,
47 measure: &mut dyn FnMut(&TextItem) -> f32,
48) -> Scene<'a> {
49 let sf = state.scale_factor();
50 let [w, h] = state.window();
51 let bar_h = state.bar_height();
52 let mut scene = Scene {
53 clear: theme.background,
54 image_clip: [0.0, 0.0, w, h - bar_h],
55 alpha: alpha_style(state.alpha_bg, theme, sf),
56 images: Vec::new(),
57 prims: Vec::new(),
58 texts: Vec::new(),
59 };
60
61 let nearest = state.effective_nearest(now);
62 if let Some(fade) = &state.fade {
63 scene.images.push(ImageDraw {
64 path: &fade.path,
65 affine: fade.affine,
66 alpha: 1.0,
67 nearest,
68 });
69 }
70 if let Some(shown) = &state.shown {
71 let alpha = state.fade.as_ref().map_or(1.0, |f| f.alpha.value(now));
72 let affine = state.view.transform(now);
73 scene.images.push(ImageDraw {
74 path: &shown.path,
75 affine,
76 alpha,
77 nearest,
78 });
79 }
80
81 if state.queue.is_empty() {
82 empty_state(&mut scene, theme, sf, [w, h - bar_h]);
83 }
84 if bar_h > 0.0 {
85 status_bar(&mut scene, state, theme, now, [w, h], bar_h, measure);
86 }
87 scene
88}
89
90fn alpha_style(bg: AlphaBg, theme: &Theme, sf: f32) -> AlphaStyle {
91 let (mode, dot) = match bg {
92 AlphaBg::Solid => (0, theme.image_background),
93 AlphaBg::Dots => (1, theme.border_visible),
94 AlphaBg::Checker => (2, theme.surface_raised),
95 };
96 let cell = if mode == 2 { 8.0 } else { 16.0 };
97 AlphaStyle {
98 mode,
99 bg: theme.image_background.linear(),
100 dot: dot.linear(),
101 cell: (cell * sf).round(),
102 radius: sf.max(1.0),
103 }
104}
105
106/// Doto `NO IMAGES` and one hint line.
107fn empty_state(scene: &mut Scene, theme: &Theme, sf: f32, area: [f32; 2]) {
108 let cx = area[0] * 0.5;
109 let cy = area[1] * 0.5;
110 scene.texts.push(TextItem {
111 spans: vec![("NO IMAGES".into(), theme.text_secondary)],
112 role: FontRole::Display,
113 size: (48.0 * sf).round(),
114 weight: 700,
115 tracking: -0.02,
116 x: cx,
117 y: cy - 12.0 * sf,
118 align: Align::Center,
119 max_width: None,
120 });
121 let hint = "remage <file|dir>…";
122 scene.texts.push(label(
123 hint,
124 theme.text_disabled,
125 sf,
126 cx,
127 cy + 32.0 * sf,
128 Align::Center,
129 ));
130}
131
132fn status_bar(
133 scene: &mut Scene,
134 state: &AppState,
135 theme: &Theme,
136 now: Instant,
137 [w, h]: [f32; 2],
138 bar_h: f32,
139 measure: &mut dyn FnMut(&TextItem) -> f32,
140) {
141 let sf = state.scale_factor();
142 let pad = 16.0 * sf;
143 let y = h - bar_h * 0.5;
144 scene.prims.push(Prim::rect(
145 [0.0, h - bar_h, w, bar_h],
146 theme.background.linear(),
147 ));
148
149 let mut spans: Vec<(String, Rgba)> = Vec::new();
150 if !state.pending.is_empty() {
151 spans.push((state.pending.clone(), theme.text_disabled));
152 }
153 if let Some(m) = &state.message {
154 let color = if m.kind == MsgKind::Error {
155 theme.accent
156 } else {
157 theme.text_primary
158 };
159 let mut text = m.text.to_uppercase();
160 if let Some((cut, _)) = text.char_indices().nth(MAX_MESSAGE) {
161 text.truncate(cut);
162 text.push('…');
163 }
164 spans.push((format!("[{text}]"), color));
165 }
166 if state.loading_visible(now) {
167 spans.push(("[LOADING]".into(), theme.text_secondary));
168 }
169 if state.skipped > 0 {
170 spans.push((format!("[SKIPPED {}]", state.skipped), theme.text_secondary));
171 }
172 let mut info = Vec::new();
173 if let Some(s) = state
174 .shown
175 .as_ref()
176 .filter(|s| Some(s.path.as_path()) == state.queue.current())
177 {
178 info.push(s.format.to_string());
179 info.push(format!("{}×{}", s.size[0], s.size[1]));
180 info.push(format!("{}%", (state.view.scale() * 100.0).round()));
181 }
182 if !state.queue.is_empty() {
183 let count = state.queue.len();
184 let digits = count.to_string().len().max(3);
185 info.push(format!("{:0digits$}/{count}", state.queue.index() + 1));
186 }
187 if !info.is_empty() {
188 spans.push((info.join(" "), theme.text_secondary));
189 }
190 // Two spaces between segments, attached to the preceding span so colours stay put.
191 let n = spans.len();
192 for (s, _) in spans.iter_mut().take(n.saturating_sub(1)) {
193 s.push_str(" ");
194 }
195 let mut right_w = 0.0;
196 if !spans.is_empty() {
197 let mut item = label("", theme.text_secondary, sf, w - pad, y, Align::Right);
198 item.spans = spans
199 .into_iter()
200 .map(|(s, c)| (s.to_uppercase(), c))
201 .collect();
202 right_w = measure(&item);
203 scene.texts.push(item);
204 }
205
206 // The filename takes whatever the right side leaves, ellipsised in the middle.
207 if let Some(cur) = state.queue.current() {
208 let room = (w - 2.0 * pad - right_w - 32.0 * sf).max(0.0);
209 scene.texts.push(TextItem {
210 spans: vec![(file_name(cur), theme.text_primary)],
211 role: FontRole::Ui,
212 size: (14.0 * sf).round(),
213 weight: 400,
214 tracking: 0.0,
215 x: pad,
216 y,
217 align: Align::Left,
218 max_width: Some(room),
219 });
220 }
221}