mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-03 04:22:43 +08:00
[feat] anchor CLI zoom to mouse and simplify view shortcuts
This commit is contained in:
@@ -58,6 +58,8 @@ struct Grid {
|
||||
Rect Fit(int width, int height, int columns, int pixel_rows,
|
||||
double pixel_aspect = 1);
|
||||
Grid Layout(const DecodedFrame &, Viewport);
|
||||
// Calculate crop and pointer geometry without decoded pixel storage.
|
||||
Grid Layout(unsigned source_width, unsigned source_height, Viewport);
|
||||
Grid Convert(const DecodedFrame &, Viewport);
|
||||
std::optional<std::pair<float, float>> MapPointer(const Grid &, int column,
|
||||
int row);
|
||||
@@ -119,9 +121,11 @@ private:
|
||||
bool pasting_ = false;
|
||||
unsigned mouse_buttons_ = 0;
|
||||
float pointer_x_ = .5f, pointer_y_ = .5f;
|
||||
int mouse_column_ = -1, mouse_row_ = -1;
|
||||
void Tap(ControllerSession &, int key, int modifier = 0);
|
||||
bool LocalKey(int key, ControllerSession &, const Grid *);
|
||||
void Navigate(char key, const Grid *);
|
||||
void Zoom(double next_zoom, const Grid *);
|
||||
void Mouse(int button, int x, int y, bool release, ControllerSession &,
|
||||
const Grid *);
|
||||
};
|
||||
|
||||
+58
-14
@@ -45,8 +45,7 @@ bool Input::LocalKey(int key, ControllerSession &s, const Grid *grid) {
|
||||
} else if (key == 'c') {
|
||||
control = !control;
|
||||
} else if (key == '+' || key == '=' || key == '-' || key == 'f' ||
|
||||
key == 'd' || key == 'i' || key == 'j' || key == 'k' ||
|
||||
key == 'l') {
|
||||
key == 'i' || key == 'j' || key == 'k' || key == 'l') {
|
||||
Navigate(char(key), grid);
|
||||
} else if (!command_mode) {
|
||||
return false;
|
||||
@@ -58,20 +57,52 @@ bool Input::LocalKey(int key, ControllerSession &s, const Grid *grid) {
|
||||
mouse_buttons_ = 0;
|
||||
return true;
|
||||
}
|
||||
void Input::Zoom(double next_zoom, const Grid *grid) {
|
||||
if (next_zoom == zoom)
|
||||
return;
|
||||
if (grid && mouse_column_ >= 0 && mouse_row_ >= 0) {
|
||||
auto v = grid->viewport;
|
||||
v.zoom = zoom;
|
||||
v.native = native;
|
||||
v.center_x = center_x;
|
||||
v.center_y = center_y;
|
||||
// Rebuild from the requested view so queued key/wheel repeats compose
|
||||
// even when the terminal is still displaying an earlier frame.
|
||||
const auto before = Layout(grid->source_width, grid->source_height, v);
|
||||
if (const auto anchor = MapPointer(before, mouse_column_, mouse_row_)) {
|
||||
v.zoom = next_zoom;
|
||||
const auto after = Layout(grid->source_width, grid->source_height, v);
|
||||
if (const auto moved = MapPointer(after, mouse_column_, mouse_row_)) {
|
||||
const auto center = [](int origin, int size, unsigned extent,
|
||||
double shift) {
|
||||
const double half = size * .5 / extent;
|
||||
return std::clamp((origin + size * .5 + shift * (extent - 1)) /
|
||||
extent,
|
||||
half, 1 - half);
|
||||
};
|
||||
// Use the actual, clamped crop center and the same source-pixel
|
||||
// mapping as remote input. Letterboxing and status rows stay aligned.
|
||||
center_x = center(after.source.x, after.source.width,
|
||||
after.source_width,
|
||||
double(anchor->first) - moved->first);
|
||||
center_y = center(after.source.y, after.source.height,
|
||||
after.source_height,
|
||||
double(anchor->second) - moved->second);
|
||||
}
|
||||
}
|
||||
}
|
||||
zoom = next_zoom;
|
||||
}
|
||||
void Input::Navigate(char key, const Grid *grid) {
|
||||
if (key == '+' || key == '=')
|
||||
zoom = std::min(32.0, zoom * 2);
|
||||
Zoom(std::min(32.0, zoom * 2), grid);
|
||||
if (key == '-')
|
||||
zoom = std::max(1.0, zoom / 2);
|
||||
Zoom(std::max(1.0, zoom / 2), grid);
|
||||
if (key == 'f') {
|
||||
native = false;
|
||||
zoom = 1;
|
||||
center_x = center_y = .5;
|
||||
}
|
||||
if (key == 'd') {
|
||||
native = true;
|
||||
zoom = 1;
|
||||
}
|
||||
// Use the displayed crop as the pan step, including native pixel mode.
|
||||
const double dx = grid && grid->source_width
|
||||
? double(grid->source.width) / grid->source_width
|
||||
@@ -99,14 +130,26 @@ void Input::Mouse(int b, int x, int y, bool release, ControllerSession &s,
|
||||
const Grid *grid) {
|
||||
++mouse_reports;
|
||||
// Extra mouse buttons must not alias the three supported remote buttons.
|
||||
if (!grid || command_mode || b < 0 || b >= 128)
|
||||
if (b < 0 || b >= 128 || x <= 0 || y <= 0)
|
||||
return;
|
||||
auto pos = MapPointer(*grid, x - 1, y - 1);
|
||||
// Track terminal coordinates even in local command mode. Re-map them for
|
||||
// every zoom; a stationary mouse must follow subsequent view changes.
|
||||
mouse_column_ = x - 1;
|
||||
mouse_row_ = y - 1;
|
||||
if (!grid || command_mode)
|
||||
return;
|
||||
auto pos = MapPointer(*grid, mouse_column_, mouse_row_);
|
||||
if (!control) {
|
||||
if (pos && !release && ((b & 64) || b == 0)) {
|
||||
center_x = pos->first;
|
||||
center_y = pos->second;
|
||||
Navigate((b & 64) ? ((b & 1) ? '-' : '+') : 'd', grid);
|
||||
if (pos && !release) {
|
||||
if (b & 64) {
|
||||
Navigate((b & 1) ? '-' : '+', grid);
|
||||
} else if (b == 0) {
|
||||
center_x = pos->first;
|
||||
center_y = pos->second;
|
||||
native = true;
|
||||
zoom = 1;
|
||||
++view_revision;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -248,6 +291,7 @@ void Input::Feed(const std::string &bytes, ControllerSession &s,
|
||||
control = false;
|
||||
s.SetControl(false);
|
||||
mouse_buttons_ = 0;
|
||||
mouse_column_ = mouse_row_ = -1;
|
||||
continue;
|
||||
}
|
||||
int key = 0;
|
||||
|
||||
@@ -25,21 +25,25 @@ static uint32_t Pixel(const DecodedFrame &f, int x, int y) {
|
||||
(clamp((298 * yy - 100 * u - 208 * v + 128) >> 8) << 8) |
|
||||
clamp((298 * yy + 516 * u + 128) >> 8);
|
||||
}
|
||||
static void Validate(const DecodedFrame &f, const Viewport &v) {
|
||||
static void Validate(unsigned width, unsigned height, const Viewport &v) {
|
||||
if (v.columns < 1 || v.rows < 1 || v.columns > 240 || v.rows > 100 ||
|
||||
v.row_offset < 0 || v.row_offset > 100 ||
|
||||
!std::isfinite(v.cell_aspect) || v.cell_aspect < .2 ||
|
||||
v.cell_aspect > 2 || !std::isfinite(v.zoom) || v.zoom < 1 ||
|
||||
v.zoom > 32 || !std::isfinite(v.center_x) || !std::isfinite(v.center_y) ||
|
||||
v.center_x < 0 || v.center_x > 1 || v.center_y < 0 || v.center_y > 1 ||
|
||||
!f.width || !f.height || f.width > 8192 || f.height > 8192 ||
|
||||
(f.width & 1) || (f.height & 1) ||
|
||||
size_t(f.width) * f.height > 16777216 ||
|
||||
f.nv12.size() < size_t(f.width) * f.height * 3 / 2)
|
||||
!width || !height || width > 8192 || height > 8192 || (width & 1) ||
|
||||
(height & 1) || size_t(width) * height > 16777216)
|
||||
throw std::invalid_argument("invalid frame or viewport");
|
||||
}
|
||||
static Rect Crop(const DecodedFrame &f, const Viewport &v) {
|
||||
double width = f.width, height = f.height;
|
||||
static void Validate(const DecodedFrame &f, const Viewport &v) {
|
||||
Validate(f.width, f.height, v);
|
||||
if (f.nv12.size() < size_t(f.width) * f.height * 3 / 2)
|
||||
throw std::invalid_argument("invalid frame or viewport");
|
||||
}
|
||||
static Rect Crop(unsigned source_width, unsigned source_height,
|
||||
const Viewport &v) {
|
||||
double width = source_width, height = source_height;
|
||||
if (v.native) {
|
||||
const int sx = v.utf8 && v.detail == AnsiDetail::Quadrant ? 2 : 1;
|
||||
const int sy = v.utf8 ? 2 : 1;
|
||||
@@ -60,8 +64,10 @@ static Rect Crop(const DecodedFrame &f, const Viewport &v) {
|
||||
}
|
||||
int w = std::max(1, int(width / v.zoom)),
|
||||
h = std::max(1, int(height / v.zoom));
|
||||
int x = std::clamp(int(v.center_x * f.width - w * .5), 0, int(f.width) - w);
|
||||
int y = std::clamp(int(v.center_y * f.height - h * .5), 0, int(f.height) - h);
|
||||
int x = std::clamp(int(v.center_x * source_width - w * .5), 0,
|
||||
int(source_width) - w);
|
||||
int y = std::clamp(int(v.center_y * source_height - h * .5), 0,
|
||||
int(source_height) - h);
|
||||
return {x, y, w, h};
|
||||
}
|
||||
// Area resampling integrates the entire source footprint. Thin lines cannot
|
||||
@@ -249,16 +255,20 @@ static void AppendNumber(std::string &out, uint32_t value) {
|
||||
}
|
||||
Grid Layout(const DecodedFrame &f, Viewport v) {
|
||||
Validate(f, v);
|
||||
return Layout(f.width, f.height, v);
|
||||
}
|
||||
Grid Layout(unsigned source_width, unsigned source_height, Viewport v) {
|
||||
Validate(source_width, source_height, v);
|
||||
const int sx = v.utf8 && v.detail == AnsiDetail::Quadrant ? 2 : 1,
|
||||
sy = v.utf8 ? 2 : 1;
|
||||
const int width = v.columns * sx, height = v.rows * sy;
|
||||
Rect crop = Crop(f, v);
|
||||
Rect crop = Crop(source_width, source_height, v);
|
||||
Grid g{v,
|
||||
Fit(crop.width, crop.height, width, height, v.cell_aspect * sy / sx),
|
||||
{}};
|
||||
g.source = crop;
|
||||
g.source_width = f.width;
|
||||
g.source_height = f.height;
|
||||
g.source_width = source_width;
|
||||
g.source_height = source_height;
|
||||
g.samples_x = sx;
|
||||
g.samples_y = sy;
|
||||
return g;
|
||||
@@ -553,7 +563,7 @@ PixelFrame ImagePixels(const DecodedFrame &f, const Viewport &requested) {
|
||||
pixels.height =
|
||||
std::clamp(v.pixel_height > 0 ? v.pixel_height : v.rows * 20, 2, 1080);
|
||||
pixels.rgb.resize(size_t(pixels.width) * pixels.height * 3, 0);
|
||||
const auto crop = Crop(f, v);
|
||||
const auto crop = Crop(f.width, f.height, v);
|
||||
const auto r = Fit(crop.width, crop.height, pixels.width, pixels.height,
|
||||
double(v.columns) * v.cell_aspect * pixels.height /
|
||||
(v.rows * pixels.width));
|
||||
@@ -678,7 +688,7 @@ std::string Kitty(const DecodedFrame &f, const Viewport &requested,
|
||||
std::clamp(v.pixel_width > 0 ? v.pixel_width : v.columns * 10, 2, 1920);
|
||||
int h =
|
||||
std::clamp(v.pixel_height > 0 ? v.pixel_height : v.rows * 20, 2, 1080);
|
||||
const auto crop = Crop(f, v);
|
||||
const auto crop = Crop(f.width, f.height, v);
|
||||
std::string b64;
|
||||
for (;;) {
|
||||
auto r = Fit(crop.width, crop.height, w, h,
|
||||
|
||||
@@ -79,41 +79,52 @@ std::vector<std::string> StatusLines(const StatusBar &bar, int columns) {
|
||||
|
||||
std::string keys, view;
|
||||
if (inner >= 63) {
|
||||
const size_t count = inner >= 99 ? 4 : inner >= 77 ? 3 : 2;
|
||||
const size_t count = inner >= 99 ? 4 : inner >= 89 ? 3 : 2;
|
||||
if (bar.command_mode) {
|
||||
keys =
|
||||
Columns("LOCAL",
|
||||
count >= 3 ? std::vector<std::string>{"<Esc/Ctrl+C> Return",
|
||||
"<c> Toggle control",
|
||||
"<q> Quit"}
|
||||
: std::vector<std::string>{"<Esc/Ctrl+C> Return",
|
||||
"<c> Toggle; <q> Quit"},
|
||||
inner, count);
|
||||
keys = Columns(
|
||||
"LOCAL",
|
||||
count == 4
|
||||
? std::vector<std::string>{"<Esc/Ctrl+C> Return",
|
||||
"<c> Toggle control", "<q> Quit",
|
||||
"<mouse> Set anchor"}
|
||||
: count == 3
|
||||
? std::vector<std::string>{"<Esc/Ctrl+C> Return",
|
||||
"<c> Toggle control", "<q> Quit"}
|
||||
: std::vector<std::string>{"<Esc/Ctrl+C> Return",
|
||||
"<c> Toggle; <q> Quit"},
|
||||
inner, count);
|
||||
} else if (bar.control) {
|
||||
keys = Columns("KEYS", {"<Ctrl+]> Commands", "Input -> remote"}, inner,
|
||||
count);
|
||||
} else {
|
||||
keys = Columns("KEYS",
|
||||
count >= 3
|
||||
? std::vector<std::string>{"<c> Control", "<q> Quit",
|
||||
"<Ctrl+]> Commands"}
|
||||
: std::vector<std::string>{"<c> Control; <q> Quit",
|
||||
"<Ctrl+]> Commands"},
|
||||
inner, count);
|
||||
keys = Columns(
|
||||
"KEYS",
|
||||
count == 4
|
||||
? std::vector<std::string>{"<c> Control", "<q> Quit",
|
||||
"<Ctrl+]> Commands", "<click> Detail"}
|
||||
: count == 3
|
||||
? std::vector<std::string>{"<c> Control; <q> Quit",
|
||||
"<Ctrl+]> Commands", "<click> Detail"}
|
||||
: std::vector<std::string>{"<c> Control; <q> Quit",
|
||||
"<Ctrl+]> Commands"},
|
||||
inner, count);
|
||||
}
|
||||
if (bar.control && !bar.command_mode) {
|
||||
view = Columns("THEN", {"<c> Toggle control", "<q> Quit"}, inner, count);
|
||||
} else {
|
||||
const auto zoom = bar.command_mode ? "<+/-> Zoom at mouse"
|
||||
: "<+/wheel/-> Zoom at mouse";
|
||||
view = Columns(
|
||||
"VIEW",
|
||||
count == 4
|
||||
? std::vector<std::string>{"<arrows/ijkl> Pan", "<+/-> Zoom",
|
||||
"<f> Fit", "<d> Detail"}
|
||||
? std::vector<std::string>{"<+/-> Zoom at mouse",
|
||||
bar.command_mode ? "<=> Zoom in"
|
||||
: "<wheel> Zoom",
|
||||
"<arrows/ijkl> Pan", "<f> Full view"}
|
||||
: count == 3
|
||||
? std::vector<std::string>{"<arrows/ijkl> Pan", "<+/-> Zoom",
|
||||
"<f> Fit; <d> Detail"}
|
||||
: std::vector<std::string>{"<arrows> Pan; <+/-> Zoom",
|
||||
"<f> Fit; <d> Detail"},
|
||||
? std::vector<std::string>{zoom, "<arrows/ijkl> Pan",
|
||||
"<f> Full view"}
|
||||
: std::vector<std::string>{zoom, "<arrows> Pan; <f> Full"},
|
||||
inner, count);
|
||||
}
|
||||
} else {
|
||||
@@ -139,11 +150,18 @@ std::vector<std::string> StatusLines(const StatusBar &bar, int columns) {
|
||||
? std::vector<std::string>{"<c> Toggle control | <q> Quit",
|
||||
"<c> Toggle; <q> Quit", "<c> / <q>",
|
||||
"<q>", "q"}
|
||||
: std::vector<std::string>{"<arrows> Pan | <+/-> Zoom | <f> Fit | "
|
||||
"<d> Detail",
|
||||
"<+/-> Zoom | <f> Fit | <d> Detail",
|
||||
"<f> Fit | <d> Detail", "<f> Fit", "<f>",
|
||||
"f"},
|
||||
: bar.command_mode
|
||||
? std::vector<
|
||||
std::string>{"<+/-> Zoom at mouse | <arrows> Pan | <f> Full",
|
||||
"<+/-> Zoom at mouse | <f> Full",
|
||||
"<+/-> Zoom | <f> Full", "<+/-> | <f>", "<+/->",
|
||||
"+"}
|
||||
: std::vector<std::string>{"<+/wheel/-> Zoom at mouse | <arrows> "
|
||||
"Pan | <f> Full",
|
||||
"<+/wheel/-> Zoom at mouse | <f> Full",
|
||||
"<+/wheel/-> Zoom | <f> Full",
|
||||
"<+/-> Zoom | <f> Full", "<+/-> | <f>",
|
||||
"<+/->", "+"},
|
||||
inner);
|
||||
}
|
||||
std::vector<std::string> lines{status, keys, view};
|
||||
|
||||
Reference in New Issue
Block a user