| 31 |
31 |
|
MouseTracking, Point, Selection, SelectionMode,
|
| 32 |
32 |
|
};
|
| 33 |
33 |
|
use shop_pty::{Pty, PtySize};
|
| 34 |
|
- |
use shop_render::{BgFill, CellText, ImagePlacement, ImageRenderer, TextRenderer};
|
|
34 |
+ |
use shop_render::{BgFill, CellMetrics, CellText, ImagePlacement, ImageRenderer, TextRenderer};
|
| 35 |
35 |
|
use shop_wayland::{
|
| 36 |
36 |
|
Capability, CompositorHandler, CompositorState, Connection, OutputHandler, OutputState,
|
| 37 |
37 |
|
Pending, ProvidesRegistryState, QueueHandle, RegistryState, SeatHandler, SeatState,
|
| 75 |
75 |
|
/// get their icons without a system font install. ~13 MB in the binary.
|
| 76 |
76 |
|
const FONT_BYTES: &[u8] = include_bytes!("../../../assets/IosevkaTermNerdFontMono-Regular.ttf");
|
| 77 |
77 |
|
const FONT_PX: f32 = 14.0;
|
| 78 |
|
- |
const CELL_ADVANCE: f32 = 8.0;
|
| 79 |
|
- |
const CELL_HEIGHT: f32 = 17.0;
|
| 80 |
78 |
|
const PAD_X: f32 = 10.0;
|
| 81 |
79 |
|
const PAD_Y: f32 = 6.0;
|
| 82 |
80 |
|
/// How solid the cursor is at each end of its phase.
|
| 269 |
267 |
|
|
| 270 |
268 |
|
let font_data: Vec<u8> = FONT_BYTES.to_vec();
|
| 271 |
269 |
|
|
|
270 |
+ |
// The cell, measured off the bundled face rather than declared. Done here
|
|
271 |
+ |
// because the PTY is sized in cells and is spawned long before there is a
|
|
272 |
+ |
// wgpu device to ask, which is why this takes bytes and not a renderer.
|
|
273 |
+ |
let cell = CellMetrics::measure(FONT_BYTES, FONT_PX)?;
|
|
274 |
+ |
let (dx, dy) = cell.rounding_error();
|
|
275 |
+ |
if dx.abs() > 0.01 || dy.abs() > 0.01 {
|
|
276 |
+ |
// Not a failure, and worth saying once: this residual is exactly what
|
|
277 |
+ |
// stops box drawing tiling perfectly, so a face cut for this terminal
|
|
278 |
+ |
// wants metrics that land on whole pixels at the sizes it runs at.
|
|
279 |
+ |
tracing::debug!(
|
|
280 |
+ |
advance = cell.exact_advance,
|
|
281 |
+ |
height = cell.exact_height,
|
|
282 |
+ |
cell_w = cell.advance,
|
|
283 |
+ |
cell_h = cell.height,
|
|
284 |
+ |
"cell rounded off the face by ({dx:.2}, {dy:.2}) px"
|
|
285 |
+ |
);
|
|
286 |
+ |
}
|
|
287 |
+ |
|
| 272 |
288 |
|
let Cli {
|
| 273 |
289 |
|
exec_cmd,
|
| 274 |
290 |
|
exec_argv,
|
| 284 |
300 |
|
let (spawn_cmd, spawn_args) =
|
| 285 |
301 |
|
spawn_target(exec_argv, exec_cmd.clone(), std::env::var("SHELL").ok());
|
| 286 |
302 |
|
let spawn_args_refs: Vec<&str> = spawn_args.iter().map(String::as_str).collect();
|
| 287 |
|
- |
let cols_initial = grid_cols(INITIAL.0);
|
| 288 |
|
- |
let rows_initial = grid_rows(INITIAL.1);
|
|
303 |
+ |
let cols_initial = grid_cols(INITIAL.0, cell);
|
|
304 |
+ |
let rows_initial = grid_rows(INITIAL.1, cell);
|
| 289 |
305 |
|
let mut pty = Pty::spawn(
|
| 290 |
306 |
|
&spawn_cmd,
|
| 291 |
307 |
|
&spawn_args_refs,
|
| 292 |
308 |
|
PtySize {
|
| 293 |
309 |
|
cols: cols_initial,
|
| 294 |
310 |
|
rows: rows_initial,
|
| 295 |
|
- |
cell_width: CELL_ADVANCE.round() as u16,
|
| 296 |
|
- |
cell_height: CELL_HEIGHT.round() as u16,
|
|
311 |
+ |
cell_width: cell.advance as u16,
|
|
312 |
+ |
cell_height: cell.height as u16,
|
| 297 |
313 |
|
},
|
| 298 |
314 |
|
"xterm-256color",
|
| 299 |
315 |
|
)?;
|
| 437 |
453 |
|
parser,
|
| 438 |
454 |
|
kitty: kgp::Parser::new(),
|
| 439 |
455 |
|
palette,
|
|
456 |
+ |
cell,
|
| 440 |
457 |
|
image_placement: None,
|
| 441 |
458 |
|
next_anon_image_id: 1,
|
| 442 |
459 |
|
font_data: FONT_BYTES.to_vec(),
|
| 477 |
494 |
|
app.surface_config.width,
|
| 478 |
495 |
|
app.surface_config.height,
|
| 479 |
496 |
|
);
|
| 480 |
|
- |
let id = identity(&app.palette, app.scale);
|
|
497 |
+ |
let id = identity(&app.palette, app.scale, app.cell);
|
| 481 |
498 |
|
app.grid.set_identity(id);
|
| 482 |
499 |
|
|
| 483 |
500 |
|
// Wayland + PTY sources onto the loop built above.
|
| 577 |
594 |
|
.xdg_window
|
| 578 |
595 |
|
.wl_surface()
|
| 579 |
596 |
|
.set_buffer_scale(s as i32);
|
| 580 |
|
- |
let cols = grid_cols(logical_w);
|
| 581 |
|
- |
let rows = grid_rows(logical_h);
|
|
597 |
+ |
let cols = grid_cols(logical_w, app.cell);
|
|
598 |
+ |
let rows = grid_rows(logical_h, app.cell);
|
| 582 |
599 |
|
app.grid.resize(cols, rows);
|
| 583 |
600 |
|
// Scale may have moved since the last configure, and cell
|
| 584 |
601 |
|
// size is reported in physical pixels.
|
| 585 |
|
- |
let id = identity(&app.palette, s);
|
|
602 |
+ |
let id = identity(&app.palette, s, app.cell);
|
| 586 |
603 |
|
app.grid.set_identity(id);
|
| 587 |
604 |
|
let _ = app.pty.resize(PtySize {
|
| 588 |
605 |
|
cols,
|
| 589 |
606 |
|
rows,
|
| 590 |
|
- |
cell_width: (CELL_ADVANCE * s as f32).round() as u16,
|
| 591 |
|
- |
cell_height: (CELL_HEIGHT * s as f32).round() as u16,
|
|
607 |
+ |
cell_width: (app.cell.advance * s as f32).round() as u16,
|
|
608 |
+ |
cell_height: (app.cell.height * s as f32).round() as u16,
|
| 592 |
609 |
|
});
|
| 593 |
610 |
|
app.dirty = true;
|
| 594 |
611 |
|
}
|
| 718 |
735 |
|
if control.action == 'T' {
|
| 719 |
736 |
|
let s = app.scale as f32;
|
| 720 |
737 |
|
let cursor = app.grid.cursor();
|
| 721 |
|
- |
let x = (PAD_X + cursor.col as f32 * CELL_ADVANCE) * s;
|
| 722 |
|
- |
let y = (PAD_Y + cursor.row as f32 * CELL_HEIGHT) * s;
|
|
738 |
+ |
let x = (PAD_X + cursor.col as f32 * app.cell.advance) * s;
|
|
739 |
+ |
let y = (PAD_Y + cursor.row as f32 * app.cell.height) * s;
|
| 723 |
740 |
|
// Prefer the cell-sized placement (`c=`, `r=`) if given; fall
|
| 724 |
741 |
|
// back to the source pixel dims scaled up. yazi always sends
|
| 725 |
742 |
|
// `c` and `r`.
|
| 726 |
743 |
|
let placement_w = control
|
| 727 |
744 |
|
.cell_cols
|
| 728 |
|
- |
.map_or(w as f32, |c| c as f32 * CELL_ADVANCE * s);
|
|
745 |
+ |
.map_or(w as f32, |c| c as f32 * app.cell.advance * s);
|
| 729 |
746 |
|
let placement_h = control
|
| 730 |
747 |
|
.cell_rows
|
| 731 |
|
- |
.map_or(h as f32, |r| r as f32 * CELL_HEIGHT * s);
|
|
748 |
+ |
.map_or(h as f32, |r| r as f32 * app.cell.height * s);
|
| 732 |
749 |
|
app.image_placement = Some(ImagePlacement {
|
| 733 |
750 |
|
image_id,
|
| 734 |
751 |
|
x,
|
| 764 |
781 |
|
|
| 765 |
782 |
|
/// The cell under a surface-local position.
|
| 766 |
783 |
|
///
|
| 767 |
|
- |
/// Positions arrive in logical pixels, which is also what the padding and cell
|
| 768 |
|
- |
/// constants are in, so scale does not enter into this. Clamped rather than
|
|
784 |
+ |
/// Positions arrive in logical pixels, which is also what the padding and the
|
|
785 |
+ |
/// cell are in, so scale does not enter into this. Clamped rather than
|
| 769 |
786 |
|
/// optional: a pointer out in the padding is treated as the nearest cell,
|
| 770 |
787 |
|
/// which is what makes dragging off the edge of the window select to the end
|
| 771 |
788 |
|
/// of the line instead of stopping dead.
|
| 772 |
|
- |
fn cell_at((x, y): (f64, f64), cols: u16, rows: u16) -> Point {
|
| 773 |
|
- |
let col = ((x - f64::from(PAD_X)) / f64::from(CELL_ADVANCE)).floor();
|
| 774 |
|
- |
let row = ((y - f64::from(PAD_Y)) / f64::from(CELL_HEIGHT)).floor();
|
|
789 |
+ |
fn cell_at((x, y): (f64, f64), cols: u16, rows: u16, cell: CellMetrics) -> Point {
|
|
790 |
+ |
let col = ((x - f64::from(PAD_X)) / f64::from(cell.advance)).floor();
|
|
791 |
+ |
let row = ((y - f64::from(PAD_Y)) / f64::from(cell.height)).floor();
|
| 775 |
792 |
|
let to_index = |v: f64, count: u16| v.clamp(0.0, f64::from(count.saturating_sub(1))) as u16;
|
| 776 |
793 |
|
Point::new(to_index(row, rows), to_index(col, cols))
|
| 777 |
794 |
|
}
|
| 823 |
840 |
|
/// theme, which is the only thing that knows them: a program asking OSC 11
|
| 824 |
841 |
|
/// whether it is on a light or a dark terminal gets the wrong answer from
|
| 825 |
842 |
|
/// anything else.
|
| 826 |
|
- |
fn identity(palette: &Palette, scale: u32) -> shop_grid::Identity {
|
|
843 |
+ |
fn identity(palette: &Palette, scale: u32, cell: CellMetrics) -> shop_grid::Identity {
|
| 827 |
844 |
|
let s = scale.max(1) as f32;
|
| 828 |
845 |
|
shop_grid::Identity {
|
| 829 |
846 |
|
name: "shop".into(),
|
| 830 |
847 |
|
version: env!("CARGO_PKG_VERSION").into(),
|
| 831 |
848 |
|
cell_px: (
|
| 832 |
|
- |
(CELL_ADVANCE * s).round() as u16,
|
| 833 |
|
- |
(CELL_HEIGHT * s).round() as u16,
|
|
849 |
+ |
(cell.advance * s).round() as u16,
|
|
850 |
+ |
(cell.height * s).round() as u16,
|
| 834 |
851 |
|
),
|
| 835 |
852 |
|
fg: srgb_bytes(palette.fg),
|
| 836 |
853 |
|
bg: srgb_bytes(palette.bg),
|
| 846 |
863 |
|
]
|
| 847 |
864 |
|
}
|
| 848 |
865 |
|
|
| 849 |
|
- |
fn grid_cols(px_w: u32) -> u16 {
|
| 850 |
|
- |
let usable = (px_w as f32 - 2.0 * PAD_X).max(CELL_ADVANCE);
|
| 851 |
|
- |
(usable / CELL_ADVANCE) as u16
|
|
866 |
+ |
fn grid_cols(px_w: u32, cell: CellMetrics) -> u16 {
|
|
867 |
+ |
let usable = (px_w as f32 - 2.0 * PAD_X).max(cell.advance);
|
|
868 |
+ |
(usable / cell.advance) as u16
|
| 852 |
869 |
|
}
|
| 853 |
870 |
|
|
| 854 |
|
- |
fn grid_rows(px_h: u32) -> u16 {
|
| 855 |
|
- |
let usable = (px_h as f32 - 2.0 * PAD_Y).max(CELL_HEIGHT);
|
| 856 |
|
- |
(usable / CELL_HEIGHT) as u16
|
|
871 |
+ |
fn grid_rows(px_h: u32, cell: CellMetrics) -> u16 {
|
|
872 |
+ |
let usable = (px_h as f32 - 2.0 * PAD_Y).max(cell.height);
|
|
873 |
+ |
(usable / cell.height) as u16
|
| 857 |
874 |
|
}
|
| 858 |
875 |
|
|
| 859 |
876 |
|
struct App {
|
| 874 |
891 |
|
kitty: kgp::Parser,
|
| 875 |
892 |
|
/// Every colour shop paints, resolved from a makeover theme at startup.
|
| 876 |
893 |
|
palette: Palette,
|
|
894 |
+ |
/// The cell, measured off the bundled face at [`FONT_PX`].
|
|
895 |
+ |
///
|
|
896 |
+ |
/// Logical pixels, so every physical use multiplies by `scale`. Held
|
|
897 |
+ |
/// rather than recomputed per frame, and not recomputed on a scale change:
|
|
898 |
+ |
/// a logical cell is what the grid, the padding and pointer positions are
|
|
899 |
+ |
/// all in, and it does not move when the output does. The renderer is
|
|
900 |
+ |
/// rebuilt at `FONT_PX * scale`, so at a fractional advance the physical
|
|
901 |
+ |
/// glyph and `advance * scale` can differ by under a pixel; the bundled
|
|
902 |
+ |
/// face is a clean half-em and does not.
|
|
903 |
+ |
cell: CellMetrics,
|
| 877 |
904 |
|
/// Single-image MVP: track the most recently displayed image + where.
|
| 878 |
905 |
|
/// Multi-image / z-order / delete-selectors are follow-ups.
|
| 879 |
906 |
|
image_placement: Option<ImagePlacement>,
|
| 1031 |
1058 |
|
// Damage flow: apply the grid's pending changes to the per-row cache,
|
| 1032 |
1059 |
|
// rebuild only dirty rows, then draw from cache.
|
| 1033 |
1060 |
|
let s = app.scale as f32;
|
| 1034 |
|
- |
let cell_w_px = CELL_ADVANCE * s;
|
| 1035 |
|
- |
let cell_h_px = CELL_HEIGHT * s;
|
|
1061 |
+ |
let cell_w_px = app.cell.advance * s;
|
|
1062 |
+ |
let cell_h_px = app.cell.height * s;
|
| 1036 |
1063 |
|
let pad_x_px = PAD_X * s;
|
| 1037 |
1064 |
|
let pad_y_px = PAD_Y * s;
|
| 1038 |
1065 |
|
let damage = app.grid.take_damage();
|
| 1790 |
1817 |
|
|
| 1791 |
1818 |
|
impl App {
|
| 1792 |
1819 |
|
fn cell_at(&self, pos: (f64, f64)) -> Point {
|
| 1793 |
|
- |
let at = cell_at(pos, self.grid.cols(), self.grid.rows());
|
|
1820 |
+ |
let at = cell_at(pos, self.grid.cols(), self.grid.rows(), self.cell);
|
| 1794 |
1821 |
|
// A wide character is one thing under two columns; a click on its right
|
| 1795 |
1822 |
|
// half means the character, not the blank standing in for it.
|
| 1796 |
1823 |
|
Point::new(at.row, self.grid.snap_col(at.row, at.col))
|
| 2061 |
2088 |
|
if self.grid.mouse_tracking() == MouseTracking::Off || self.modifiers.shift {
|
| 2062 |
2089 |
|
return false;
|
| 2063 |
2090 |
|
}
|
| 2064 |
|
- |
let at = cell_at(self.pointer_at, self.grid.cols(), self.grid.rows());
|
|
2091 |
+ |
let at = cell_at(
|
|
2092 |
+ |
self.pointer_at,
|
|
2093 |
+ |
self.grid.cols(),
|
|
2094 |
+ |
self.grid.rows(),
|
|
2095 |
+ |
self.cell,
|
|
2096 |
+ |
);
|
| 2065 |
2097 |
|
// Motion is continuous and reports are per cell, so a move that has
|
| 2066 |
2098 |
|
// not left its cell has nothing to say. Presses and releases always
|
| 2067 |
2099 |
|
// do, however still the pointer was.
|
| 2346 |
2378 |
|
Point::new(row, col)
|
| 2347 |
2379 |
|
}
|
| 2348 |
2380 |
|
|
|
2381 |
+ |
/// The real cell, measured off the real bundled face.
|
|
2382 |
+ |
///
|
|
2383 |
+ |
/// Not a fixture. Hit-testing against numbers a test made up would pass
|
|
2384 |
+ |
/// while the terminal put the pointer in the wrong cell, which is the
|
|
2385 |
+ |
/// class of bug this whole change is about.
|
|
2386 |
+ |
fn test_cell() -> CellMetrics {
|
|
2387 |
+ |
CellMetrics::measure(FONT_BYTES, FONT_PX).expect("the bundled face measures")
|
|
2388 |
+ |
}
|
|
2389 |
+ |
|
|
2390 |
+ |
// The regression guard the two constants never had. They were 8.0 x 17.0
|
|
2391 |
+ |
// against a face that is 7.0 x 17.5 at this size, so every column carried a
|
|
2392 |
+ |
// pixel of dead space and every row lost half of one.
|
|
2393 |
+ |
#[test]
|
|
2394 |
+ |
fn the_cell_is_the_bundled_faces_own_and_not_a_number_someone_typed() {
|
|
2395 |
+ |
let cell = test_cell();
|
|
2396 |
+ |
assert!((cell.advance - 7.0).abs() < f32::EPSILON, "{cell:?}");
|
|
2397 |
+ |
assert!((cell.height - 18.0).abs() < f32::EPSILON, "{cell:?}");
|
|
2398 |
+ |
assert!(
|
|
2399 |
+ |
(cell.advance - cell.exact_advance).abs() < f32::EPSILON,
|
|
2400 |
+ |
"this face's advance is a whole pixel at 14px, so nothing is rounded away"
|
|
2401 |
+ |
);
|
|
2402 |
+ |
}
|
|
2403 |
+ |
|
|
2404 |
+ |
// Cells tile: what the grid steps by is what a glyph fills. Asserted
|
|
2405 |
+ |
// through the same two functions the resize path uses rather than by
|
|
2406 |
+ |
// arithmetic, so a change to either is caught.
|
|
2407 |
+ |
#[test]
|
|
2408 |
+ |
fn a_window_divides_into_whole_cells_with_only_the_padding_left_over() {
|
|
2409 |
+ |
let cell = test_cell();
|
|
2410 |
+ |
let width = 960;
|
|
2411 |
+ |
let cols = grid_cols(width, cell);
|
|
2412 |
+ |
let used = f32::from(cols) * cell.advance + 2.0 * PAD_X;
|
|
2413 |
+ |
assert!(
|
|
2414 |
+ |
used <= width as f32,
|
|
2415 |
+ |
"{cols} columns need {used}px of {width}"
|
|
2416 |
+ |
);
|
|
2417 |
+ |
assert!(
|
|
2418 |
+ |
used > width as f32 - cell.advance,
|
|
2419 |
+ |
"another column would have fitted"
|
|
2420 |
+ |
);
|
|
2421 |
+ |
}
|
|
2422 |
+ |
|
| 2349 |
2423 |
|
fn argv(items: &[&str]) -> Vec<String> {
|
| 2350 |
2424 |
|
items.iter().map(|s| (*s).to_string()).collect()
|
| 2351 |
2425 |
|
}
|
| 2638 |
2712 |
|
#[test]
|
| 2639 |
2713 |
|
fn the_origin_cell_starts_after_the_padding() {
|
| 2640 |
2714 |
|
assert_eq!(
|
| 2641 |
|
- |
cell_at((f64::from(PAD_X), f64::from(PAD_Y)), 80, 24),
|
|
2715 |
+ |
cell_at((f64::from(PAD_X), f64::from(PAD_Y)), 80, 24, test_cell()),
|
| 2642 |
2716 |
|
at(0, 0)
|
| 2643 |
2717 |
|
);
|
| 2644 |
2718 |
|
}
|
| 2645 |
2719 |
|
|
| 2646 |
2720 |
|
#[test]
|
| 2647 |
2721 |
|
fn a_position_maps_to_the_cell_it_is_inside() {
|
| 2648 |
|
- |
let x = f64::from(PAD_X) + f64::from(CELL_ADVANCE) * 3.5;
|
| 2649 |
|
- |
let y = f64::from(PAD_Y) + f64::from(CELL_HEIGHT) * 2.5;
|
| 2650 |
|
- |
assert_eq!(cell_at((x, y), 80, 24), at(2, 3));
|
|
2722 |
+ |
let cell = test_cell();
|
|
2723 |
+ |
let x = f64::from(PAD_X) + f64::from(cell.advance) * 3.5;
|
|
2724 |
+ |
let y = f64::from(PAD_Y) + f64::from(cell.height) * 2.5;
|
|
2725 |
+ |
assert_eq!(cell_at((x, y), 80, 24, cell), at(2, 3));
|
| 2651 |
2726 |
|
}
|
| 2652 |
2727 |
|
|
| 2653 |
2728 |
|
#[test]
|
| 2654 |
2729 |
|
fn the_padding_clamps_to_the_nearest_cell_rather_than_falling_off() {
|
| 2655 |
2730 |
|
// Above and left of the grid entirely.
|
| 2656 |
|
- |
assert_eq!(cell_at((0.0, 0.0), 80, 24), at(0, 0));
|
|
2731 |
+ |
assert_eq!(cell_at((0.0, 0.0), 80, 24, test_cell()), at(0, 0));
|
| 2657 |
2732 |
|
// Far below and right — a drag that left the window.
|
| 2658 |
|
- |
assert_eq!(cell_at((100_000.0, 100_000.0), 80, 24), at(23, 79));
|
|
2733 |
+ |
assert_eq!(
|
|
2734 |
+ |
cell_at((100_000.0, 100_000.0), 80, 24, test_cell()),
|
|
2735 |
+ |
at(23, 79)
|
|
2736 |
+ |
);
|
| 2659 |
2737 |
|
// Negative, which is what a drag past the top-left edge reports.
|
| 2660 |
|
- |
assert_eq!(cell_at((-500.0, -500.0), 80, 24), at(0, 0));
|
|
2738 |
+ |
assert_eq!(cell_at((-500.0, -500.0), 80, 24, test_cell()), at(0, 0));
|
| 2661 |
2739 |
|
}
|
| 2662 |
2740 |
|
|
| 2663 |
2741 |
|
#[test]
|
| 2664 |
2742 |
|
fn a_one_by_one_grid_has_no_cell_to_clamp_past() {
|
| 2665 |
|
- |
assert_eq!(cell_at((10_000.0, 10_000.0), 1, 1), at(0, 0));
|
|
2743 |
+ |
assert_eq!(cell_at((10_000.0, 10_000.0), 1, 1, test_cell()), at(0, 0));
|
| 2666 |
2744 |
|
}
|
| 2667 |
2745 |
|
|
| 2668 |
2746 |
|
#[test]
|