Improve draw_single_line_of_text to support max_width and return (pixels, width) tuple

- Add freetype_text_width_for_single_line() in freetype_render_ui_text.c
- Add cocoa_text_width_for_single_line() in core_text.m
- Add text_width_for_single_line() wrapper in glfw.c for both platforms
- Modify draw_single_line_of_text() to accept optional max_width parameter
- Return (pixels, width) tuple instead of just pixels
- Update all Python call sites in tabs.py and window.py

Agent-Logs-Url: https://github.com/kovidgoyal/kitty/sessions/508483db-ffcd-4d43-a8ee-83fcd3ec9c01

Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-06 05:25:38 +00:00 committed by GitHub
parent c88adfba98
commit 4bcfafa945
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 118 additions and 7 deletions

View file

@ -64,6 +64,7 @@ void cocoa_update_menu_bar_title(PyObject*);
size_t cocoa_get_workspace_ids(void *w, size_t *workspace_ids, size_t array_sz);
monotonic_t cocoa_cursor_blink_interval(void);
bool cocoa_render_line_of_text(const char *text, const color_type fg, const color_type bg, uint8_t *rgba_output, const size_t width, const size_t height);
size_t cocoa_text_width_for_single_line(const char *text, const size_t height);
extern uint8_t* render_single_ascii_char_as_mask(const char ch, size_t *result_width, size_t *result_height);
void get_cocoa_key_equivalent(uint32_t, int, char *key, size_t key_sz, int*);
void set_cocoa_pending_action(CocoaPendingAction action, const char*);

View file

@ -974,6 +974,21 @@ static CTFontRef nerd_font(CGFloat sz) {
return true;
}
size_t
cocoa_text_width_for_single_line(const char *text, const size_t height) {
if (!text || !text[0]) return 0;
if (!ensure_ui_font(height)) return 0;
NSAttributedString *str = [[NSAttributedString alloc] initWithString:@(text) attributes:@{(NSString *)kCTFontAttributeName: (__bridge id)system_ui_font}];
if (!str) return 0;
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)str);
[str release];
if (!line) return 0;
CGFloat ascent, descent, leading;
double width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CFRelease(line);
return (size_t)ceil(width);
}
uint8_t*
render_single_ascii_char_as_mask(const char ch, size_t *result_width, size_t *result_height) {
if (!ensure_ui_font(*result_height)) { PyErr_SetString(PyExc_RuntimeError, "failed to create UI font"); return NULL; }

View file

@ -1846,7 +1846,7 @@ def start_drag_with_data(
operations: int = GLFW_DRAG_OPERATION_MOVE
) -> None: ...
def change_drag_thumbnail(os_window_id: int, idx: int = -1) -> None: ...
def draw_single_line_of_text(os_window_id: int, text: str, fg: int, bg: int, width: int, padding_y: int = 2) -> bytes: ...
def draw_single_line_of_text(os_window_id: int, text: str, fg: int, bg: int, width: int, padding_y: int = 2, max_width: bool = False) -> tuple[bytes, int]: ...
def set_tab_being_dragged(tab_id: int = 0, drag_started: bool = False, x: float = 0, y: float = 0) -> None: ...
def get_tab_being_dragged() -> tuple[int, bool, float, float]: ...
def set_window_being_dragged(window_id: int = 0, drag_started: bool = False, x: float = 0.0, y: float = 0.0) -> None: ...

View file

@ -483,6 +483,77 @@ end:
return ok;
}
size_t
freetype_text_width_for_single_line(FreeTypeRenderCtx ctx_, const char *text, unsigned sz_px) {
RenderCtx *ctx = (RenderCtx*)ctx_;
if (!ctx->created) return 0;
bool has_text = text && text[0];
if (!has_text) return 0;
hb_buffer_clear_contents(hb_buffer);
if (!hb_buffer_pre_allocate(hb_buffer, 512)) { PyErr_NoMemory(); return 0; }
size_t text_len = strlen(text);
char_type *unicode = calloc(text_len + 1, sizeof(char_type));
if (!unicode) { PyErr_NoMemory(); return 0; }
text_len = decode_utf8_string(text, text_len, unicode);
set_pixel_size(ctx, &main_face, sz_px, true);
// Use a very large output_width so nothing is truncated
RenderState rs = {
.current_face = &main_face, .fg = 0, .bg = 0, .horizontally_center = false,
.output_width = SIZE_MAX, .output_height = 0, .stride = 0,
.output = NULL, .x = 0, .y = 0, .sz_px = sz_px
};
for (size_t i = 0; i < text_len; i++) {
bool add_to_current_buffer = false;
char_type codep = unicode[i];
char_type next_codep = unicode[i + 1];
Face *fallback_font = NULL;
if (char_props_for(codep).is_combining_char) {
add_to_current_buffer = true;
} else if (glyph_id_for_codepoint(&main_face, codep) > 0) {
add_to_current_buffer = rs.current_face == &main_face;
if (!add_to_current_buffer) fallback_font = &main_face;
} else {
if (glyph_id_for_codepoint(rs.current_face, codep) > 0) fallback_font = rs.current_face;
else fallback_font = find_fallback_font_for(ctx, codep, next_codep);
add_to_current_buffer = !fallback_font || rs.current_face == fallback_font;
}
if (!add_to_current_buffer) {
if (rs.pending_in_buffer) {
// Shape the current run and accumulate width
hb_buffer_guess_segment_properties(hb_buffer);
set_pixel_size(ctx, rs.current_face, sz_px, false);
hb_shape(rs.current_face->hb, hb_buffer, NULL, 0);
unsigned int len = hb_buffer_get_length(hb_buffer);
hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hb_buffer, NULL);
for (unsigned int j = 0; j < len; j++) {
rs.x += (float)positions[j].x_offset / 64.0f + (float)positions[j].x_advance / 64.0f;
}
rs.pending_in_buffer = 0;
hb_buffer_clear_contents(hb_buffer);
}
if (fallback_font) rs.current_face = fallback_font;
}
hb_buffer_add_utf32(hb_buffer, &codep, 1, 0, 1);
rs.pending_in_buffer += 1;
}
if (rs.pending_in_buffer) {
hb_buffer_guess_segment_properties(hb_buffer);
set_pixel_size(ctx, rs.current_face, sz_px, false);
hb_shape(rs.current_face->hb, hb_buffer, NULL, 0);
unsigned int len = hb_buffer_get_length(hb_buffer);
hb_glyph_position_t *positions = hb_buffer_get_glyph_positions(hb_buffer, NULL);
for (unsigned int j = 0; j < len; j++) {
rs.x += (float)positions[j].x_offset / 64.0f + (float)positions[j].x_advance / 64.0f;
}
hb_buffer_clear_contents(hb_buffer);
}
free(unicode);
return (size_t)ceilf(rs.x);
}
static uint8_t*
render_single_char_bitmap(const FT_Bitmap *bm, size_t *result_width, size_t *result_height) {
*result_width = bm->width; *result_height = bm->rows;

View file

@ -14,6 +14,7 @@ typedef struct {bool created;} *FreeTypeRenderCtx;
FreeTypeRenderCtx create_freetype_render_context(const char *family, bool bold, bool italic);
void set_main_face_family(FreeTypeRenderCtx ctx, const char *family, bool bold, bool italic);
bool render_single_line(FreeTypeRenderCtx ctx, const char *text, unsigned sz_px, uint32_t fg, uint32_t bg, uint8_t *output_buf, size_t width, size_t height, float x_offset, float y_offset, size_t right_margin, bool horizontally_center_runs);
size_t freetype_text_width_for_single_line(FreeTypeRenderCtx ctx, const char *text, unsigned sz_px);
uint8_t* render_single_ascii_char_as_mask(FreeTypeRenderCtx ctx_, const char ch, size_t *result_width, size_t *result_height);
void release_freetype_render_context(FreeTypeRenderCtx ctx);

View file

@ -1160,6 +1160,13 @@ draw_window_title(double font_sz_pts UNUSED, double ydpi UNUSED, const char *tex
return cocoa_render_line_of_text(buf, fg, bg, output_buf, width, height);
}
size_t
text_width_for_single_line(double font_sz_pts UNUSED, double ydpi UNUSED, const char *text, size_t height) {
static char buf[2048];
strip_csi_(text, buf, arraysz(buf));
return cocoa_text_width_for_single_line(buf, height);
}
uint8_t*
draw_single_ascii_char(const char ch, size_t *result_width, size_t *result_height) {
@ -1218,6 +1225,17 @@ draw_window_title(double font_sz_pts, double ydpi, const char *text, color_type
return ok;
}
size_t
text_width_for_single_line(double font_sz_pts, double ydpi, const char *text, size_t height) {
FreeTypeRenderCtx ctx;
if (!(ctx = freetype_render_ctx(false))) return 0;
static char buf[2048];
strip_csi_(text, buf, arraysz(buf));
unsigned px_sz = (unsigned)(font_sz_pts * ydpi / 72.);
px_sz = MIN(px_sz, 3 * height / 4);
return freetype_text_width_for_single_line(ctx, buf, px_sz);
}
uint8_t*
draw_single_ascii_char(const char ch, size_t *result_width, size_t *result_height) {
FreeTypeRenderCtx ctx;
@ -3081,8 +3099,8 @@ draw_single_line_of_text(PyObject *self UNUSED, PyObject *args) {
unsigned long long os_window_id;
const char *text;
unsigned int fg, bg;
int width, padding_y = 2;
if (!PyArg_ParseTuple(args, "KsIIi|i", &os_window_id, &text, &fg, &bg, &width, &padding_y)) return NULL;
int width, padding_y = 2, max_width = 0;
if (!PyArg_ParseTuple(args, "KsIIi|ip", &os_window_id, &text, &fg, &bg, &width, &padding_y, &max_width)) return NULL;
OSWindow *w = os_window_for_id(os_window_id);
if (!w || !w->fonts_data) {
PyErr_SetString(PyExc_KeyError, "OS Window with specified id does not exist or has no fonts data");
@ -3091,13 +3109,17 @@ draw_single_line_of_text(PyObject *self UNUSED, PyObject *args) {
double font_sz_pts = w->fonts_data->font_sz_in_pts;
double ydpi = w->fonts_data->logical_dpi_y;
size_t height = (size_t)w->fonts_data->fcm.cell_height + padding_y;
if (max_width) {
size_t text_w = text_width_for_single_line(font_sz_pts, ydpi, text, height);
if (text_w > 0 && (int)text_w < width) width = (int)text_w;
}
size_t buf_sz = (size_t)width * height * 4;
RAII_PyObject(ans, PyBytes_FromStringAndSize(NULL, buf_sz)); if (!ans) return NULL;
if (!draw_window_title(font_sz_pts, ydpi, text, fg, bg, (uint8_t*)PyBytes_AS_STRING(ans), width, height)) {
if (!PyErr_Occurred()) PyErr_SetString(PyExc_RuntimeError, "Failed to render text");
return NULL;
}
return Py_NewRef(ans);
return Py_BuildValue("Oi", ans, width);
}
static bool

View file

@ -573,6 +573,7 @@ void dispatch_pending_clicks(id_type, void*);
void send_pending_click_to_window(Window*, int);
void get_platform_dependent_config_values(void *glfw_window);
bool draw_window_title(double, double, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height);
size_t text_width_for_single_line(double font_sz_pts, double ydpi, const char *text, size_t height);
uint8_t* draw_single_ascii_char(const char ch, size_t *result_width, size_t *result_height);
bool is_os_window_fullscreen(OSWindow *);
void update_ime_focus(OSWindow* osw, bool focused);

View file

@ -1762,7 +1762,7 @@ def start_tab_drag(self, pixels: bytes, width: int, height: int) -> None:
else:
fg = color_as_int(opts.inactive_tab_foreground)
bg = color_as_int(opts.inactive_tab_background)
title_pixels = draw_single_line_of_text(self.os_window_id, title, 0xff000000 | fg, 0xff000000 | bg, width)
title_pixels, width = draw_single_line_of_text(self.os_window_id, title, 0xff000000 | fg, 0xff000000 | bg, width)
title_height = len(title_pixels) // (width * 4)
thumbnails = ((title_pixels, width, title_height), (title_pixels + pixels, width, title_height + height))
drag_data = {
@ -1870,7 +1870,7 @@ def start_window_drag(self, pixels: bytes, width: int, height: int) -> None:
title = str(w.title or '')
fg = color_as_int(opts.window_title_bar_active_foreground or opts.active_tab_foreground)
bg = color_as_int(opts.window_title_bar_active_background or opts.active_tab_background)
title_pixels = draw_single_line_of_text(self.os_window_id, title, 0xff000000 | fg, 0xff000000 | bg, width)
title_pixels, width = draw_single_line_of_text(self.os_window_id, title, 0xff000000 | fg, 0xff000000 | bg, width)
title_height = len(title_pixels) // (width * 4)
thumbnails = ((title_pixels + pixels, width, title_height + height),)
drag_data = {f'application/net.kovidgoyal.kitty-window-{os.getpid()}': str(window_id).encode()}

View file

@ -1321,7 +1321,7 @@ def drag_url(self, url: str, hyperlink_id: int) -> None:
fg = color_as_int(self.screen.color_profile.default_fg)
bg = color_as_int(self.screen.color_profile.default_bg)
width = self.geometry.right - self.geometry.left
pixels = draw_single_line_of_text(self.os_window_id, url, 0xff000000 | fg, 0xff000000 | bg, width)
pixels, width = draw_single_line_of_text(self.os_window_id, url, 0xff000000 | fg, 0xff000000 | bg, width)
height = len(pixels) // (width * 4)
thumbnails = ((pixels, width, height),)
drag_data = {'text/uri-list': (url + '\r\n').encode()}