diff --git a/kitty/cocoa_window.h b/kitty/cocoa_window.h index 375f3fa89..3942f9e82 100644 --- a/kitty/cocoa_window.h +++ b/kitty/cocoa_window.h @@ -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*); diff --git a/kitty/core_text.m b/kitty/core_text.m index c988a0c0e..d8fef7e29 100644 --- a/kitty/core_text.m +++ b/kitty/core_text.m @@ -974,6 +974,21 @@ cocoa_render_line_of_text(const char *text, const color_type fg, const color_typ 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; } diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 73d6feb66..05f65ffe9 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -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: ... diff --git a/kitty/freetype_render_ui_text.c b/kitty/freetype_render_ui_text.c index bed467ddf..d14d0eec0 100644 --- a/kitty/freetype_render_ui_text.c +++ b/kitty/freetype_render_ui_text.c @@ -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; diff --git a/kitty/freetype_render_ui_text.h b/kitty/freetype_render_ui_text.h index 5c7d4366b..5b1d59502 100644 --- a/kitty/freetype_render_ui_text.h +++ b/kitty/freetype_render_ui_text.h @@ -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); diff --git a/kitty/glfw.c b/kitty/glfw.c index 15f35d94e..89bacf884 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -1154,9 +1154,14 @@ apple_url_open_callback(const char* url) { bool -draw_window_title(double font_sz_pts UNUSED, double ydpi UNUSED, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height) { +draw_window_title(double font_sz_pts UNUSED, double ydpi UNUSED, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height, size_t *actual_width) { static char buf[2048]; strip_csi_(text, buf, arraysz(buf)); + if (actual_width) { + size_t text_w = cocoa_text_width_for_single_line(buf, height); + if (text_w > 0 && text_w < width) width = text_w; + *actual_width = width; + } return cocoa_render_line_of_text(buf, fg, bg, output_buf, width, height); } @@ -1204,13 +1209,18 @@ draw_text_callback(GLFWwindow *window, const char *text, uint32_t fg, uint32_t b } bool -draw_window_title(double font_sz_pts, double ydpi, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height) { +draw_window_title(double font_sz_pts, double ydpi, const char *text, color_type fg, color_type bg, uint8_t *output_buf, size_t width, size_t height, size_t *actual_width) { FreeTypeRenderCtx ctx; if (!(ctx = freetype_render_ctx(false))) return false; 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); + if (actual_width) { + size_t text_w = freetype_text_width_for_single_line(ctx, buf, px_sz); + if (text_w > 0 && text_w < width) width = text_w; + *actual_width = width; + } #define RGB2BGR(x) (x & 0xFF000000) | ((x & 0xFF0000) >> 16) | (x & 0x00FF00) | ((x & 0x0000FF) << 16) bool ok = render_single_line(ctx, buf, px_sz, RGB2BGR(fg), RGB2BGR(bg), output_buf, width, height, 0, 0, 0, false); #undef RGB2BGR @@ -3077,12 +3087,13 @@ grab_keyboard(PyObject *self UNUSED, PyObject *action) { } static PyObject* -draw_single_line_of_text(PyObject *self UNUSED, PyObject *args) { +draw_single_line_of_text(PyObject *self UNUSED, PyObject *args, PyObject *kw) { 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; + static const char* kwlist[] = {"os_window_id", "text", "fg", "bg", "width", "padding_y", "max_width", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kw, "KsIIi|ip", (char**)kwlist, &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"); @@ -3093,11 +3104,15 @@ draw_single_line_of_text(PyObject *self UNUSED, PyObject *args) { size_t height = (size_t)w->fonts_data->fcm.cell_height + padding_y; 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)) { + size_t actual_width = width; + if (!draw_window_title(font_sz_pts, ydpi, text, fg, bg, (uint8_t*)PyBytes_AS_STRING(ans), width, height, max_width ? &actual_width : NULL)) { if (!PyErr_Occurred()) PyErr_SetString(PyExc_RuntimeError, "Failed to render text"); return NULL; } - return Py_NewRef(ans); + if (actual_width < (size_t)width) { + if (_PyBytes_Resize(&ans, actual_width * height * 4) < 0) return NULL; + } + return Py_BuildValue("Oi", ans, (int)actual_width); } static bool @@ -3242,7 +3257,7 @@ static PyMethodDef module_methods[] = { {"create_os_window", (PyCFunction)(void (*) (void))(create_os_window), METH_VARARGS | METH_KEYWORDS, NULL}, {"start_drag_with_data", (PyCFunction)(void (*) (void))(start_drag_with_data), METH_VARARGS | METH_KEYWORDS, NULL}, METHODB(change_drag_thumbnail, METH_VARARGS), - METHODB(draw_single_line_of_text, METH_VARARGS), + {"draw_single_line_of_text", (PyCFunction)(void (*) (void))(draw_single_line_of_text), METH_VARARGS | METH_KEYWORDS, NULL}, METHODB(set_default_window_icon, METH_VARARGS), METHODB(set_os_window_icon, METH_VARARGS), METHODB(set_clipboard_data_types, METH_VARARGS), diff --git a/kitty/shaders.c b/kitty/shaders.c index 51f2c2c58..2d50e4d49 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -845,7 +845,7 @@ render_a_bar(const UIRenderData *ui, WindowBarData *bar, PyObject *title, bool a static char titlebuf[2048] = {0}; if (!title) return 0; snprintf(titlebuf, arraysz(titlebuf), " %s", PyUnicode_AsUTF8(title)); - if (!draw_window_title(ui->os_window->fonts_data->font_sz_in_pts, ui->os_window->fonts_data->logical_dpi_y, titlebuf, fg, bg, bar->buf, bar_width, bar_height)) return 0; + if (!draw_window_title(ui->os_window->fonts_data->font_sz_in_pts, ui->os_window->fonts_data->logical_dpi_y, titlebuf, fg, bg, bar->buf, bar_width, bar_height, NULL)) return 0; Py_CLEAR(bar->last_drawn_title_object_id); bar->last_drawn_title_object_id = Py_NewRef(title); } diff --git a/kitty/state.h b/kitty/state.h index fb0a513d0..9eae12db6 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -572,7 +572,7 @@ const char* format_mods(unsigned mods); 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); +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 *actual_width); 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); diff --git a/kitty/tabs.py b/kitty/tabs.py index f30d245ab..a1026681f 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -1762,7 +1762,7 @@ class TabManager: # {{{ 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 @@ class TabManager: # {{{ 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()} diff --git a/kitty/window.py b/kitty/window.py index 0f08e0cd3..39e27e468 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -1321,7 +1321,7 @@ class Window: 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, max_width=True) height = len(pixels) // (width * 4) thumbnails = ((pixels, width, height),) drag_data = {'text/uri-list': (url + '\r\n').encode()}