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..7cc51d264 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -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 diff --git a/kitty/state.h b/kitty/state.h index fb0a513d0..79d18de09 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -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); 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..baaf1b88f 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) height = len(pixels) // (width * 4) thumbnails = ((pixels, width, height),) drag_data = {'text/uri-list': (url + '\r\n').encode()}