mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 16:37:27 +00:00
Merge branch 'copilot/implement-drag-thumbnail-text' of https://github.com/kovidgoyal/kitty
This commit is contained in:
commit
7942e7aaac
6 changed files with 126 additions and 10 deletions
64
kitty/dnd.c
64
kitty/dnd.c
|
|
@ -25,6 +25,7 @@ static size_t MIME_LIST_SIZE_CAP = DEFAULT_MIME_LIST_SIZE_CAP;
|
|||
static size_t PRESENT_DATA_CAP = DEFAULT_PRESENT_DATA_CAP;
|
||||
#define DEFAULT_REMOTE_DRAG_LIMIT 1024 * 1024 * 1024
|
||||
static size_t REMOTE_DRAG_LIMIT = DEFAULT_REMOTE_DRAG_LIMIT;
|
||||
#define DRAG_OPACITY_MAX 1024u
|
||||
static PyObject *g_dnd_test_write_func = NULL;
|
||||
static const unsigned file_permissions = 0644;
|
||||
static const unsigned dir_permissions = 0755;
|
||||
|
|
@ -1343,17 +1344,18 @@ drag_add_pre_sent_data(Window *w, unsigned idx, const uint8_t *payload, size_t s
|
|||
#define img ds.images[idx]
|
||||
|
||||
void
|
||||
drag_add_image(Window *w, unsigned idx, int fmt, int width, int height, const uint8_t *payload, size_t sz) {
|
||||
drag_add_image(Window *w, unsigned idx, int fmt, int width, int height, int opacity, const uint8_t *payload, size_t sz) {
|
||||
if (ds.state != DRAG_SOURCE_BEING_BUILT) abrt(EINVAL);
|
||||
if (idx + 1 >= arraysz(ds.images)) abrt(EFBIG);
|
||||
if (ds.images_sent_total_sz + sz > PRESENT_DATA_CAP) abrt(EFBIG);
|
||||
ds.images_sent_total_sz += sz;
|
||||
if (!img.started) {
|
||||
if (fmt != 24 && fmt != 32 && fmt != 100) abrt(EINVAL);
|
||||
if (width < 1 || height < 1) abrt(EINVAL);
|
||||
if (fmt != 0 && fmt != 24 && fmt != 32 && fmt != 100) abrt(EINVAL);
|
||||
if (fmt != 0 && (width < 1 || height < 1)) abrt(EINVAL);
|
||||
img.started = true;
|
||||
img.width = width; img.height = height;
|
||||
img.fmt = fmt;
|
||||
img.opacity = opacity;
|
||||
base64_init_stream_decoder(&img.base64_state);
|
||||
}
|
||||
if (img.capacity < sz + img.sz) {
|
||||
|
|
@ -1420,10 +1422,64 @@ drag_start(Window *w) {
|
|||
case 100:
|
||||
if (!expand_png_data(w, idx)) return;
|
||||
break;
|
||||
case 0: {
|
||||
// Text format: render using draw_window_title
|
||||
OSWindow *osw = os_window_for_kitty_window(w->id);
|
||||
if (!osw || !osw->fonts_data) break; // no fonts available, skip
|
||||
int X = img.width > 0 ? img.width : 1;
|
||||
int Y = img.height > 0 ? img.height : 1;
|
||||
double adjusted_font_sz = osw->fonts_data->font_sz_in_pts * (double)X / (double)Y;
|
||||
double ydpi = osw->fonts_data->logical_dpi_y;
|
||||
double px_sz_d = adjusted_font_sz * ydpi / 72.0;
|
||||
if (px_sz_d < 1.0) px_sz_d = 1.0;
|
||||
size_t render_height = (size_t)(px_sz_d * 4.0 / 3.0 + 0.5);
|
||||
if (render_height < 1) render_height = 1;
|
||||
// White text on a background with the specified opacity (0-DRAG_OPACITY_MAX)
|
||||
color_type fg_color = 0xFFFFFF;
|
||||
uint8_t bg_alpha = (uint8_t)(((uint32_t)img.opacity * 255u + DRAG_OPACITY_MAX / 2) / DRAG_OPACITY_MAX);
|
||||
color_type bg_color = ((uint32_t)bg_alpha) << 24;
|
||||
// Add a null terminator for draw_window_title
|
||||
uint8_t *txt = realloc(img.data, img.sz + 1);
|
||||
if (!txt) { abrt(ENOMEM); return; }
|
||||
img.data = txt; txt[img.sz] = '\0';
|
||||
// Render into a max-width buffer; draw_window_title reduces actual_width
|
||||
// to fit the text, using actual_width as the row stride in the buffer
|
||||
size_t max_width = 4096;
|
||||
uint8_t *render_buf = malloc(max_width * render_height * 4);
|
||||
if (!render_buf) { abrt(ENOMEM); return; }
|
||||
size_t actual_width = max_width;
|
||||
bool ok = draw_window_title(adjusted_font_sz, ydpi, (const char*)img.data,
|
||||
fg_color, bg_color, render_buf,
|
||||
max_width, render_height, &actual_width);
|
||||
if (!ok || actual_width < 1) { free(render_buf); break; }
|
||||
// Shrink the buffer to the actual rendered size (data is already compact
|
||||
// since draw_window_title uses actual_width as the row stride)
|
||||
size_t final_sz = actual_width * render_height * 4;
|
||||
uint8_t *compact = realloc(render_buf, final_sz);
|
||||
if (!compact) { free(render_buf); abrt(ENOMEM); return; }
|
||||
render_buf = compact;
|
||||
// Un-premultiply alpha: draw_window_title output is pre-multiplied RGBA
|
||||
for (size_t j = 0; j < actual_width * render_height; j++) {
|
||||
uint8_t *px = render_buf + j * 4;
|
||||
uint16_t a = px[3];
|
||||
if (a > 0) {
|
||||
px[0] = (uint8_t)(((uint16_t)px[0] * 255u + a / 2u) / a);
|
||||
px[1] = (uint8_t)(((uint16_t)px[1] * 255u + a / 2u) / a);
|
||||
px[2] = (uint8_t)(((uint16_t)px[2] * 255u + a / 2u) / a);
|
||||
}
|
||||
}
|
||||
free(img.data);
|
||||
img.data = render_buf;
|
||||
img.sz = final_sz;
|
||||
img.width = (int)actual_width;
|
||||
img.height = (int)render_height;
|
||||
img.fmt = 32;
|
||||
break;
|
||||
}
|
||||
}
|
||||
total_size += img.sz;
|
||||
if (total_size > 2 * PRESENT_DATA_CAP) abrt(EFBIG);
|
||||
if (img.sz != (size_t)img.width * (size_t)img.height * 4u) abrt(EINVAL);
|
||||
if (img.fmt != 0 && img.sz != (size_t)img.width * (size_t)img.height * 4u) abrt(EINVAL);
|
||||
}
|
||||
}
|
||||
last_total_image_size = total_size;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ typedef enum { DRAG_NOTIFY_ACCEPTED, DRAG_NOTIFY_ACTION_CHANGED, DRAG_NOTIFY_DRO
|
|||
void drag_free_offer(Window *w);
|
||||
void drag_add_mimes(Window *w, int allowed_operations, uint32_t client_id, const char *data, size_t sz, bool has_more);
|
||||
void drag_add_pre_sent_data(Window *w, unsigned idx, const uint8_t *payload, size_t sz);
|
||||
void drag_add_image(Window *w, unsigned idx_, int fmt, int width, int height, const uint8_t *payload, size_t sz);
|
||||
void drag_add_image(Window *w, unsigned idx_, int fmt, int width, int height, int opacity, const uint8_t *payload, size_t sz);
|
||||
void drag_change_image(Window *w, unsigned idx);
|
||||
void drag_start(Window *w);
|
||||
void drag_notify(Window *w, DragNotifyType type);
|
||||
|
|
|
|||
|
|
@ -488,6 +488,26 @@ fallback_font(char_type ch, const char *family, bool bold, bool italic, bool pre
|
|||
ok = _native_fc_match(pat, ans);
|
||||
end:
|
||||
if (pat != NULL) FcPatternDestroy(pat);
|
||||
if (!ok && builtin_nerd_font.face && builtin_nerd_font.descriptor &&
|
||||
glyph_id_for_codepoint(builtin_nerd_font.face, ch) > 0) {
|
||||
PyObject *pypath = PyDict_GetItemString(builtin_nerd_font.descriptor, "path");
|
||||
PyObject *pyindex = PyDict_GetItemString(builtin_nerd_font.descriptor, "index");
|
||||
PyObject *pyhinting = PyDict_GetItemString(builtin_nerd_font.descriptor, "hinting");
|
||||
PyObject *pyhintstyle = PyDict_GetItemString(builtin_nerd_font.descriptor, "hint_style");
|
||||
if (pypath && PyUnicode_Check(pypath)) {
|
||||
const char *path = PyUnicode_AsUTF8(pypath);
|
||||
if (path) {
|
||||
ans->path = strdup(path);
|
||||
if (ans->path) {
|
||||
ans->index = (pyindex && PyLong_Check(pyindex)) ? (int)PyLong_AsLong(pyindex) : 0;
|
||||
ans->hinting = (pyhinting && PyLong_Check(pyhinting)) ? (int)PyLong_AsLong(pyhinting) : 0;
|
||||
ans->hintstyle = (pyhintstyle && PyLong_Check(pyhintstyle)) ? (int)PyLong_AsLong(pyhintstyle) : 0;
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (PyErr_Occurred()) PyErr_Clear();
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1562,7 +1562,7 @@ screen_handle_dnd_command(Screen *self, const DnDCommand *cmd_, const uint8_t *p
|
|||
} break;
|
||||
case 'p': {
|
||||
if (cmd->cell_x >= 0) drag_add_pre_sent_data(w, cmd->cell_x, payload, cmd->payload_sz);
|
||||
else drag_add_image(w, -cmd->cell_x, cmd->cell_y, cmd->pixel_x, cmd->pixel_y, payload, cmd->payload_sz);
|
||||
else drag_add_image(w, -cmd->cell_x, cmd->cell_y, cmd->pixel_x, cmd->pixel_y, (int)cmd->operation, payload, cmd->payload_sz);
|
||||
} break;
|
||||
case 'P': {
|
||||
if (cmd->cell_x >= 0) drag_change_image(w, cmd->cell_x);
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@ typedef struct Window {
|
|||
char *base_dir_for_remote_items; int base_dir_fd_plus_one;
|
||||
} *items;
|
||||
struct {
|
||||
int width, height, fmt; uint8_t *data; size_t sz, capacity; bool started; base64_state base64_state;
|
||||
int width, height, fmt, opacity; uint8_t *data; size_t sz, capacity; bool started; base64_state base64_state;
|
||||
} images[16];
|
||||
size_t pre_sent_total_sz, images_sent_total_sz;
|
||||
unsigned img_idx;
|
||||
|
|
|
|||
|
|
@ -140,14 +140,17 @@ def client_drag_pre_send(idx: int, data_b64: str, client_id: int = 0, more: bool
|
|||
|
||||
def client_drag_add_image(
|
||||
idx: int, fmt: int, width: int, height: int, data_b64: str,
|
||||
client_id: int = 0, more: bool = False,
|
||||
client_id: int = 0, more: bool = False, opacity: int = 0,
|
||||
) -> bytes:
|
||||
"""Escape code for adding an image thumbnail (t=p:x=-idx:y=fmt:X=w:Y=h ; b64).
|
||||
|
||||
*idx*: 1-based image number (will be negated, so idx=1 means x=-1).
|
||||
*fmt*: 24=RGB, 32=RGBA, 100=PNG.
|
||||
*fmt*: 0=text, 24=RGB, 32=RGBA, 100=PNG.
|
||||
*opacity*: background opacity for fmt=0 (0=transparent, 1024=opaque).
|
||||
"""
|
||||
meta = f'{DND_CODE};t=p:x=-{idx}:y={fmt}:X={width}:Y={height}'
|
||||
if opacity:
|
||||
meta += f':o={opacity}'
|
||||
if client_id:
|
||||
meta += f':i={client_id}'
|
||||
if more:
|
||||
|
|
@ -1930,8 +1933,45 @@ class TestDnDProtocol(BaseTest):
|
|||
parse_bytes(screen, client_drag_start())
|
||||
self.assert_error(cap)
|
||||
|
||||
def test_drag_add_image_text_valid(self) -> None:
|
||||
"""Adding a text thumbnail (fmt=0) is accepted without error."""
|
||||
with dnd_test_window() as (screen, cap):
|
||||
self._setup_drag_offer(screen, cap, 'text/plain')
|
||||
text = '📁'
|
||||
data_b64 = standard_b64encode(text.encode()).decode()
|
||||
parse_bytes(screen, client_drag_add_image(1, 0, 1, 1, data_b64))
|
||||
self._assert_no_output(cap)
|
||||
|
||||
def test_drag_add_image_text_zero_scale(self) -> None:
|
||||
"""Text thumbnail with X=0 and Y=0 (default 1/1 scaling) is accepted."""
|
||||
with dnd_test_window() as (screen, cap):
|
||||
self._setup_drag_offer(screen, cap, 'text/plain')
|
||||
text = 'drag me'
|
||||
data_b64 = standard_b64encode(text.encode()).decode()
|
||||
# X=0, Y=0 means default scaling (1/1)
|
||||
parse_bytes(screen, client_drag_add_image(1, 0, 0, 0, data_b64))
|
||||
self._assert_no_output(cap)
|
||||
|
||||
def test_drag_add_image_text_with_opacity(self) -> None:
|
||||
"""Text thumbnail with opacity key is accepted."""
|
||||
with dnd_test_window() as (screen, cap):
|
||||
self._setup_drag_offer(screen, cap, 'text/plain')
|
||||
text = 'file'
|
||||
data_b64 = standard_b64encode(text.encode()).decode()
|
||||
# o=512 means 50% opaque background
|
||||
parse_bytes(screen, client_drag_add_image(1, 0, 1, 1, data_b64, opacity=512))
|
||||
self._assert_no_output(cap)
|
||||
|
||||
def test_drag_add_image_text_fully_opaque(self) -> None:
|
||||
"""Text thumbnail with fully opaque background (o=1024) is accepted."""
|
||||
with dnd_test_window() as (screen, cap):
|
||||
self._setup_drag_offer(screen, cap, 'text/plain')
|
||||
text = 'hello'
|
||||
data_b64 = standard_b64encode(text.encode()).decode()
|
||||
parse_bytes(screen, client_drag_add_image(1, 0, 2, 1, data_b64, opacity=1024))
|
||||
self._assert_no_output(cap)
|
||||
|
||||
|
||||
# ---- Request queue and disambiguation tests --------------------------------
|
||||
|
||||
def test_x_key_echoed_in_data_response(self) -> None:
|
||||
"""x= key is echoed in data responses to identify which request is being answered."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue