| 14 |
14 |
|
use calloop_wayland_source::WaylandSource;
|
| 15 |
15 |
|
use shop_grid::Grid;
|
| 16 |
16 |
|
use shop_pty::{Pty, PtySize};
|
| 17 |
|
- |
use shop_render::TextRenderer;
|
|
17 |
+ |
use shop_render::{CellDraw, TextRenderer};
|
| 18 |
18 |
|
use shop_wayland::{
|
| 19 |
19 |
|
Capability, CompositorHandler, CompositorState, Connection, OutputHandler, OutputState,
|
| 20 |
20 |
|
Pending, ProvidesRegistryState, QueueHandle, RegistryState, SeatHandler, SeatState,
|
| 28 |
28 |
|
|
| 29 |
29 |
|
const INITIAL: (u32, u32) = (960, 540);
|
| 30 |
30 |
|
const FONT_PATH: &str = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf";
|
| 31 |
|
- |
const FONT_PX: f32 = 18.0;
|
| 32 |
|
- |
const CELL_ADVANCE: f32 = 11.0;
|
| 33 |
|
- |
const CELL_HEIGHT: f32 = 22.0;
|
|
31 |
+ |
const FONT_PX: f32 = 14.0;
|
|
32 |
+ |
const CELL_ADVANCE: f32 = 8.5;
|
|
33 |
+ |
const CELL_HEIGHT: f32 = 17.0;
|
|
34 |
+ |
const PAD_X: f32 = 10.0;
|
|
35 |
+ |
const PAD_Y: f32 = 6.0;
|
| 34 |
36 |
|
const CLEAR: wgpu::Color = wgpu::Color {
|
| 35 |
37 |
|
r: 0.043,
|
| 36 |
38 |
|
g: 0.055,
|
| 51 |
53 |
|
.map_err(|e| anyhow::anyhow!("read font {FONT_PATH}: {e}"))?;
|
| 52 |
54 |
|
|
| 53 |
55 |
|
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".into());
|
| 54 |
|
- |
let cols_initial: u16 = (INITIAL.0 as f32 / CELL_ADVANCE) as u16;
|
| 55 |
|
- |
let rows_initial: u16 = (INITIAL.1 as f32 / CELL_HEIGHT) as u16;
|
|
56 |
+ |
let cols_initial = grid_cols(INITIAL.0);
|
|
57 |
+ |
let rows_initial = grid_rows(INITIAL.1);
|
| 56 |
58 |
|
let pty = Pty::spawn(
|
| 57 |
59 |
|
&shell,
|
| 58 |
60 |
|
&[],
|
| 143 |
145 |
|
chrome,
|
| 144 |
146 |
|
surface,
|
| 145 |
147 |
|
surface_config,
|
|
148 |
+ |
surface_format: format,
|
| 146 |
149 |
|
device: Arc::new(device),
|
| 147 |
150 |
|
queue: Arc::new(queue),
|
| 148 |
151 |
|
text,
|
| 149 |
152 |
|
pty,
|
| 150 |
153 |
|
grid,
|
| 151 |
154 |
|
parser,
|
|
155 |
+ |
font_data: std::fs::read(FONT_PATH)?,
|
|
156 |
+ |
scale: 1,
|
| 152 |
157 |
|
pending_redraw: true,
|
| 153 |
158 |
|
};
|
| 154 |
159 |
|
app.surface.configure(&app.device, &app.surface_config);
|
| 202 |
207 |
|
|
| 203 |
208 |
|
for change in app.chrome.pending.drain(..).collect::<Vec<_>>() {
|
| 204 |
209 |
|
match change {
|
| 205 |
|
- |
Pending::Resized { width, height } => {
|
| 206 |
|
- |
app.surface_config.width = width;
|
| 207 |
|
- |
app.surface_config.height = height;
|
|
210 |
+ |
Pending::Resized {
|
|
211 |
+ |
width: logical_w,
|
|
212 |
+ |
height: logical_h,
|
|
213 |
+ |
} => {
|
|
214 |
+ |
let s = app.scale.max(1);
|
|
215 |
+ |
let physical_w = logical_w * s;
|
|
216 |
+ |
let physical_h = logical_h * s;
|
|
217 |
+ |
app.surface_config.width = physical_w;
|
|
218 |
+ |
app.surface_config.height = physical_h;
|
| 208 |
219 |
|
app.surface.configure(&app.device, &app.surface_config);
|
| 209 |
|
- |
app.text.resize(&app.queue, width, height);
|
| 210 |
|
- |
let cols = (width as f32 / CELL_ADVANCE).max(1.0) as u16;
|
| 211 |
|
- |
let rows = (height as f32 / CELL_HEIGHT).max(1.0) as u16;
|
|
220 |
+ |
app.text.resize(&app.queue, physical_w, physical_h);
|
|
221 |
+ |
// Tell the compositor our buffer is N logical px per buffer px.
|
|
222 |
+ |
app.chrome.xdg_window.wl_surface().set_buffer_scale(s as i32);
|
|
223 |
+ |
let cols = grid_cols(logical_w);
|
|
224 |
+ |
let rows = grid_rows(logical_h);
|
| 212 |
225 |
|
app.grid.resize(cols, rows);
|
| 213 |
226 |
|
let _ = app.pty.resize(PtySize {
|
| 214 |
227 |
|
cols,
|
| 215 |
228 |
|
rows,
|
| 216 |
|
- |
cell_width: CELL_ADVANCE.round() as u16,
|
| 217 |
|
- |
cell_height: CELL_HEIGHT.round() as u16,
|
|
229 |
+ |
cell_width: (CELL_ADVANCE * s as f32).round() as u16,
|
|
230 |
+ |
cell_height: (CELL_HEIGHT * s as f32).round() as u16,
|
| 218 |
231 |
|
});
|
| 219 |
232 |
|
app.pending_redraw = true;
|
| 220 |
233 |
|
}
|
| 233 |
246 |
|
Ok(())
|
| 234 |
247 |
|
}
|
| 235 |
248 |
|
|
|
249 |
+ |
fn grid_cols(px_w: u32) -> u16 {
|
|
250 |
+ |
let usable = (px_w as f32 - 2.0 * PAD_X).max(CELL_ADVANCE);
|
|
251 |
+ |
(usable / CELL_ADVANCE) as u16
|
|
252 |
+ |
}
|
|
253 |
+ |
|
|
254 |
+ |
fn grid_rows(px_h: u32) -> u16 {
|
|
255 |
+ |
let usable = (px_h as f32 - 2.0 * PAD_Y).max(CELL_HEIGHT);
|
|
256 |
+ |
(usable / CELL_HEIGHT) as u16
|
|
257 |
+ |
}
|
|
258 |
+ |
|
| 236 |
259 |
|
struct App {
|
| 237 |
260 |
|
chrome: WaylandChrome,
|
| 238 |
261 |
|
surface: wgpu::Surface<'static>,
|
| 239 |
262 |
|
surface_config: wgpu::SurfaceConfiguration,
|
|
263 |
+ |
surface_format: wgpu::TextureFormat,
|
| 240 |
264 |
|
device: Arc<wgpu::Device>,
|
| 241 |
265 |
|
queue: Arc<wgpu::Queue>,
|
| 242 |
266 |
|
text: TextRenderer,
|
| 243 |
267 |
|
pty: Pty,
|
| 244 |
268 |
|
grid: Grid,
|
| 245 |
269 |
|
parser: vte::Parser,
|
|
270 |
+ |
font_data: Vec<u8>,
|
|
271 |
+ |
/// HiDPI integer scale from `wl_surface.enter` outputs.
|
|
272 |
+ |
scale: u32,
|
| 246 |
273 |
|
pending_redraw: bool,
|
| 247 |
274 |
|
}
|
| 248 |
275 |
|
|
| 274 |
301 |
|
..Default::default()
|
| 275 |
302 |
|
});
|
| 276 |
303 |
|
}
|
| 277 |
|
- |
// Render each row of the grid as a single line. Colors and per-cell
|
| 278 |
|
- |
// attributes come next milestone. The parser has already resolved wrap,
|
| 279 |
|
- |
// control bytes, and cursor motion, so this is just character emission.
|
|
304 |
+ |
// Build a batch of cell-anchored glyph draws. Positions are in PHYSICAL
|
|
305 |
+ |
// pixels (matches surface + text uniform), so scale-multiply here.
|
|
306 |
+ |
// Ignoring per-glyph font advance is deliberate: monospace terminal cells
|
|
307 |
+ |
// must line up regardless of what the font wants.
|
|
308 |
+ |
let s = app.scale as f32;
|
|
309 |
+ |
let mut batch: Vec<CellDraw> = Vec::with_capacity(
|
|
310 |
+ |
app.grid.rows() as usize * app.grid.cols() as usize,
|
|
311 |
+ |
);
|
| 280 |
312 |
|
for r in 0..app.grid.rows() {
|
| 281 |
|
- |
let line: String = app
|
| 282 |
|
- |
.grid
|
| 283 |
|
- |
.row(r)
|
| 284 |
|
- |
.iter()
|
| 285 |
|
- |
.map(|c| if c.c == '\0' { ' ' } else { c.c })
|
| 286 |
|
- |
.collect();
|
| 287 |
|
- |
let trimmed = line.trim_end_matches(' ');
|
| 288 |
|
- |
if !trimmed.is_empty() {
|
| 289 |
|
- |
let y = r as f32 * CELL_HEIGHT;
|
| 290 |
|
- |
app.text.draw(
|
| 291 |
|
- |
&app.device,
|
| 292 |
|
- |
&app.queue,
|
| 293 |
|
- |
&view,
|
| 294 |
|
- |
&mut encoder,
|
| 295 |
|
- |
trimmed,
|
| 296 |
|
- |
4.0,
|
|
313 |
+ |
let y = (PAD_Y + r as f32 * CELL_HEIGHT) * s;
|
|
314 |
+ |
for (col, cell) in app.grid.row(r).iter().enumerate() {
|
|
315 |
+ |
let c = cell.c;
|
|
316 |
+ |
if c == ' ' || c == '\0' {
|
|
317 |
+ |
continue;
|
|
318 |
+ |
}
|
|
319 |
+ |
batch.push(CellDraw {
|
|
320 |
+ |
c,
|
|
321 |
+ |
x: (PAD_X + col as f32 * CELL_ADVANCE) * s,
|
| 297 |
322 |
|
y,
|
| 298 |
|
- |
TEXT_COLOR,
|
| 299 |
|
- |
);
|
|
323 |
+ |
color: TEXT_COLOR,
|
|
324 |
+ |
});
|
| 300 |
325 |
|
}
|
| 301 |
326 |
|
}
|
|
327 |
+ |
if !batch.is_empty() {
|
|
328 |
+ |
app.text.draw_cells(
|
|
329 |
+ |
&app.device,
|
|
330 |
+ |
&app.queue,
|
|
331 |
+ |
&view,
|
|
332 |
+ |
&mut encoder,
|
|
333 |
+ |
&batch,
|
|
334 |
+ |
);
|
|
335 |
+ |
}
|
| 302 |
336 |
|
app.queue.submit(std::iter::once(encoder.finish()));
|
| 303 |
337 |
|
app.queue.present(frame);
|
| 304 |
338 |
|
Ok(())
|
| 312 |
346 |
|
_: &Connection,
|
| 313 |
347 |
|
_: &QueueHandle<Self>,
|
| 314 |
348 |
|
_: &wl_surface::WlSurface,
|
| 315 |
|
- |
_: i32,
|
|
349 |
+ |
new_factor: i32,
|
| 316 |
350 |
|
) {
|
|
351 |
+ |
let new_scale = (new_factor.max(1) as u32).min(8);
|
|
352 |
+ |
if new_scale == self.scale {
|
|
353 |
+ |
return;
|
|
354 |
+ |
}
|
|
355 |
+ |
self.scale = new_scale;
|
|
356 |
+ |
match TextRenderer::new(
|
|
357 |
+ |
&self.device,
|
|
358 |
+ |
self.surface_format,
|
|
359 |
+ |
self.font_data.clone(),
|
|
360 |
+ |
FONT_PX * new_scale as f32,
|
|
361 |
+ |
) {
|
|
362 |
+ |
Ok(t) => self.text = t,
|
|
363 |
+ |
Err(e) => {
|
|
364 |
+ |
tracing::warn!("rebuild text at scale {new_scale}: {e:?}");
|
|
365 |
+ |
return;
|
|
366 |
+ |
}
|
|
367 |
+ |
}
|
|
368 |
+ |
// Re-queue a resize with the current logical dims so the surface
|
|
369 |
+ |
// reconfigures at the new physical resolution and set_buffer_scale
|
|
370 |
+ |
// fires.
|
|
371 |
+ |
self.chrome.pending.push_back(Pending::Resized {
|
|
372 |
+ |
width: self.chrome.width,
|
|
373 |
+ |
height: self.chrome.height,
|
|
374 |
+ |
});
|
| 317 |
375 |
|
}
|
| 318 |
376 |
|
fn transform_changed(
|
| 319 |
377 |
|
&mut self,
|