From 1c73f757df65c466d6fc57129dee22cdade250f8 Mon Sep 17 00:00:00 2001 From: RubenRME Date: Mon, 31 Mar 2025 03:46:41 +0200 Subject: [PATCH 001/114] lang: added Korean language file --- po/ko_KR.UTF-8.po | 259 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 po/ko_KR.UTF-8.po diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po new file mode 100644 index 000000000..48659c388 --- /dev/null +++ b/po/ko_KR.UTF-8.po @@ -0,0 +1,259 @@ +# Korean translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Ruben Engelbrecht , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:54-0700\n" +"PO-Revision-Date: 2025-03-31 03:08+0200\n" +"Last-Translator: Ruben Engelbrecht \n" +"Language-Team: Korean \n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "터미널 제목 변경" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "기본 제목으로 복원하려면 비워 두세요." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "취소" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "확인" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "설정 오류" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "하나 이상의 설정 오류가 발견되었습니다. 아래 오류를 확인한 후 설정을 다시 로드하거나 무시하세요." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "무시" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Reload Configuration" +msgstr "설정을 다시 로드하기" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "복사" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "붙여넣기" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "지우기" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "초기화" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "나누기" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "제목 변경…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "위로 창 나누기" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "아래로 창 나누기" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "왼쪽으로 창 나누기" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "오른쪽으로 창 나누기" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "탭" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:246 +msgid "New Tab" +msgstr "새 탭" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "탭 닫기" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "창" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "새 창" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "창 닫기" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "설정" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Open Configuration" +msgstr "설정 열기" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "터미널 인스펙터" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Ghostty 정보" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +msgid "Quit" +msgstr "종료" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "클립보드 액세스 권한 부여" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "응용 프로그램이 클립보드에서 읽기를 시도하고 있습니다. 현재 클립보드 내용은 아래와 같습니다." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "거부" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "허용" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "응용 프로그램이 클립보드에 쓰기를 시도하고 있습니다. 현재 클립보드 내용은 아래와 같습니다." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "경고: 잠재적으로 안전하지 않은 붙여넣기" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "이 텍스트를 터미널에 붙여넣는 것은 위험할 수 있습니다. 일부 명령이 실행될 수 있는 것으로 보입니다." + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: 터미널 인스펙터" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "클립보드에 복사됨" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "닫기" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Ghostty를 종료하시겠습니까?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "창을 닫으시겠습니까?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "탭을 닫으시겠습니까?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "분할을 닫으시겠습니까?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "모든 터미널 세션이 종료됩니다." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "이 창의 모든 터미널 세션이 종료됩니다." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "이 탭의 모든 터미널 세션이 종료됩니다." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "이 분할에서 현재 실행 중인 프로세스가 종료됩니다." + +#: src/apprt/gtk/Window.zig:200 +msgid "Main Menu" +msgstr "메인 메뉴" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "열린 탭 보기" + +#: src/apprt/gtk/Window.zig:295 +msgid "" +"⚠️ 디버그 빌드를 실행 중입니다! 성능이 저하될 수 있습니다." +msgstr "" + +#: src/apprt/gtk/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "구성을 다시 로드했습니다" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Ghostty 개발자들" From 62bbad96b11aea5d12c16f33f8cff6a019fd79fc Mon Sep 17 00:00:00 2001 From: RubenRME Date: Mon, 31 Mar 2025 12:48:51 +0200 Subject: [PATCH 002/114] fix: fixed missing translation key at line 250 --- po/ko_KR.UTF-8.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po index 48659c388..708391eef 100644 --- a/po/ko_KR.UTF-8.po +++ b/po/ko_KR.UTF-8.po @@ -247,8 +247,8 @@ msgstr "열린 탭 보기" #: src/apprt/gtk/Window.zig:295 msgid "" -"⚠️ 디버그 빌드를 실행 중입니다! 성능이 저하될 수 있습니다." -msgstr "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ 디버그 빌드를 실행 중입니다! 성능이 저하될 수 있습니다." #: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" From a92e761c09f977f67f155a467b9d73427a00df7f Mon Sep 17 00:00:00 2001 From: RubenRME Date: Mon, 31 Mar 2025 12:51:19 +0200 Subject: [PATCH 003/114] fix: added locale to il8n.zig --- src/os/i18n.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/os/i18n.zig b/src/os/i18n.zig index baae73e46..4cf8817a5 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -29,6 +29,7 @@ pub const locales = [_][:0]const u8{ "nb_NO.UTF-8", "uk_UA.UTF-8", "pl_PL.UTF-8", + "ko_KR.UTF-8", }; /// Set for faster membership lookup of locales. From d6dea79bde1f2060fce56d178102db2c80773332 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 5 Feb 2025 13:58:01 +0100 Subject: [PATCH 004/114] gtk: add option to always display the tab bar Also fixes crashes in both vanilla GTK and Adwaita implementations of `closeTab`, which erroneously close windows twice when there are no more tabs left (we probably already handle it somewhere else). --- src/apprt/gtk/Window.zig | 56 ++++++++++++++++++++++------------------ src/config/Config.zig | 29 ++++++++++++++++++++- 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index d82087ff0..6bed56ec0 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -54,6 +54,9 @@ window: *adw.ApplicationWindow, /// The header bar for the window. headerbar: HeaderBar, +/// The tab bar for the window. +tab_bar: *adw.TabBar, + /// The tab overview for the window. This is possibly null since there is no /// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0). tab_overview: ?*adw.TabOverview, @@ -82,6 +85,7 @@ pub const DerivedConfig = struct { gtk_tabs_location: configpkg.Config.GtkTabsLocation, gtk_wide_tabs: bool, gtk_toolbar_style: configpkg.Config.GtkToolbarStyle, + window_show_tab_bar: configpkg.Config.WindowShowTabBar, quick_terminal_position: configpkg.Config.QuickTerminalPosition, quick_terminal_size: configpkg.Config.QuickTerminalSize, @@ -101,6 +105,7 @@ pub const DerivedConfig = struct { .gtk_tabs_location = config.@"gtk-tabs-location", .gtk_wide_tabs = config.@"gtk-wide-tabs", .gtk_toolbar_style = config.@"gtk-toolbar-style", + .window_show_tab_bar = config.@"window-show-tab-bar", .quick_terminal_position = config.@"quick-terminal-position", .quick_terminal_size = config.@"quick-terminal-size", @@ -135,6 +140,7 @@ pub fn init(self: *Window, app: *App) !void { .config = DerivedConfig.init(&app.config), .window = undefined, .headerbar = undefined, + .tab_bar = undefined, .tab_overview = null, .notebook = undefined, .titlebar_menu = undefined, @@ -216,8 +222,9 @@ pub fn init(self: *Window, app: *App) !void { // If we're using an AdwWindow then we can support the tab overview. if (self.tab_overview) |tab_overview| { if (!adw_version.supportsTabOverview()) unreachable; - const btn = switch (self.config.gtk_tabs_location) { - .top, .bottom => btn: { + + const btn = switch (self.config.window_show_tab_bar) { + .always, .auto => btn: { const btn = gtk.ToggleButton.new(); btn.as(gtk.Widget).setTooltipText(i18n._("View Open Tabs")); btn.as(gtk.Button).setIconName("view-grid-symbolic"); @@ -229,8 +236,7 @@ pub fn init(self: *Window, app: *App) !void { ); break :btn btn.as(gtk.Widget); }, - - .hidden => btn: { + .never => btn: { const btn = adw.TabButton.new(); btn.setView(self.notebook.tab_view); btn.as(gtk.Actionable).setActionName("overview.open"); @@ -376,21 +382,16 @@ pub fn init(self: *Window, app: *App) !void { // Our actions for the menu initActions(self); + self.tab_bar = adw.TabBar.new(); + self.tab_bar.setView(self.notebook.tab_view); + if (adw_version.supportsToolbarView()) { const toolbar_view = adw.ToolbarView.new(); toolbar_view.addTopBar(self.headerbar.asWidget()); - if (self.config.gtk_tabs_location != .hidden) { - const tab_bar = adw.TabBar.new(); - tab_bar.setView(self.notebook.tab_view); - - if (!self.config.gtk_wide_tabs) tab_bar.setExpandTabs(0); - - switch (self.config.gtk_tabs_location) { - .top => toolbar_view.addTopBar(tab_bar.as(gtk.Widget)), - .bottom => toolbar_view.addBottomBar(tab_bar.as(gtk.Widget)), - .hidden => unreachable, - } + switch (self.config.gtk_tabs_location) { + .top => toolbar_view.addTopBar(self.tab_bar.as(gtk.Widget)), + .bottom => toolbar_view.addBottomBar(self.tab_bar.as(gtk.Widget)), } toolbar_view.setContent(box.as(gtk.Widget)); @@ -405,23 +406,18 @@ pub fn init(self: *Window, app: *App) !void { // Set our application window content. self.tab_overview.?.setChild(toolbar_view.as(gtk.Widget)); self.window.setContent(self.tab_overview.?.as(gtk.Widget)); - } else tab_bar: { - if (self.config.gtk_tabs_location == .hidden) break :tab_bar; + } else { // In earlier adwaita versions, we need to add the tabbar manually since we do not use // an AdwToolbarView. - const tab_bar = adw.TabBar.new(); - tab_bar.as(gtk.Widget).addCssClass("inline"); + self.tab_bar.as(gtk.Widget).addCssClass("inline"); + switch (self.config.gtk_tabs_location) { .top => box.insertChildAfter( - tab_bar.as(gtk.Widget), + self.tab_bar.as(gtk.Widget), self.headerbar.asWidget(), ), - .bottom => box.append(tab_bar.as(gtk.Widget)), - .hidden => unreachable, + .bottom => box.append(self.tab_bar.as(gtk.Widget)), } - tab_bar.setView(self.notebook.tab_view); - - if (!self.config.gtk_wide_tabs) tab_bar.setExpandTabs(0); } // If we want the window to be maximized, we do that here. @@ -543,6 +539,16 @@ pub fn syncAppearance(self: *Window) !void { } } + self.tab_bar.setExpandTabs(@intFromBool(self.config.gtk_wide_tabs)); + self.tab_bar.setAutohide(switch (self.config.window_show_tab_bar) { + .auto, .never => @intFromBool(true), + .always => @intFromBool(false), + }); + self.tab_bar.as(gtk.Widget).setVisible(switch (self.config.window_show_tab_bar) { + .always, .auto => @intFromBool(true), + .never => @intFromBool(false), + }); + self.winproto.syncAppearance() catch |err| { log.warn("failed to sync winproto appearance error={}", .{err}); }; diff --git a/src/config/Config.zig b/src/config/Config.zig index ca330f8f6..fd6ae798e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1410,6 +1410,27 @@ keybind: Keybinds = .{}, /// * `end` - Insert the new tab at the end of the tab list. @"window-new-tab-position": WindowNewTabPosition = .current, +/// Whether to show the tab bar. +/// +/// Valid values: +/// +/// - `always` +/// +/// Always display the tab bar, even when there's only one tab. +/// +/// - `auto` *(default)* +/// +/// Automatically show and hide the tab bar. The tab bar is only +/// shown when there are two or more tabs present. +/// +/// - `never` +/// +/// Never show the tab bar. Tabs are only accessible via the tab +/// overview or by keybind actions. +/// +/// Currently only supported on Linux (GTK). +@"window-show-tab-bar": WindowShowTabBar = .auto, + /// Background color for the window titlebar. This only takes effect if /// window-theme is set to ghostty. Currently only supported in the GTK app /// runtime. @@ -5747,7 +5768,6 @@ pub const GtkSingleInstance = enum { pub const GtkTabsLocation = enum { top, bottom, - hidden, }; /// See gtk-toolbar-style @@ -5795,6 +5815,13 @@ pub const WindowNewTabPosition = enum { end, }; +/// See window-show-tab-bar +pub const WindowShowTabBar = enum { + always, + auto, + never, +}; + /// See resize-overlay pub const ResizeOverlay = enum { always, From 5f3e5afb88602c90e6ff39fd8948e84842d96af4 Mon Sep 17 00:00:00 2001 From: Alan Moyano Date: Mon, 19 May 2025 20:27:22 -0300 Subject: [PATCH 005/114] Add Argentinian Spanish translation and locale support --- po/es_AR.UTF-8.po | 285 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 2 files changed, 286 insertions(+) create mode 100644 po/es_AR.UTF-8.po diff --git a/po/es_AR.UTF-8.po b/po/es_AR.UTF-8.po new file mode 100644 index 000000000..0cb99f6be --- /dev/null +++ b/po/es_AR.UTF-8.po @@ -0,0 +1,285 @@ +# Spanish translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Alan Moyano , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-04-23 16:58+0800\n" +"PO-Revision-Date: 2025-05-19 20:17-0300\n" +"Last-Translator: Alan Moyano \n" +"Language-Team: Argentinian \n" +"Language: es_AR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Cambiar el título de la terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Deja en blanco para restaurar el título predeterminado." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Cancelar" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr Aceptar" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Errores de configuración" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Se encontraron uno o más errores de configuración. Por favor revisa los " +"errores a continuación, y recarga tu configuración o ignora estos errores." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Ignorar" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +msgid "Reload Configuration" +msgstr "Recargar configuración" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Dividir hacia arriba" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Dividir hacia abajo" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Dividir hacia la izquierda" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Dividir hacia la derecha" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "Ejecutar un comando…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Copiar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "Pegar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Limpiar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Reiniciar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Dividir" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Cambiar título…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Pestaña" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:255 +msgid "New Tab" +msgstr "Nueva pestaña" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Cerrar pestaña" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Ventana" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Nueva ventana" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Cerrar ventana" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Configuración" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Open Configuration" +msgstr "Abrir configuración" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "Paleta de comandos" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Terminal Inspector" +msgstr "Inspector de la terminal" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1024 +msgid "About Ghostty" +msgstr "Acerca de Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 +msgid "Quit" +msgstr "Salir" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Autorizar acceso al portapapeles" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicación está intentando leer desde el portapapeles. El contenido " +"actual del portapapeles se muestra a continuación." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Denegar" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Permitir" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicación está intentando escribir en el portapapeles. El contenido " +"actual del portapapeles se muestra a continuación." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Advertencia: Pegado potencialmente inseguro" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Pegar este texto en la terminal puede ser peligroso ya que parece que " +"algunos comandos podrían ejecutarse." + +#: src/apprt/gtk/Window.zig:208 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/Window.zig:229 +msgid "View Open Tabs" +msgstr "Ver pestañas abiertas" + +#: src/apprt/gtk/Window.zig:256 +msgid "New Split" +msgstr "Nueva división" + +#: src/apprt/gtk/Window.zig:319 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Está ejecutando una versión de depuración de Ghostty. El rendimiento no " +"será óptimo." + +#: src/apprt/gtk/Window.zig:765 +msgid "Reloaded the configuration" +msgstr "Configuración recargada" + +#: src/apprt/gtk/Window.zig:1005 +msgid "Ghostty Developers" +msgstr "Desarrolladores de Ghostty" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspector de la terminal" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Cerrar" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "¿Salir de Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "¿Cerrar ventana?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "¿Cerrar pestaña?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "msgstr "¿Cerrar división?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Todas las sesiones de la terminal serán terminadas." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Todas las sesiones de la terminal en esta ventana serán terminadas." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Todas las sesiones de la terminal en esta pestaña serán terminadas." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "El proceso actualmente en ejecución en esta división será terminado." + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiado al portapapeles" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 475f5e705..fd1d44ab0 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -43,6 +43,7 @@ pub const locales = [_][:0]const u8{ "tr_TR.UTF-8", "id_ID.UTF-8", "es_BO.UTF-8", + "es_AR.UTF-8", "pt_BR.UTF-8", "ca_ES.UTF-8", }; From cf7e76d8f23f7e048ff55fd4db5da7a08e5176c6 Mon Sep 17 00:00:00 2001 From: Alan Moyano Date: Fri, 23 May 2025 17:25:14 -0300 Subject: [PATCH 006/114] Adding Argentinian Spanish to CODEOWNERS --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 3d8a4da3d..a53fb6da2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -161,6 +161,7 @@ /po/ca_ES.UTF-8.po @ghostty-org/ca_ES /po/de_DE.UTF-8.po @ghostty-org/de_DE /po/es_BO.UTF-8.po @ghostty-org/es_BO +/po/es_AR.UTF-8.po @ghostty-org/es_AR /po/fr_FR.UTF-8.po @ghostty-org/fr_FR /po/id_ID.UTF-8.po @ghostty-org/id_ID /po/ja_JP.UTF-8.po @ghostty-org/ja_JP From cb991620b964e28f7551c4851f3aa16aaa5b812e Mon Sep 17 00:00:00 2001 From: RME Date: Thu, 19 Jun 2025 13:51:34 +0200 Subject: [PATCH 007/114] Apply suggestions from code review Co-authored-by: Hojin You --- po/ko_KR.UTF-8.po | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po index 708391eef..4b87a9d35 100644 --- a/po/ko_KR.UTF-8.po +++ b/po/ko_KR.UTF-8.po @@ -23,7 +23,7 @@ msgstr "터미널 제목 변경" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 msgid "Leave blank to restore the default title." -msgstr "기본 제목으로 복원하려면 비워 두세요." +msgstr "제목란을 비워 두면 기본값으로 복원됩니다." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 #: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 @@ -42,7 +42,7 @@ msgstr "설정 오류" msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." -msgstr "하나 이상의 설정 오류가 발견되었습니다. 아래 오류를 확인한 후 설정을 다시 로드하거나 무시하세요." +msgstr "설정에 하나 이상의 문제가 발견되었습니다. 아래 오류(를)들을 확인한 후 설정을 다시 불러오거나 무시하세요." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -52,7 +52,7 @@ msgstr "무시" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Reload Configuration" -msgstr "설정을 다시 로드하기" +msgstr "설정 값 다시 불러오기" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -145,7 +145,7 @@ msgstr "설정 열기" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Terminal Inspector" -msgstr "터미널 인스펙터" +msgstr "터미널 조사 도구" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 #: src/apprt/gtk/Window.zig:960 @@ -248,11 +248,11 @@ msgstr "열린 탭 보기" #: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "⚠️ 디버그 빌드를 실행 중입니다! 성능이 저하될 수 있습니다." +msgstr "⚠️ 디버그 빌드 실행 중! 성능이 저하됩니다." #: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" -msgstr "구성을 다시 로드했습니다" +msgstr "설정값을 다시 불러왔습니다" #: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" From 84b1984f08b8aa9bb5c2b0ad16c57e1ba010b5a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Sun, 22 Jun 2025 19:45:02 +0100 Subject: [PATCH 008/114] Added Irish translation --- po/ga_IE.UTF-8.po | 286 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 po/ga_IE.UTF-8.po diff --git a/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po new file mode 100644 index 000000000..c7123194f --- /dev/null +++ b/po/ga_IE.UTF-8.po @@ -0,0 +1,286 @@ +# Irish translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Aindriú Mac Giolla Eoin , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-04-23 16:58+0800\n" +"PO-Revision-Date: 2025-06-22 19:43+0100\n" +"Last-Translator: Aindriú Mac Giolla Eoin \n" +"Language-Team: Irish \n" +"Language: ga\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n" +"X-Generator: Poedit 3.4.2\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Athraigh Teideal Teirminéil" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Fág bán chun an teideal réamhshocraithe a athbhunú." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Cealaigh" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "Ceart go leor" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Earráidí Cumraíochta" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Fuarthas earráid chumraíochta amháin nó níos mó. Athbhreithnigh na hearráidí " +"thíos, agus athlódáil do chumraíocht nó déan neamhaird de na hearráidí seo." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Déan neamhaird de" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +msgid "Reload Configuration" +msgstr "Athlódáil Cumraíocht" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Scoilt Suas" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Scoilt Síos" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Scoilt ar Chlé" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Scoilt ar Dheis" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "Rith ordú…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Cóipeáil" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "Greamaigh" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Glan" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Athshocraigh" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Scoilt" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Athraigh Teideal…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Cluaisín" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:255 +msgid "New Tab" +msgstr "Cluaisín Nua" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Dún Cluaisín" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Fuinneog" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Fuinneog Nua" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Dún Fuinneog" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Cumraíocht" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Open Configuration" +msgstr "Oscail Cumraíocht" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "Pailéad Ordú" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Terminal Inspector" +msgstr "Cigire Teirminéil" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1024 +msgid "About Ghostty" +msgstr "Maidir le Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 +msgid "Quit" +msgstr "Scoir" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Údarú Rochtain ar an nGearrthaisce" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Tá feidhmchlár ag iarraidh léamh ón ngearrthaisce. Taispeántar ábhar reatha " +"an ghearrthaisce thíos." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Diúltaigh" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Ceadaigh" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Tá feidhmchlár ag iarraidh scríobh chuig an ngearrthaisce. Taispeántar ábhar " +"reatha an ghearrthaisce thíos." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Rabhadh: Greamaigh a d'fhéadfadh a bheith neamhshábháilte" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"D’fhéadfadh sé a bheith contúirteach an téacs seo a ghreamú isteach sa " +"chríochfort mar is cosúil go bhféadfaí roinnt orduithe a fhorghníomhú." + +#: src/apprt/gtk/Window.zig:208 +msgid "Main Menu" +msgstr "Príomh-Roghchlár" + +#: src/apprt/gtk/Window.zig:229 +msgid "View Open Tabs" +msgstr "Féach ar na Cluaisíní Oscailte" + +#: src/apprt/gtk/Window.zig:256 +msgid "New Split" +msgstr "Scoilt Nua" + +#: src/apprt/gtk/Window.zig:319 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Tá leagan dífhabhtaithe de Ghostty á rith agat! Laghdófar an fheidhmíocht." + +#: src/apprt/gtk/Window.zig:765 +msgid "Reloaded the configuration" +msgstr "Athluchtaigh an chumraíocht" + +#: src/apprt/gtk/Window.zig:1005 +msgid "Ghostty Developers" +msgstr "Forbróirí Ghostty" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Cigire Teirminéil" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Dún" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Scoir Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Dún Fuinneog?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Dún Cluaisín?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Dún an Scoilt?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Cuirfear deireadh le gach seisiún teirminéil." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Cuirfear deireadh le gach seisiún teirminéil san fhuinneog seo." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Cuirfear deireadh le gach seisiún críochfoirt sa chluaisín seo." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "" +"Cuirfear deireadh leis an bpróiseas atá ar siúl faoi láthair sa scoilt seo." + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Cóipeáilte chuig an ghearrthaisce" From 72fb87b20e50fc68ce00c60498fcdb2c6e6eabe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Mon, 23 Jun 2025 09:27:22 +0100 Subject: [PATCH 009/114] Updated Irish to sentence case, updated struct, added ga_IE to supported locales --- CODEOWNERS | 1 + po/ga_IE.UTF-8.po | 46 +++++++++++++++++++++++----------------------- src/os/i18n.zig | 1 + 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 3d8a4da3d..c0d802621 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -173,6 +173,7 @@ /po/tr_TR.UTF-8.po @ghostty-org/tr_TR /po/uk_UA.UTF-8.po @ghostty-org/uk_UA /po/zh_CN.UTF-8.po @ghostty-org/zh_CN +/po/ga_IE.UTF-8.po @ghostty-org/ga_IE # Packaging - Snap /snap/ @ghostty-org/snap diff --git a/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po index c7123194f..1f1740d09 100644 --- a/po/ga_IE.UTF-8.po +++ b/po/ga_IE.UTF-8.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-04-23 16:58+0800\n" -"PO-Revision-Date: 2025-06-22 19:43+0100\n" +"PO-Revision-Date: 2025-06-23 09:00+0100\n" "Last-Translator: Aindriú Mac Giolla Eoin \n" "Language-Team: Irish \n" "Language: ga\n" @@ -20,7 +20,7 @@ msgstr "" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" -msgstr "Athraigh Teideal Teirminéil" +msgstr "Athraigh teideal teirminéil" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 msgid "Leave blank to restore the default title." @@ -37,7 +37,7 @@ msgstr "Ceart go leor" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 msgid "Configuration Errors" -msgstr "Earráidí Cumraíochta" +msgstr "Earráidí cumraíochta" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 msgid "" @@ -55,31 +55,31 @@ msgstr "Déan neamhaird de" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 msgid "Reload Configuration" -msgstr "Athlódáil Cumraíocht" +msgstr "Athlódáil cumraíocht" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 msgid "Split Up" -msgstr "Scoilt Suas" +msgstr "Scoilt suas" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 msgid "Split Down" -msgstr "Scoilt Síos" +msgstr "Scoilt síos" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 msgid "Split Left" -msgstr "Scoilt ar Chlé" +msgstr "Scoilt ar chlé" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 msgid "Split Right" -msgstr "Scoilt ar Dheis" +msgstr "Scoilt ar dheis" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" @@ -114,7 +114,7 @@ msgstr "Scoilt" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 msgid "Change Title…" -msgstr "Athraigh Teideal…" +msgstr "Athraigh teideal…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" @@ -124,12 +124,12 @@ msgstr "Cluaisín" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 #: src/apprt/gtk/Window.zig:255 msgid "New Tab" -msgstr "Cluaisín Nua" +msgstr "Cluaisín nua" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 msgid "Close Tab" -msgstr "Dún Cluaisín" +msgstr "Dún cluaisín" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 msgid "Window" @@ -138,12 +138,12 @@ msgstr "Fuinneog" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 msgid "New Window" -msgstr "Fuinneog Nua" +msgstr "Fuinneog nua" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 msgid "Close Window" -msgstr "Dún Fuinneog" +msgstr "Dún fuinneog" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 msgid "Config" @@ -152,15 +152,15 @@ msgstr "Cumraíocht" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" -msgstr "Oscail Cumraíocht" +msgstr "Oscail cumraíocht" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "Pailéad Ordú" +msgstr "Pailéad ordú" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" -msgstr "Cigire Teirminéil" +msgstr "Cigire teirminéil" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 #: src/apprt/gtk/Window.zig:1024 @@ -174,7 +174,7 @@ msgstr "Scoir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" -msgstr "Údarú Rochtain ar an nGearrthaisce" +msgstr "Údarú rochtain ar an ngearrthaisce" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 msgid "" @@ -220,11 +220,11 @@ msgstr "Príomh-Roghchlár" #: src/apprt/gtk/Window.zig:229 msgid "View Open Tabs" -msgstr "Féach ar na Cluaisíní Oscailte" +msgstr "Féach ar na cluaisíní oscailte" #: src/apprt/gtk/Window.zig:256 msgid "New Split" -msgstr "Scoilt Nua" +msgstr "Scoilt nua" #: src/apprt/gtk/Window.zig:319 msgid "" @@ -242,7 +242,7 @@ msgstr "Forbróirí Ghostty" #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Cigire Teirminéil" +msgstr "Ghostty: Cigire teirminéil" #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" @@ -254,15 +254,15 @@ msgstr "Scoir Ghostty?" #: src/apprt/gtk/CloseDialog.zig:88 msgid "Close Window?" -msgstr "Dún Fuinneog?" +msgstr "Dún fuinneog?" #: src/apprt/gtk/CloseDialog.zig:89 msgid "Close Tab?" -msgstr "Dún Cluaisín?" +msgstr "Dún cluaisín?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" -msgstr "Dún an Scoilt?" +msgstr "Dún an scoilt?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." diff --git a/src/os/i18n.zig b/src/os/i18n.zig index c5fca6a78..26c032df0 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -45,6 +45,7 @@ pub const locales = [_][:0]const u8{ "es_BO.UTF-8", "pt_BR.UTF-8", "ca_ES.UTF-8", + "ga_IE.UTF-8", }; /// Set for faster membership lookup of locales. From b8bc37fb95488197ba68e6d52b08d1bfc7178a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Tue, 24 Jun 2025 09:22:33 +0100 Subject: [PATCH 010/114] =?UTF-8?q?Changed=20Pail=C3=A9ad=20ord=C3=BA=20to?= =?UTF-8?q?=20Pail=C3=A9ad=20ordaithe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- po/ga_IE.UTF-8.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po index 1f1740d09..838497954 100644 --- a/po/ga_IE.UTF-8.po +++ b/po/ga_IE.UTF-8.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-04-23 16:58+0800\n" -"PO-Revision-Date: 2025-06-23 09:00+0100\n" +"PO-Revision-Date: 2025-06-24 09:21+0100\n" "Last-Translator: Aindriú Mac Giolla Eoin \n" "Language-Team: Irish \n" "Language: ga\n" @@ -156,7 +156,7 @@ msgstr "Oscail cumraíocht" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "Pailéad ordú" +msgstr "Pailéad ordaithe" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" From c12bccc9c57691c4634e99673ed5fff6390506c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Tue, 24 Jun 2025 12:45:21 +0100 Subject: [PATCH 011/114] =?UTF-8?q?Replaced=206=20strings=20with=20t=C3=A1?= =?UTF-8?q?b=20rather=20than=20cluais=C3=ADn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- po/ga_IE.UTF-8.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po index 838497954..3f7d6b068 100644 --- a/po/ga_IE.UTF-8.po +++ b/po/ga_IE.UTF-8.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-04-23 16:58+0800\n" -"PO-Revision-Date: 2025-06-24 09:21+0100\n" +"PO-Revision-Date: 2025-06-24 12:42+0100\n" "Last-Translator: Aindriú Mac Giolla Eoin \n" "Language-Team: Irish \n" "Language: ga\n" @@ -118,18 +118,18 @@ msgstr "Athraigh teideal…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" -msgstr "Cluaisín" +msgstr "Táb" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 #: src/apprt/gtk/Window.zig:255 msgid "New Tab" -msgstr "Cluaisín nua" +msgstr "Táb nua" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 msgid "Close Tab" -msgstr "Dún cluaisín" +msgstr "Dún táb" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 msgid "Window" @@ -220,7 +220,7 @@ msgstr "Príomh-Roghchlár" #: src/apprt/gtk/Window.zig:229 msgid "View Open Tabs" -msgstr "Féach ar na cluaisíní oscailte" +msgstr "Féach ar na táib oscailte" #: src/apprt/gtk/Window.zig:256 msgid "New Split" @@ -258,7 +258,7 @@ msgstr "Dún fuinneog?" #: src/apprt/gtk/CloseDialog.zig:89 msgid "Close Tab?" -msgstr "Dún cluaisín?" +msgstr "Dún táb?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" @@ -274,7 +274,7 @@ msgstr "Cuirfear deireadh le gach seisiún teirminéil san fhuinneog seo." #: src/apprt/gtk/CloseDialog.zig:98 msgid "All terminal sessions in this tab will be terminated." -msgstr "Cuirfear deireadh le gach seisiún críochfoirt sa chluaisín seo." +msgstr "Cuirfear deireadh le gach seisiún teirminéil sa táb seo." #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." From 360124ded029c9c150a858a0808b008506230f85 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 25 Jun 2025 23:16:43 -0600 Subject: [PATCH 012/114] terminal/Screen: account for rectangle selection in clone Fixes an issue where rectangle selections would appear visually wrong if their start or end were out of the viewport area, because when cloning them the restored pins were defaulting to the start and end of the row instead of the appropriate column. --- src/terminal/Screen.zig | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 2688b03a7..deff4a394 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -402,8 +402,8 @@ pub fn clonePool( }; const start_pin = pin_remap.get(ordered.tl) orelse start: { - // No start means it is outside the cloned area. We change it - // to the top-left. + // No start means it is outside the cloned area. + // We move it back in bounds to the top row. // If we have no end pin then either // (1) our whole selection is outside the cloned area or @@ -417,14 +417,17 @@ pub fn clonePool( if (!sel.contains(self, clone_top)) break :sel null; } - break :start try pages.trackPin(.{ .node = pages.pages.first.? }); + break :start try pages.trackPin(.{ + .node = pages.pages.first.?, + .x = if (sel.rectangle) ordered.tl.x else 0, + }); }; const end_pin = pin_remap.get(ordered.br) orelse end: { - // No end means it is outside the cloned area. We change it - // to the bottom-right. + // No end means it is outside the cloned area. + // We move it back in bounds to the bottom row. break :end try pages.trackPin(pages.pin(.{ .active = .{ - .x = pages.cols - 1, + .x = if (sel.rectangle) ordered.br.x else pages.cols - 1, .y = pages.rows - 1, } }) orelse break :sel null); }; From f7ee6b3bdacc64a4973b121dc5f06fdeffb4761f Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 26 Jun 2025 11:26:03 -0600 Subject: [PATCH 013/114] terminal/Screen: clean up selection remap in clone Cleans up the logic, checks for out of bounds using rows instead of sel.contains because that excludes cases where a rectangle selection doesn't include the leftmost column. Also adds test for clipping behavior of rectangular selections. --- src/terminal/Screen.zig | 79 +++++++++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index deff4a394..8cdaf3fa2 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -403,34 +403,46 @@ pub fn clonePool( const start_pin = pin_remap.get(ordered.tl) orelse start: { // No start means it is outside the cloned area. - // We move it back in bounds to the top row. // If we have no end pin then either // (1) our whole selection is outside the cloned area or // (2) our cloned area is within the selection if (pin_remap.get(ordered.br) == null) { - // If our tl is before the cloned area and br is after - // the cloned area then the whole screen is selected. - // This detection is somewhat more expensive so we try - // to avoid it if possible so its nested in this if. + // We check if the selection bottom right pin is above + // the cloned area or if the top left pin is below the + // cloned area, in either of these cases it means that + // the selection is fully out of bounds, so we have no + // selection in the cloned area and break out now. const clone_top = self.pages.pin(top) orelse break :sel null; - if (!sel.contains(self, clone_top)) break :sel null; + const clone_top_y = self.pages.pointFromPin( + .screen, + clone_top, + ).?.screen.y; + if (self.pages.pointFromPin( + .screen, + ordered.br.*, + ).?.screen.y < clone_top_y) break :sel null; + if (self.pages.pointFromPin( + .screen, + ordered.tl.*, + ).?.screen.y > clone_top_y) break :sel null; } + // We move the top pin back in bounds to the top row. break :start try pages.trackPin(.{ .node = pages.pages.first.?, .x = if (sel.rectangle) ordered.tl.x else 0, }); }; - const end_pin = pin_remap.get(ordered.br) orelse end: { - // No end means it is outside the cloned area. - // We move it back in bounds to the bottom row. - break :end try pages.trackPin(pages.pin(.{ .active = .{ - .x = if (sel.rectangle) ordered.br.x else pages.cols - 1, - .y = pages.rows - 1, - } }) orelse break :sel null); - }; + // If we got to this point it means that the selection is not + // fully out of bounds, so we move the bottom right pin back + // in bounds if it isn't already. + const end_pin = pin_remap.get(ordered.br) orelse try pages.trackPin(.{ + .node = pages.pages.last.?, + .x = if (sel.rectangle) ordered.br.x else pages.cols - 1, + .y = pages.pages.last.?.data.size.rows - 1, + }); break :sel .{ .bounds = .{ .tracked = .{ @@ -5290,6 +5302,45 @@ test "Screen: clone contains subset of selection" { } } +test "Screen: clone contains subset of rectangle selection" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 5, 4, 1); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n4ABCD"); + + // Select the full screen from x=1 to x=3 + try s.select(Selection.init( + s.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, + s.pages.pin(.{ .active = .{ .x = 3, .y = 3 } }).?, + true, + )); + + // Clone + var s2 = try s.clone( + alloc, + .{ .active = .{ .y = 1 } }, + .{ .active = .{ .y = 2 } }, + ); + defer s2.deinit(); + + // Our selection should remain valid and be properly clipped + // preserving the columns of the start and end points of the + // selection. + { + const sel = s2.selection.?; + try testing.expectEqual(point.Point{ .active = .{ + .x = 1, + .y = 0, + } }, s2.pages.pointFromPin(.active, sel.start()).?); + try testing.expectEqual(point.Point{ .active = .{ + .x = 3, + .y = 3, + } }, s2.pages.pointFromPin(.active, sel.end()).?); + } +} + test "Screen: clone basic" { const testing = std.testing; const alloc = testing.allocator; From 81403f59ce98009d16dc6719635b543a88cc2ad5 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 24 Jun 2025 22:56:17 -0500 Subject: [PATCH 014/114] dbus and systemd activation - take 2 This replaces #7433. The improvements are: 1) Install the systemd user service in the proper directory depending on if it's a 'user' install or a 'system' install. This is controlled either by using the `--system` build flag (as most packages will) or by the `-Dsystem-package` flag. 2) Add the absolute path to the `ghostty` binary in the application file, the DBus service, and the systemd user service. This is done so that they do not depend on `ghostty` being in the `PATH` of whatever is launching Ghostty. That `PATH` is not necessarily the same as the `PATH` in a user shell (especially for DBus activation and systemd user services). 3) Adjust the DBus bus name that is expected by the system depending on the optimization level that Ghostty is compiled with. --- PACKAGING.md | 3 +- dist/linux/app.desktop | 11 +++-- dist/linux/dbus.service | 4 ++ dist/linux/systemd.service | 7 +++ nix/package.nix | 1 + src/apprt/gtk/App.zig | 23 +++++++-- src/apprt/gtk/Surface.zig | 9 ++++ src/build/Config.zig | 9 +++- src/build/GhosttyResources.zig | 86 ++++++++++++++++++++++++++++++++-- src/config/Config.zig | 7 ++- 10 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 dist/linux/dbus.service create mode 100644 dist/linux/systemd.service diff --git a/PACKAGING.md b/PACKAGING.md index d85f55de7..e85a9e987 100644 --- a/PACKAGING.md +++ b/PACKAGING.md @@ -83,7 +83,8 @@ zig build \ --prefix /usr \ --system /tmp/offline-cache/p \ -Doptimize=ReleaseFast \ - -Dcpu=baseline + -Dcpu=baseline \ + -Dsystem-package=true ``` The build options are covered in the next section, but this will build diff --git a/dist/linux/app.desktop b/dist/linux/app.desktop index 6e464ea87..7dc0062f7 100644 --- a/dist/linux/app.desktop +++ b/dist/linux/app.desktop @@ -1,13 +1,15 @@ [Desktop Entry] -Name=Ghostty +Version=1.0 +Name=Ghostty@@NAME@@ Type=Application Comment=A terminal emulator -Exec=ghostty +TryExec=@@GHOSTTY@@ +Exec=@@GHOSTTY@@ --launched-from=desktop Icon=com.mitchellh.ghostty Categories=System;TerminalEmulator; Keywords=terminal;tty;pty; StartupNotify=true -StartupWMClass=com.mitchellh.ghostty +StartupWMClass=com.mitchellh.ghostty@@DEBUG@@ Terminal=false Actions=new-window; X-GNOME-UsesNotifications=true @@ -16,7 +18,8 @@ X-TerminalArgTitle=--title= X-TerminalArgAppId=--class= X-TerminalArgDir=--working-directory= X-TerminalArgHold=--wait-after-command +DBusActivatable=true [Desktop Action new-window] Name=New Window -Exec=ghostty +Exec=ghostty --launched-from=desktop diff --git a/dist/linux/dbus.service b/dist/linux/dbus.service new file mode 100644 index 000000000..f9c5ce7a4 --- /dev/null +++ b/dist/linux/dbus.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=com.mitchellh.ghostty@@DEBUG@@ +SystemdService=com.mitchellh.ghostty@@DEBUG@@.service +Exec=@@GHOSTTY@@ --launched-from=dbus diff --git a/dist/linux/systemd.service b/dist/linux/systemd.service new file mode 100644 index 000000000..2d1acdaea --- /dev/null +++ b/dist/linux/systemd.service @@ -0,0 +1,7 @@ +[Unit] +Description=Ghostty@@NAME@@ + +[Service] +Type=dbus +BusName=com.mitchellh.ghostty@@DEBUG@@ +ExecStart=@@GHOSTTY@@ --launched-from=systemd diff --git a/nix/package.nix b/nix/package.nix index 08dfd710b..f24b401cc 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -99,6 +99,7 @@ in "-Dgtk-x11=${lib.boolToString enableX11}" "-Dgtk-wayland=${lib.boolToString enableWayland}" "-Dstrip=${lib.boolToString strip}" + "-Dsystem-package=true" ]; outputs = [ diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 7c9c15191..93e069376 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -405,11 +405,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // This just calls the `activate` signal but its part of the normal startup // routine so we just call it, but only if the config allows it (this allows // for launching Ghostty in the "background" without immediately opening - // a window) + // a window). An initial window will not be immediately created if we were + // launched by D-Bus activation or systemd. D-Bus activation will send it's + // own `activate` or `new-window` signal later. // // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302 - if (config.@"initial-window") - gio_app.activate(); + if (config.@"initial-window") switch (config.@"launched-from".?) { + .desktop, .cli => gio_app.activate(), + .dbus, .systemd => {}, + }; // Internally, GTK ensures that only one instance of this provider exists in the provider list // for the display. @@ -1683,6 +1687,17 @@ fn gtkActionShowGTKInspector( }; } +fn gtkActionNewWindow( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *App, +) callconv(.c) void { + log.info("received new window action", .{}); + _ = self.core_app.mailbox.push(.{ + .new_window = .{}, + }, .{ .forever = {} }); +} + /// This is called to setup the action map that this application supports. /// This should be called only once on startup. fn initActions(self: *App) void { @@ -1702,7 +1717,9 @@ fn initActions(self: *App) void { .{ "reload-config", gtkActionReloadConfig, null }, .{ "present-surface", gtkActionPresentSurface, t }, .{ "show-gtk-inspector", gtkActionShowGTKInspector, null }, + .{ "new-window", gtkActionNewWindow, null }, }; + inline for (actions) |entry| { const action = gio.SimpleAction.new(entry[0], entry[2]); defer action.unref(); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index cf8d651dd..5c886e663 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2325,6 +2325,15 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap { env.remove("GDK_DISABLE"); env.remove("GSK_RENDERER"); + // Remove some environment variables that are set when Ghostty is launched + // from a `.desktop` file, by D-Bus activation, or systemd. + env.remove("GIO_LAUNCHED_DESKTOP_FILE"); + env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID"); + env.remove("DBUS_STARTER_ADDRESS"); + env.remove("DBUS_STARTER_BUS_TYPE"); + env.remove("INVOCATION_ID"); + env.remove("JOURNAL_STREAM"); + // Unset environment varies set by snaps if we're running in a snap. // This allows Ghostty to further launch additional snaps. if (env.get("SNAP")) |_| { diff --git a/src/build/Config.zig b/src/build/Config.zig index 8974e1f0c..ede42c738 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -45,6 +45,7 @@ version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 }, pie: bool = false, strip: bool = false, patch_rpath: ?[]const u8 = null, +system_package: bool = false, /// Artifacts flatpak: bool = false, @@ -87,7 +88,11 @@ pub fn init(b: *std.Build) !Config { // This is set to true when we're building a system package. For now // this is trivially detected using the "system_package_mode" bool // but we may want to make this more sophisticated in the future. - const system_package: bool = b.graph.system_package_mode; + const system_package = b.option( + bool, + "system-package", + "Controls whether we build a system package", + ) orelse b.graph.system_package_mode; // This specifies our target wasm runtime. For now only one semi-usable // one exists so this is hardcoded. @@ -261,6 +266,8 @@ pub fn init(b: *std.Build) !Config { .ReleaseFast, .ReleaseSmall => true, }; + config.system_package = system_package; + //--------------------------------------------------------------- // Artifacts to Emit diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 640491fd6..f3b169de1 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -224,10 +224,57 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html // Desktop file so that we have an icon and other metadata - try steps.append(&b.addInstallFile( - b.path("dist/linux/app.desktop"), - "share/applications/com.mitchellh.ghostty.desktop", - ).step); + try steps.append( + formatService( + b, + cfg, + b.path("dist/linux/app.desktop"), + b.fmt( + "share/applications/com.mitchellh.ghostty{s}.desktop", + .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => "-debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }, + ), + ), + ); + // Service for DBus activation. + try steps.append( + formatService( + b, + cfg, + b.path("dist/linux/dbus.service"), + b.fmt( + "share/dbus-1/services/com.mitchellh.ghostty{s}.service", + .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => "-debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }, + ), + ), + ); + // systemd user service + try steps.append( + formatService( + b, + cfg, + b.path("dist/linux/systemd.service"), + b.fmt( + "{s}/systemd/user/com.mitchellh.ghostty{s}.service", + .{ + if (cfg.system_package) "lib" else "share", + switch (cfg.optimize) { + .Debug, .ReleaseSafe => "-debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }, + ), + ), + ); // AppStream metainfo so that application has rich metadata within app stores try steps.append(&b.addInstallFile( @@ -303,3 +350,34 @@ pub fn install(self: *const GhosttyResources) void { const b = self.steps[0].owner; for (self.steps) |step| b.getInstallStep().dependOn(step); } + +pub fn formatService(b: *std.Build, cfg: *const Config, src: std.Build.LazyPath, dest: []const u8) *std.Build.Step { + var cmd = b.addSystemCommand(&.{"sed"}); + cmd.setStdIn(.{ .lazy_path = src }); + const output = cmd.captureStdOut(); + + cmd.addArg(b.fmt( + "-e s!@@NAME@@!{s}!g", + .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => " Debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }, + )); + cmd.addArg(b.fmt( + "-e s!@@DEBUG@@!{s}!g", + .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => "-debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }, + )); + cmd.addArg(b.fmt( + "-e s!@@GHOSTTY@@!{s}/bin/ghostty!g", + .{b.install_prefix}, + )); + + return &b.addInstallFile(output, dest).step; +} diff --git a/src/config/Config.zig b/src/config/Config.zig index be59ae94f..64b00eb1c 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1029,12 +1029,17 @@ title: ?[:0]const u8 = null, /// The setting that will change the application class value. /// /// This controls the class field of the `WM_CLASS` X11 property (when running -/// under X11), and the Wayland application ID (when running under Wayland). +/// under X11), the Wayland application ID (when running under Wayland), and the +/// bus name that Ghostty uses to connect to DBus. /// /// Note that changing this value between invocations will create new, separate /// instances, of Ghostty when running with `gtk-single-instance=true`. See that /// option for more details. /// +/// Changing this value may break launching Ghostty from `.desktop` files, via +/// DBus activation, or systemd user services as the system is expecting Ghostty +/// to connect to DBus using the default `class` when it is launched. +/// /// The class name must follow the requirements defined [in the GTK /// documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html). /// From ddada2fb3fb7740a8b560cd96403cf06946c5d89 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 24 Jun 2025 23:28:04 -0500 Subject: [PATCH 015/114] flatpak: remove references to systemd unit Replaces #7676 When building as a flatpak, don't install the systemd user services since flatpaks can't use them. Remove references to the systemd service from the DBus service. Also, customize the app metadata depending on the debug mode. Co-authored-by: Leorize --- dist/linux/com.mitchellh.ghostty.metainfo.xml | 4 +-- flatpak/com.mitchellh.ghostty-debug.yml | 4 --- src/build/GhosttyResources.zig | 35 +++++++++++++------ 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/dist/linux/com.mitchellh.ghostty.metainfo.xml b/dist/linux/com.mitchellh.ghostty.metainfo.xml index 0424d3a09..46b370bb8 100644 --- a/dist/linux/com.mitchellh.ghostty.metainfo.xml +++ b/dist/linux/com.mitchellh.ghostty.metainfo.xml @@ -1,7 +1,7 @@ - com.mitchellh.ghostty - com.mitchellh.ghostty.desktop + com.mitchellh.ghostty@@DEBUG@@ + com.mitchellh.ghostty@@DEBUG@@.desktop Ghostty https://ghostty.org https://ghostty.org/docs diff --git a/flatpak/com.mitchellh.ghostty-debug.yml b/flatpak/com.mitchellh.ghostty-debug.yml index 8a2c0056e..fe4722ef5 100644 --- a/flatpak/com.mitchellh.ghostty-debug.yml +++ b/flatpak/com.mitchellh.ghostty-debug.yml @@ -6,11 +6,7 @@ sdk-extensions: - org.freedesktop.Sdk.Extension.ziglang default-branch: tip command: ghostty -# Integrate the rename into zig build, maybe? -rename-desktop-file: com.mitchellh.ghostty.desktop -rename-appdata-file: com.mitchellh.ghostty.metainfo.xml rename-icon: com.mitchellh.ghostty -desktop-file-name-suffix: " (Debug)" finish-args: # 3D rendering - --device=dri diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index f3b169de1..f72006b4b 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -258,15 +258,34 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { ), ); // systemd user service + if (!cfg.flatpak) + try steps.append( + formatService( + b, + cfg, + b.path("dist/linux/systemd.service"), + b.fmt( + "{s}/systemd/user/com.mitchellh.ghostty{s}.service", + .{ + if (cfg.system_package) "lib" else "share", + switch (cfg.optimize) { + .Debug, .ReleaseSafe => "-debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }, + ), + ), + ); + + // AppStream metainfo so that application has rich metadata within app stores try steps.append( formatService( b, cfg, - b.path("dist/linux/systemd.service"), + b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml"), b.fmt( - "{s}/systemd/user/com.mitchellh.ghostty{s}.service", + "share/metainfo/com.mitchellh.ghostty{s}.metainfo.xml", .{ - if (cfg.system_package) "lib" else "share", switch (cfg.optimize) { .Debug, .ReleaseSafe => "-debug", .ReleaseFast, .ReleaseSmall => "", @@ -276,12 +295,6 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { ), ); - // AppStream metainfo so that application has rich metadata within app stores - try steps.append(&b.addInstallFile( - b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml"), - "share/metainfo/com.mitchellh.ghostty.metainfo.xml", - ).step); - // Right click menu action for Plasma desktop try steps.append(&b.addInstallFile( b.path("dist/linux/ghostty_dolphin.desktop"), @@ -360,7 +373,7 @@ pub fn formatService(b: *std.Build, cfg: *const Config, src: std.Build.LazyPath, "-e s!@@NAME@@!{s}!g", .{ switch (cfg.optimize) { - .Debug, .ReleaseSafe => " Debug", + .Debug, .ReleaseSafe => " (Debug)", .ReleaseFast, .ReleaseSmall => "", }, }, @@ -378,6 +391,8 @@ pub fn formatService(b: *std.Build, cfg: *const Config, src: std.Build.LazyPath, "-e s!@@GHOSTTY@@!{s}/bin/ghostty!g", .{b.install_prefix}, )); + if (cfg.flatpak) + cmd.addArg("-e /^SystemdService=/d"); return &b.addInstallFile(output, dest).step; } From b68f9f2321048702bf11f3c7eba0fce0c4deba9c Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 24 Jun 2025 23:42:45 -0500 Subject: [PATCH 016/114] make sure that the desktop file uses the absolute path everywhere --- dist/linux/app.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/linux/app.desktop b/dist/linux/app.desktop index 7dc0062f7..17d7b65e3 100644 --- a/dist/linux/app.desktop +++ b/dist/linux/app.desktop @@ -22,4 +22,4 @@ DBusActivatable=true [Desktop Action new-window] Name=New Window -Exec=ghostty --launched-from=desktop +Exec=@@GHOSTTY@@ --launched-from=desktop From cf561fcc5569c6a4282c29df1de47b6f84485d89 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 25 Jun 2025 10:11:45 -0500 Subject: [PATCH 017/114] rename templated files with .in suffix --- dist/linux/{app.desktop => app.desktop.in} | 0 ...metainfo.xml => com.mitchellh.ghostty.metainfo.xml.in} | 0 dist/linux/{dbus.service => dbus.service.in} | 0 dist/linux/{systemd.service => systemd.service.in} | 0 src/build/GhosttyResources.zig | 8 ++++---- 5 files changed, 4 insertions(+), 4 deletions(-) rename dist/linux/{app.desktop => app.desktop.in} (100%) rename dist/linux/{com.mitchellh.ghostty.metainfo.xml => com.mitchellh.ghostty.metainfo.xml.in} (100%) rename dist/linux/{dbus.service => dbus.service.in} (100%) rename dist/linux/{systemd.service => systemd.service.in} (100%) diff --git a/dist/linux/app.desktop b/dist/linux/app.desktop.in similarity index 100% rename from dist/linux/app.desktop rename to dist/linux/app.desktop.in diff --git a/dist/linux/com.mitchellh.ghostty.metainfo.xml b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in similarity index 100% rename from dist/linux/com.mitchellh.ghostty.metainfo.xml rename to dist/linux/com.mitchellh.ghostty.metainfo.xml.in diff --git a/dist/linux/dbus.service b/dist/linux/dbus.service.in similarity index 100% rename from dist/linux/dbus.service rename to dist/linux/dbus.service.in diff --git a/dist/linux/systemd.service b/dist/linux/systemd.service.in similarity index 100% rename from dist/linux/systemd.service rename to dist/linux/systemd.service.in diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index f72006b4b..364818a1a 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -228,7 +228,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { formatService( b, cfg, - b.path("dist/linux/app.desktop"), + b.path("dist/linux/app.desktop.in"), b.fmt( "share/applications/com.mitchellh.ghostty{s}.desktop", .{ @@ -245,7 +245,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { formatService( b, cfg, - b.path("dist/linux/dbus.service"), + b.path("dist/linux/dbus.service.in"), b.fmt( "share/dbus-1/services/com.mitchellh.ghostty{s}.service", .{ @@ -263,7 +263,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { formatService( b, cfg, - b.path("dist/linux/systemd.service"), + b.path("dist/linux/systemd.service.in"), b.fmt( "{s}/systemd/user/com.mitchellh.ghostty{s}.service", .{ @@ -282,7 +282,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { formatService( b, cfg, - b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml"), + b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"), b.fmt( "share/metainfo/com.mitchellh.ghostty{s}.metainfo.xml", .{ From 9c95ce28ae7f9a4026ce8d8ebe463b018c27f27a Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 25 Jun 2025 10:24:47 -0500 Subject: [PATCH 018/114] drop system-package option --- PACKAGING.md | 3 +-- nix/package.nix | 1 - src/build/Config.zig | 9 +-------- src/build/GhosttyResources.zig | 2 +- 4 files changed, 3 insertions(+), 12 deletions(-) diff --git a/PACKAGING.md b/PACKAGING.md index e85a9e987..d85f55de7 100644 --- a/PACKAGING.md +++ b/PACKAGING.md @@ -83,8 +83,7 @@ zig build \ --prefix /usr \ --system /tmp/offline-cache/p \ -Doptimize=ReleaseFast \ - -Dcpu=baseline \ - -Dsystem-package=true + -Dcpu=baseline ``` The build options are covered in the next section, but this will build diff --git a/nix/package.nix b/nix/package.nix index f24b401cc..08dfd710b 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -99,7 +99,6 @@ in "-Dgtk-x11=${lib.boolToString enableX11}" "-Dgtk-wayland=${lib.boolToString enableWayland}" "-Dstrip=${lib.boolToString strip}" - "-Dsystem-package=true" ]; outputs = [ diff --git a/src/build/Config.zig b/src/build/Config.zig index ede42c738..e55da3860 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -45,7 +45,6 @@ version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 }, pie: bool = false, strip: bool = false, patch_rpath: ?[]const u8 = null, -system_package: bool = false, /// Artifacts flatpak: bool = false, @@ -88,11 +87,7 @@ pub fn init(b: *std.Build) !Config { // This is set to true when we're building a system package. For now // this is trivially detected using the "system_package_mode" bool // but we may want to make this more sophisticated in the future. - const system_package = b.option( - bool, - "system-package", - "Controls whether we build a system package", - ) orelse b.graph.system_package_mode; + const system_package = b.graph.system_package_mode; // This specifies our target wasm runtime. For now only one semi-usable // one exists so this is hardcoded. @@ -266,8 +261,6 @@ pub fn init(b: *std.Build) !Config { .ReleaseFast, .ReleaseSmall => true, }; - config.system_package = system_package; - //--------------------------------------------------------------- // Artifacts to Emit diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 364818a1a..f52c2e810 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -267,7 +267,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { b.fmt( "{s}/systemd/user/com.mitchellh.ghostty{s}.service", .{ - if (cfg.system_package) "lib" else "share", + if (b.graph.system_package_mode) "lib" else "share", switch (cfg.optimize) { .Debug, .ReleaseSafe => "-debug", .ReleaseFast, .ReleaseSmall => "", From eb5a488b57d4e0528d4fc3724ff1f77a6894ab61 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 25 Jun 2025 11:09:44 -0500 Subject: [PATCH 019/114] clean up duplicated code in installation of desktop services --- dist/linux/app.desktop.in | 4 +- .../com.mitchellh.ghostty.metainfo.xml.in | 4 +- dist/linux/dbus.service.in | 4 +- dist/linux/systemd.service.in | 4 +- src/build/GhosttyResources.zig | 81 ++++++++++--------- 5 files changed, 49 insertions(+), 48 deletions(-) diff --git a/dist/linux/app.desktop.in b/dist/linux/app.desktop.in index 17d7b65e3..aefb5c9cd 100644 --- a/dist/linux/app.desktop.in +++ b/dist/linux/app.desktop.in @@ -1,6 +1,6 @@ [Desktop Entry] Version=1.0 -Name=Ghostty@@NAME@@ +Name=@@NAME@@ Type=Application Comment=A terminal emulator TryExec=@@GHOSTTY@@ @@ -9,7 +9,7 @@ Icon=com.mitchellh.ghostty Categories=System;TerminalEmulator; Keywords=terminal;tty;pty; StartupNotify=true -StartupWMClass=com.mitchellh.ghostty@@DEBUG@@ +StartupWMClass=@@APPID@@ Terminal=false Actions=new-window; X-GNOME-UsesNotifications=true diff --git a/dist/linux/com.mitchellh.ghostty.metainfo.xml.in b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in index 46b370bb8..97598c215 100644 --- a/dist/linux/com.mitchellh.ghostty.metainfo.xml.in +++ b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in @@ -1,7 +1,7 @@ - com.mitchellh.ghostty@@DEBUG@@ - com.mitchellh.ghostty@@DEBUG@@.desktop + @@APPID@@ + @@APPID@@.desktop Ghostty https://ghostty.org https://ghostty.org/docs diff --git a/dist/linux/dbus.service.in b/dist/linux/dbus.service.in index f9c5ce7a4..a11ab2c13 100644 --- a/dist/linux/dbus.service.in +++ b/dist/linux/dbus.service.in @@ -1,4 +1,4 @@ [D-BUS Service] -Name=com.mitchellh.ghostty@@DEBUG@@ -SystemdService=com.mitchellh.ghostty@@DEBUG@@.service +Name=@@APPID@@ +SystemdService=@@APPID@@.service Exec=@@GHOSTTY@@ --launched-from=dbus diff --git a/dist/linux/systemd.service.in b/dist/linux/systemd.service.in index 2d1acdaea..ede2174a2 100644 --- a/dist/linux/systemd.service.in +++ b/dist/linux/systemd.service.in @@ -1,7 +1,7 @@ [Unit] -Description=Ghostty@@NAME@@ +Description=@@NAME@@ [Service] Type=dbus -BusName=com.mitchellh.ghostty@@DEBUG@@ +BusName=@@APPID@@ ExecStart=@@GHOSTTY@@ --launched-from=systemd diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index f52c2e810..a62ba0528 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -223,20 +223,31 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { if (cfg.target.result.os.tag == .linux) { // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html + const name = b.fmt("Ghostty{s}", .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => " (Debug)", + .ReleaseFast, .ReleaseSmall => "", + }, + }); + + const app_id = b.fmt("com.mitchellh.ghostty{s}", .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => "-debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }); + // Desktop file so that we have an icon and other metadata try steps.append( formatService( b, cfg, + name, + app_id, b.path("dist/linux/app.desktop.in"), b.fmt( - "share/applications/com.mitchellh.ghostty{s}.desktop", - .{ - switch (cfg.optimize) { - .Debug, .ReleaseSafe => "-debug", - .ReleaseFast, .ReleaseSmall => "", - }, - }, + "share/applications/{s}.desktop", + .{app_id}, ), ), ); @@ -245,15 +256,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { formatService( b, cfg, + name, + app_id, b.path("dist/linux/dbus.service.in"), b.fmt( - "share/dbus-1/services/com.mitchellh.ghostty{s}.service", - .{ - switch (cfg.optimize) { - .Debug, .ReleaseSafe => "-debug", - .ReleaseFast, .ReleaseSmall => "", - }, - }, + "share/dbus-1/services/{s}.service", + .{app_id}, ), ), ); @@ -263,15 +271,14 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { formatService( b, cfg, + name, + app_id, b.path("dist/linux/systemd.service.in"), b.fmt( - "{s}/systemd/user/com.mitchellh.ghostty{s}.service", + "{s}/systemd/user/{s}.service", .{ if (b.graph.system_package_mode) "lib" else "share", - switch (cfg.optimize) { - .Debug, .ReleaseSafe => "-debug", - .ReleaseFast, .ReleaseSmall => "", - }, + app_id, }, ), ), @@ -282,15 +289,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { formatService( b, cfg, + name, + app_id, b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"), b.fmt( - "share/metainfo/com.mitchellh.ghostty{s}.metainfo.xml", - .{ - switch (cfg.optimize) { - .Debug, .ReleaseSafe => "-debug", - .ReleaseFast, .ReleaseSmall => "", - }, - }, + "share/metainfo/{s}.metainfo.xml", + .{app_id}, ), ), ); @@ -364,28 +368,25 @@ pub fn install(self: *const GhosttyResources) void { for (self.steps) |step| b.getInstallStep().dependOn(step); } -pub fn formatService(b: *std.Build, cfg: *const Config, src: std.Build.LazyPath, dest: []const u8) *std.Build.Step { +pub fn formatService( + b: *std.Build, + cfg: *const Config, + name: []const u8, + app_id: []const u8, + src: std.Build.LazyPath, + dest: []const u8, +) *std.Build.Step { var cmd = b.addSystemCommand(&.{"sed"}); cmd.setStdIn(.{ .lazy_path = src }); const output = cmd.captureStdOut(); cmd.addArg(b.fmt( "-e s!@@NAME@@!{s}!g", - .{ - switch (cfg.optimize) { - .Debug, .ReleaseSafe => " (Debug)", - .ReleaseFast, .ReleaseSmall => "", - }, - }, + .{name}, )); cmd.addArg(b.fmt( - "-e s!@@DEBUG@@!{s}!g", - .{ - switch (cfg.optimize) { - .Debug, .ReleaseSafe => "-debug", - .ReleaseFast, .ReleaseSmall => "", - }, - }, + "-e s!@@APPID@@!{s}!g", + .{app_id}, )); cmd.addArg(b.fmt( "-e s!@@GHOSTTY@@!{s}/bin/ghostty!g", From fa4f4207687f56ea0651951107e29f2d69a3c2b0 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 25 Jun 2025 11:55:43 -0500 Subject: [PATCH 020/114] replace sed with a simple Zig program for templating desktop files --- src/build/GhosttyResources.zig | 36 ++++++++++++++++++---------------- src/build/desktop_template.zig | 29 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 17 deletions(-) create mode 100644 src/build/desktop_template.zig diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index a62ba0528..23aceda98 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -376,24 +376,26 @@ pub fn formatService( src: std.Build.LazyPath, dest: []const u8, ) *std.Build.Step { - var cmd = b.addSystemCommand(&.{"sed"}); - cmd.setStdIn(.{ .lazy_path = src }); - const output = cmd.captureStdOut(); + var cmd = b.addExecutable( + .{ + .name = "desktop-template", + .root_source_file = b.path("src/build/desktop_template.zig"), + .target = b.graph.host, + }, + ); - cmd.addArg(b.fmt( - "-e s!@@NAME@@!{s}!g", - .{name}, - )); - cmd.addArg(b.fmt( - "-e s!@@APPID@@!{s}!g", - .{app_id}, - )); - cmd.addArg(b.fmt( - "-e s!@@GHOSTTY@@!{s}/bin/ghostty!g", - .{b.install_prefix}, - )); - if (cfg.flatpak) - cmd.addArg("-e /^SystemdService=/d"); + const options = b.addOptions(); + + options.addOption([]const u8, "app_id", app_id); + options.addOption([]const u8, "name", name); + options.addOption([]const u8, "ghostty", b.fmt("{s}/bin/ghostty", .{b.install_prefix})); + options.addOption(bool, "flatpak", cfg.flatpak); + + cmd.root_module.addOptions("cfg", options); + + const run = b.addRunArtifact(cmd); + run.setStdIn(.{ .lazy_path = src }); + const output = run.captureStdOut(); return &b.addInstallFile(output, dest).step; } diff --git a/src/build/desktop_template.zig b/src/build/desktop_template.zig new file mode 100644 index 000000000..0741ff81a --- /dev/null +++ b/src/build/desktop_template.zig @@ -0,0 +1,29 @@ +const std = @import("std"); +const cfg = @import("cfg"); + +pub fn main() !void { + var debug: std.heap.DebugAllocator(.{}) = .init; + defer _ = debug.deinit(); + + const alloc = debug.allocator(); + const stdin = std.io.getStdIn(); + + const stdout = std.io.getStdOut(); + var input = stdin.reader(); + while (try input.readUntilDelimiterOrEofAlloc(alloc, '\n', 4096)) |line| { + defer alloc.free(line); + + const buf1 = try std.mem.replaceOwned(u8, alloc, line, "@@APPID@@", cfg.app_id); + defer alloc.free(buf1); + + const buf2 = try std.mem.replaceOwned(u8, alloc, buf1, "@@NAME@@", cfg.name); + defer alloc.free(buf2); + + const buf3 = try std.mem.replaceOwned(u8, alloc, buf2, "@@GHOSTTY@@", cfg.ghostty); + defer alloc.free(buf3); + + if (cfg.flatpak and std.mem.startsWith(u8, buf3, "SystemdService=")) continue; + try stdout.writeAll(buf3); + try stdout.writeAll("\n"); + } +} From 8a95212197ff0362b21027cc3f1348468322823e Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 25 Jun 2025 15:09:53 -0500 Subject: [PATCH 021/114] fix up the name in the metainfo when templating --- dist/linux/com.mitchellh.ghostty.metainfo.xml.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/linux/com.mitchellh.ghostty.metainfo.xml.in b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in index 97598c215..7bfa30518 100644 --- a/dist/linux/com.mitchellh.ghostty.metainfo.xml.in +++ b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in @@ -2,7 +2,7 @@ @@APPID@@ @@APPID@@.desktop - Ghostty + @@NAME@@ https://ghostty.org https://ghostty.org/docs https://github.com/ghostty-org/ghostty/discussions From 73d5eb928c80a411e5879bd609b82317258a0af4 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 25 Jun 2025 15:11:58 -0500 Subject: [PATCH 022/114] fix up formatting of desktop_template.zig --- src/build/desktop_template.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/build/desktop_template.zig b/src/build/desktop_template.zig index 0741ff81a..7b066bfcc 100644 --- a/src/build/desktop_template.zig +++ b/src/build/desktop_template.zig @@ -6,10 +6,11 @@ pub fn main() !void { defer _ = debug.deinit(); const alloc = debug.allocator(); - const stdin = std.io.getStdIn(); + const stdin = std.io.getStdIn(); const stdout = std.io.getStdOut(); var input = stdin.reader(); + while (try input.readUntilDelimiterOrEofAlloc(alloc, '\n', 4096)) |line| { defer alloc.free(line); @@ -23,6 +24,7 @@ pub fn main() !void { defer alloc.free(buf3); if (cfg.flatpak and std.mem.startsWith(u8, buf3, "SystemdService=")) continue; + try stdout.writeAll(buf3); try stdout.writeAll("\n"); } From 739b691a6d1a9ea86a021af1ce70ad5cd1e41d14 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Jun 2025 10:28:17 -0700 Subject: [PATCH 023/114] use cmake formatting to template, avoids the custom binary --- dist/linux/app.desktop.in | 10 +- .../com.mitchellh.ghostty.metainfo.xml.in | 6 +- dist/linux/dbus.service.flatpak.in | 3 + dist/linux/dbus.service.in | 6 +- dist/linux/systemd.service.in | 6 +- src/build/GhosttyResources.zig | 307 +++++++++--------- src/build/desktop_template.zig | 31 -- 7 files changed, 168 insertions(+), 201 deletions(-) create mode 100644 dist/linux/dbus.service.flatpak.in delete mode 100644 src/build/desktop_template.zig diff --git a/dist/linux/app.desktop.in b/dist/linux/app.desktop.in index aefb5c9cd..c39164158 100644 --- a/dist/linux/app.desktop.in +++ b/dist/linux/app.desktop.in @@ -1,15 +1,15 @@ [Desktop Entry] Version=1.0 -Name=@@NAME@@ +Name=@NAME@ Type=Application Comment=A terminal emulator -TryExec=@@GHOSTTY@@ -Exec=@@GHOSTTY@@ --launched-from=desktop +TryExec=@GHOSTTY@ +Exec=@GHOSTTY@ --launched-from=desktop Icon=com.mitchellh.ghostty Categories=System;TerminalEmulator; Keywords=terminal;tty;pty; StartupNotify=true -StartupWMClass=@@APPID@@ +StartupWMClass=@APPID@ Terminal=false Actions=new-window; X-GNOME-UsesNotifications=true @@ -22,4 +22,4 @@ DBusActivatable=true [Desktop Action new-window] Name=New Window -Exec=@@GHOSTTY@@ --launched-from=desktop +Exec=@GHOSTTY@ --launched-from=desktop diff --git a/dist/linux/com.mitchellh.ghostty.metainfo.xml.in b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in index 7bfa30518..00d99adb2 100644 --- a/dist/linux/com.mitchellh.ghostty.metainfo.xml.in +++ b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in @@ -1,8 +1,8 @@ - @@APPID@@ - @@APPID@@.desktop - @@NAME@@ + @APPID@id> + @APPID@desktop + @NAME@name> https://ghostty.org https://ghostty.org/docs https://github.com/ghostty-org/ghostty/discussions diff --git a/dist/linux/dbus.service.flatpak.in b/dist/linux/dbus.service.flatpak.in new file mode 100644 index 000000000..213cda78f --- /dev/null +++ b/dist/linux/dbus.service.flatpak.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=@APPID@ +Exec=@GHOSTTY@ --launched-from=dbus diff --git a/dist/linux/dbus.service.in b/dist/linux/dbus.service.in index a11ab2c13..85b7c425c 100644 --- a/dist/linux/dbus.service.in +++ b/dist/linux/dbus.service.in @@ -1,4 +1,4 @@ [D-BUS Service] -Name=@@APPID@@ -SystemdService=@@APPID@@.service -Exec=@@GHOSTTY@@ --launched-from=dbus +Name=@APPID@ +SystemdService=@APPID@service +Exec=@GHOSTTY@ --launched-from=dbus diff --git a/dist/linux/systemd.service.in b/dist/linux/systemd.service.in index ede2174a2..b0ef3d59a 100644 --- a/dist/linux/systemd.service.in +++ b/dist/linux/systemd.service.in @@ -1,7 +1,7 @@ [Unit] -Description=@@NAME@@ +Description=@NAME@ [Service] Type=dbus -BusName=@@APPID@@ -ExecStart=@@GHOSTTY@@ --launched-from=systemd +BusName=@APPID@ +ExecStart=@GHOSTTY@ --launched-from=systemd diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 23aceda98..6b305d0da 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -2,6 +2,7 @@ const GhosttyResources = @This(); const std = @import("std"); const builtin = @import("builtin"); +const assert = std.debug.assert; const buildpkg = @import("main.zig"); const Config = @import("Config.zig"); const config_vim = @import("../config/vim.zig"); @@ -220,182 +221,176 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { } // App (Linux) - if (cfg.target.result.os.tag == .linux) { - // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html + if (cfg.target.result.os.tag == .linux) try addLinuxAppResources( + b, + cfg, + &steps, + ); - const name = b.fmt("Ghostty{s}", .{ - switch (cfg.optimize) { - .Debug, .ReleaseSafe => " (Debug)", - .ReleaseFast, .ReleaseSmall => "", - }, - }); + return .{ .steps = steps.items }; +} - const app_id = b.fmt("com.mitchellh.ghostty{s}", .{ - switch (cfg.optimize) { - .Debug, .ReleaseSafe => "-debug", - .ReleaseFast, .ReleaseSmall => "", - }, - }); +/// Add the resource files needed to make Ghostty a proper +/// Linux desktop application (for various desktop environments). +fn addLinuxAppResources( + b: *std.Build, + cfg: *const Config, + steps: *std.ArrayList(*std.Build.Step), +) !void { + assert(cfg.target.result.os.tag == .linux); + + // Background: + // https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html + + const name = b.fmt("Ghostty{s}", .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => " (Debug)", + .ReleaseFast, .ReleaseSmall => "", + }, + }); + + const app_id = b.fmt("com.mitchellh.ghostty{s}", .{ + switch (cfg.optimize) { + .Debug, .ReleaseSafe => "-debug", + .ReleaseFast, .ReleaseSmall => "", + }, + }); + + const exe_abs_path = b.fmt( + "{s}/bin/ghostty", + .{b.install_prefix}, + ); + + // The templates that we will process. The templates are in + // cmake format and will be processed and saved to the + // second element of the tuple. + const Template = struct { std.Build.LazyPath, []const u8 }; + const templates: []const Template = templates: { + var ts: std.ArrayList(Template) = .init(b.allocator); // Desktop file so that we have an icon and other metadata - try steps.append( - formatService( - b, - cfg, - name, - app_id, - b.path("dist/linux/app.desktop.in"), - b.fmt( - "share/applications/{s}.desktop", - .{app_id}, - ), - ), - ); + try ts.append(.{ + b.path("dist/linux/app.desktop.in"), + b.fmt("share/applications/{s}.desktop", .{app_id}), + }); + // Service for DBus activation. - try steps.append( - formatService( - b, - cfg, - name, - app_id, + try ts.append(.{ + if (cfg.flatpak) + b.path("dist/linux/dbus.service.flatpak.in") + else b.path("dist/linux/dbus.service.in"), - b.fmt( - "share/dbus-1/services/{s}.service", - .{app_id}, - ), - ), - ); - // systemd user service - if (!cfg.flatpak) - try steps.append( - formatService( - b, - cfg, - name, + b.fmt("share/dbus-1/services/{s}.service", .{app_id}), + }); + + // systemd user service. This is kind of nasty but systemd + // looks for user services in different paths depending on + // if we are installed as a system package or not (lib vs. + // share) so we have to handle that here. We might be able + // to get away with always installing to both because it + // only ever searches in one... but I don't want to do that hack + // until we have to. + if (!cfg.flatpak) try ts.append(.{ + b.path("dist/linux/systemd.service.in"), + b.fmt( + "{s}/systemd/user/{s}.service", + .{ + if (b.graph.system_package_mode) "lib" else "share", app_id, - b.path("dist/linux/systemd.service.in"), - b.fmt( - "{s}/systemd/user/{s}.service", - .{ - if (b.graph.system_package_mode) "lib" else "share", - app_id, - }, - ), - ), - ); - - // AppStream metainfo so that application has rich metadata within app stores - try steps.append( - formatService( - b, - cfg, - name, - app_id, - b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"), - b.fmt( - "share/metainfo/{s}.metainfo.xml", - .{app_id}, - ), + }, ), + }); + + // AppStream metainfo so that application has rich metadata + // within app stores + try ts.append(.{ + b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"), + b.fmt("share/metainfo/{s}.metainfo.xml", .{app_id}), + }); + + break :templates ts.items; + }; + + // Process all our templates + for (templates) |template| { + const tpl = b.addConfigHeader(.{ + .style = .{ .cmake = template[0] }, + }, .{ + .NAME = name, + .APPID = app_id, + .GHOSTTY = exe_abs_path, + }); + + const copy = b.addInstallFile( + tpl.getOutput(), + template[1], ); - // Right click menu action for Plasma desktop - try steps.append(&b.addInstallFile( - b.path("dist/linux/ghostty_dolphin.desktop"), - "share/kio/servicemenus/com.mitchellh.ghostty.desktop", - ).step); + try steps.append(©.step); + } - // Right click menu action for Nautilus. Note that this _must_ be named - // `ghostty.py`. Using the full app id causes problems (see #5468). - try steps.append(&b.addInstallFile( - b.path("dist/linux/ghostty_nautilus.py"), - "share/nautilus-python/extensions/ghostty.py", - ).step); + // Right click menu action for Plasma desktop + try steps.append(&b.addInstallFile( + b.path("dist/linux/ghostty_dolphin.desktop"), + "share/kio/servicemenus/com.mitchellh.ghostty.desktop", + ).step); - // Various icons that our application can use, including the icon - // that will be used for the desktop. - try steps.append(&b.addInstallFile( - b.path("images/icons/icon_16.png"), - "share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png", - ).step); - try steps.append(&b.addInstallFile( - b.path("images/icons/icon_32.png"), - "share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png", - ).step); - try steps.append(&b.addInstallFile( - b.path("images/icons/icon_128.png"), - "share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png", - ).step); - try steps.append(&b.addInstallFile( - b.path("images/icons/icon_256.png"), - "share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png", - ).step); - try steps.append(&b.addInstallFile( - b.path("images/icons/icon_512.png"), - "share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png", - ).step); - // Flatpaks only support icons up to 512x512. - if (!cfg.flatpak) { - try steps.append(&b.addInstallFile( - b.path("images/icons/icon_1024.png"), - "share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png", - ).step); - } + // Right click menu action for Nautilus. Note that this _must_ be named + // `ghostty.py`. Using the full app id causes problems (see #5468). + try steps.append(&b.addInstallFile( + b.path("dist/linux/ghostty_nautilus.py"), + "share/nautilus-python/extensions/ghostty.py", + ).step); + // Various icons that our application can use, including the icon + // that will be used for the desktop. + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_16.png"), + "share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_32.png"), + "share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_128.png"), + "share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_256.png"), + "share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_512.png"), + "share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png", + ).step); + // Flatpaks only support icons up to 512x512. + if (!cfg.flatpak) { try steps.append(&b.addInstallFile( - b.path("images/icons/icon_16@2x.png"), - "share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png", - ).step); - try steps.append(&b.addInstallFile( - b.path("images/icons/icon_32@2x.png"), - "share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png", - ).step); - try steps.append(&b.addInstallFile( - b.path("images/icons/icon_128@2x.png"), - "share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png", - ).step); - try steps.append(&b.addInstallFile( - b.path("images/icons/icon_256@2x.png"), - "share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png", + b.path("images/icons/icon_1024.png"), + "share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png", ).step); } - return .{ .steps = steps.items }; + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_16@2x.png"), + "share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_32@2x.png"), + "share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_128@2x.png"), + "share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png", + ).step); + try steps.append(&b.addInstallFile( + b.path("images/icons/icon_256@2x.png"), + "share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png", + ).step); } pub fn install(self: *const GhosttyResources) void { const b = self.steps[0].owner; for (self.steps) |step| b.getInstallStep().dependOn(step); } - -pub fn formatService( - b: *std.Build, - cfg: *const Config, - name: []const u8, - app_id: []const u8, - src: std.Build.LazyPath, - dest: []const u8, -) *std.Build.Step { - var cmd = b.addExecutable( - .{ - .name = "desktop-template", - .root_source_file = b.path("src/build/desktop_template.zig"), - .target = b.graph.host, - }, - ); - - const options = b.addOptions(); - - options.addOption([]const u8, "app_id", app_id); - options.addOption([]const u8, "name", name); - options.addOption([]const u8, "ghostty", b.fmt("{s}/bin/ghostty", .{b.install_prefix})); - options.addOption(bool, "flatpak", cfg.flatpak); - - cmd.root_module.addOptions("cfg", options); - - const run = b.addRunArtifact(cmd); - run.setStdIn(.{ .lazy_path = src }); - const output = run.captureStdOut(); - - return &b.addInstallFile(output, dest).step; -} diff --git a/src/build/desktop_template.zig b/src/build/desktop_template.zig deleted file mode 100644 index 7b066bfcc..000000000 --- a/src/build/desktop_template.zig +++ /dev/null @@ -1,31 +0,0 @@ -const std = @import("std"); -const cfg = @import("cfg"); - -pub fn main() !void { - var debug: std.heap.DebugAllocator(.{}) = .init; - defer _ = debug.deinit(); - - const alloc = debug.allocator(); - - const stdin = std.io.getStdIn(); - const stdout = std.io.getStdOut(); - var input = stdin.reader(); - - while (try input.readUntilDelimiterOrEofAlloc(alloc, '\n', 4096)) |line| { - defer alloc.free(line); - - const buf1 = try std.mem.replaceOwned(u8, alloc, line, "@@APPID@@", cfg.app_id); - defer alloc.free(buf1); - - const buf2 = try std.mem.replaceOwned(u8, alloc, buf1, "@@NAME@@", cfg.name); - defer alloc.free(buf2); - - const buf3 = try std.mem.replaceOwned(u8, alloc, buf2, "@@GHOSTTY@@", cfg.ghostty); - defer alloc.free(buf3); - - if (cfg.flatpak and std.mem.startsWith(u8, buf3, "SystemdService=")) continue; - - try stdout.writeAll(buf3); - try stdout.writeAll("\n"); - } -} From 77654eb01cd1c0dcf07e1e427019b8633a98d188 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Jun 2025 13:07:38 -0700 Subject: [PATCH 024/114] use tail to clear the first line of the template --- src/build/GhosttyResources.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 6b305d0da..34b5e35f8 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -321,8 +321,13 @@ fn addLinuxAppResources( .GHOSTTY = exe_abs_path, }); + // Template output has a single header line we want to remove. + // We use `tail` to do it since its part of the POSIX standard. + const tail = b.addSystemCommand(&.{ "tail", "-n", "+2" }); + tail.setStdIn(.{ .lazy_path = tpl.getOutput() }); + const copy = b.addInstallFile( - tpl.getOutput(), + tail.captureStdOut(), template[1], ); From b4e81949ee9397bec34c68969e5a679eda0a7f5d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Jun 2025 13:12:03 -0700 Subject: [PATCH 025/114] wrong service name for dbus systemd service --- dist/linux/dbus.service.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/linux/dbus.service.in b/dist/linux/dbus.service.in index 85b7c425c..2f782a7ed 100644 --- a/dist/linux/dbus.service.in +++ b/dist/linux/dbus.service.in @@ -1,4 +1,4 @@ [D-BUS Service] Name=@APPID@ -SystemdService=@APPID@service +SystemdService=@APPID@.service Exec=@GHOSTTY@ --launched-from=dbus From d92d1cac2a29b813c67006c86aedf13ff6479b91 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Jun 2025 13:16:54 -0700 Subject: [PATCH 026/114] remove unused TODO.md --- TODO.md | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 696bed75f..000000000 --- a/TODO.md +++ /dev/null @@ -1,21 +0,0 @@ -Performance: - -- Loading fonts on startups should probably happen in multiple threads - -Correctness: - -- test wrap against wraptest: https://github.com/mattiase/wraptest - - automate this in some way -- Charsets: UTF-8 vs. ASCII mode - - we only support UTF-8 input right now - - need fallback glyphs if they're not supported - - can effect a crash using `vttest` menu `3 10` since it tries to parse - ASCII as UTF-8. - -Mac: - -- Preferences window - -Major Features: - -- Bell From 6d6dcf863a54bdbaa47c00f76b65a89670bf548a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 26 Jun 2025 13:36:38 -0700 Subject: [PATCH 027/114] Correct AppStream metainfo XML, broken trailing tags --- dist/linux/com.mitchellh.ghostty.metainfo.xml.in | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dist/linux/com.mitchellh.ghostty.metainfo.xml.in b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in index 00d99adb2..42ccc2754 100644 --- a/dist/linux/com.mitchellh.ghostty.metainfo.xml.in +++ b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in @@ -1,8 +1,8 @@ - @APPID@id> - @APPID@desktop - @NAME@name> + @APPID@ + @APPID@.desktop + @NAME@ https://ghostty.org https://ghostty.org/docs https://github.com/ghostty-org/ghostty/discussions From 810ab6a8447f45a84155b58a00606afef5637eb3 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 26 Jun 2025 16:34:51 -0600 Subject: [PATCH 028/114] renderer/OpenGL: revert change to compressed texture format This was applied to the wrong thing by accident, making the custom shader ping-pong textures compressed, which breaks custom shaders because compressed texture formats are not color renderable. Additionally, I've not switched the compressed format to the correct texture options, because I tried that and it turns out that the default compression applied by drivers can't be trusted to be good quality and generally speaking looks terrible. In the future we can explore doing the compression ourselves CPU-side with something like b7enc_rdo. --- src/renderer/OpenGL.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index e112c0df7..3b4ba6d80 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -395,7 +395,7 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options { _ = self; return .{ .format = .rgba, - .internal_format = .srgba_compressed, + .internal_format = .srgba, .target = .@"2D", }; } From d6db3013be7aff7982814c1f9ea4c17d71c71f8f Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 26 Jun 2025 16:37:06 -0600 Subject: [PATCH 029/114] renderer/OpenGL: switch image texture from Rect to 2D We were using the Rectangle target for simpler addressing, since that allows for pixel coordinates instead of normalized coordinates, but there are downsides to rectangle textures, including not supporting compressed texture formats, and we do probably want to use compressed formats in the future, so I'm making this change now. --- src/renderer/OpenGL.zig | 2 +- src/renderer/shaders/glsl/bg_image.f.glsl | 7 ++++--- src/renderer/shaders/glsl/bg_image.v.glsl | 4 ++-- src/renderer/shaders/glsl/image.f.glsl | 2 +- src/renderer/shaders/glsl/image.v.glsl | 7 ++++--- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 3b4ba6d80..cf195361e 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -428,7 +428,7 @@ pub inline fn imageTextureOptions( return .{ .format = format.toPixelFormat(), .internal_format = if (srgb) .srgba else .rgba, - .target = .Rectangle, + .target = .@"2D", }; } diff --git a/src/renderer/shaders/glsl/bg_image.f.glsl b/src/renderer/shaders/glsl/bg_image.f.glsl index 7c3e4363a..ee1195ef5 100644 --- a/src/renderer/shaders/glsl/bg_image.f.glsl +++ b/src/renderer/shaders/glsl/bg_image.f.glsl @@ -4,7 +4,7 @@ // so as to align with our texture's directionality. layout(origin_upper_left) in vec4 gl_FragCoord; -layout(binding = 0) uniform sampler2DRect image; +layout(binding = 0) uniform sampler2D image; flat in vec4 bg_color; flat in vec2 offset; @@ -23,7 +23,7 @@ void main() { // size of the texture to the dest rect size. vec2 tex_coord = (gl_FragCoord.xy - offset) * scale; - vec2 tex_size = textureSize(image); + vec2 tex_size = textureSize(image, 0); // If we need to repeat the texture, wrap the coordinates. if (repeat != 0) { @@ -38,7 +38,8 @@ void main() { { rgba = vec4(0.0); } else { - rgba = texture(image, tex_coord); + // We divide by the texture size to normalize for sampling. + rgba = texture(image, tex_coord / tex_size); if (!use_linear_blending) { rgba = unlinearize(rgba); diff --git a/src/renderer/shaders/glsl/bg_image.v.glsl b/src/renderer/shaders/glsl/bg_image.v.glsl index 875c40518..d55aa174a 100644 --- a/src/renderer/shaders/glsl/bg_image.v.glsl +++ b/src/renderer/shaders/glsl/bg_image.v.glsl @@ -1,6 +1,6 @@ #include "common.glsl" -layout(binding = 0) uniform sampler2DRect image; +layout(binding = 0) uniform sampler2D image; layout(location = 0) in float in_opacity; layout(location = 1) in uint info; @@ -64,7 +64,7 @@ void main() { repeat = info & BG_IMAGE_REPEAT; vec2 screen_size = screen_size; - vec2 tex_size = textureSize(image); + vec2 tex_size = textureSize(image, 0); vec2 dest_size = tex_size; switch (info & BG_IMAGE_FIT) { diff --git a/src/renderer/shaders/glsl/image.f.glsl b/src/renderer/shaders/glsl/image.f.glsl index cd93cf666..4f89d7a78 100644 --- a/src/renderer/shaders/glsl/image.f.glsl +++ b/src/renderer/shaders/glsl/image.f.glsl @@ -1,6 +1,6 @@ #include "common.glsl" -layout(binding = 0) uniform sampler2DRect image; +layout(binding = 0) uniform sampler2D image; in vec2 tex_coord; diff --git a/src/renderer/shaders/glsl/image.v.glsl b/src/renderer/shaders/glsl/image.v.glsl index 55b12ed68..779fae32f 100644 --- a/src/renderer/shaders/glsl/image.v.glsl +++ b/src/renderer/shaders/glsl/image.v.glsl @@ -1,6 +1,6 @@ #include "common.glsl" -layout(binding = 0) uniform sampler2DRect image; +layout(binding = 0) uniform sampler2D image; layout(location = 0) in vec2 grid_pos; layout(location = 1) in vec2 cell_offset; @@ -32,11 +32,12 @@ void main() { // The texture coordinates start at our source x/y // and add the width/height depending on the corner. - // - // We don't need to normalize because we use pixel addressing for our sampler. tex_coord = source_rect.xy; tex_coord += source_rect.zw * corner; + // Normalize the coordinates. + tex_coord /= textureSize(image, 0); + // The position of our image starts at the top-left of the grid cell and // adds the source rect width/height components. vec2 image_pos = (cell_size * grid_pos) + cell_offset; From 937b10b42238ae38e53bd7af12ce2ef680cc3db5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Jun 2025 06:58:10 -0700 Subject: [PATCH 030/114] Update CODEOWNERS for localization managers --- CODEOWNERS | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 3d8a4da3d..343e1dcc1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -81,6 +81,10 @@ # - @ghostty-org/localization/* - Anything related to localization # for a specific locale. # +# - @ghosty-org/localization/manager - Manage all localization tasks +# and tooling. They are not responsible for any specific locale but +# are responsible for the overall localization process and tooling. +# # - @ghostty-org/macos - The Ghostty macOS app and any macOS-specific # features, configurations, etc. # From c6f23bbb32cac497022f19280c929ee9b2ace3ec Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 1 Feb 2025 15:10:05 -0600 Subject: [PATCH 031/114] core: con't copy App and apprt.App Besides avoiding copying, this allows consumers to choose to allocate these structs on the stack or to allocate on the heap. It also gives the apprt.App a stable pointer sooner in the process. --- src/App.zig | 17 +++++------------ src/apprt/embedded.zig | 17 +++++++++++------ src/apprt/glfw.zig | 4 ++-- src/apprt/gtk/App.zig | 4 ++-- src/main_ghostty.zig | 8 +++++--- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/App.zig b/src/App.zig index 3bbeff2c8..fceab0275 100644 --- a/src/App.zig +++ b/src/App.zig @@ -82,28 +82,23 @@ pub const CreateError = Allocator.Error || font.SharedGridSet.InitError; /// /// After calling this function, well behaved apprts should then call /// `focusEvent` to set the initial focus state of the app. -pub fn create( +pub fn init( + self: *App, alloc: Allocator, -) CreateError!*App { - var app = try alloc.create(App); - errdefer alloc.destroy(app); - +) CreateError!void { var font_grid_set = try font.SharedGridSet.init(alloc); errdefer font_grid_set.deinit(); - app.* = .{ + self.* = .{ .alloc = alloc, .surfaces = .{}, .mailbox = .{}, .font_grid_set = font_grid_set, .config_conditional_state = .{}, }; - errdefer app.surfaces.deinit(alloc); - - return app; } -pub fn destroy(self: *App) void { +pub fn deinit(self: *App) void { // Clean up all our surfaces for (self.surfaces.items) |surface| surface.deinit(); self.surfaces.deinit(self.alloc); @@ -114,8 +109,6 @@ pub fn destroy(self: *App) void { // should gracefully close all surfaces. assert(self.font_grid_set.count() == 0); self.font_grid_set.deinit(); - - self.alloc.destroy(self); } /// Tick ticks the app loop. This will drain our mailbox and process those diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 31dd2f46b..2234fdc2d 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -117,10 +117,11 @@ pub const App = struct { config: Config, pub fn init( + self: *App, core_app: *CoreApp, config: *const Config, opts: Options, - ) !App { + ) !void { // We have to clone the config. const alloc = core_app.alloc; var config_clone = try config.clone(alloc); @@ -129,7 +130,7 @@ pub const App = struct { var keymap = try input.Keymap.init(); errdefer keymap.deinit(); - return .{ + self.* = .{ .core_app = core_app, .config = config_clone, .opts = opts, @@ -1316,13 +1317,16 @@ pub const CAPI = struct { opts: *const apprt.runtime.App.Options, config: *const Config, ) !*App { - var core_app = try CoreApp.create(global.alloc); - errdefer core_app.destroy(); + var core_app = try global.alloc.create(CoreApp); + errdefer { + core_app.deinit(); + global.alloc.destroy(core_app); + } // Create our runtime app var app = try global.alloc.create(App); errdefer global.alloc.destroy(app); - app.* = try .init(core_app, config, opts.*); + try app.init(core_app, config, opts.*); errdefer app.terminate(); return app; @@ -1345,7 +1349,8 @@ pub const CAPI = struct { const core_app = v.core_app; v.terminate(); global.alloc.destroy(v); - core_app.destroy(); + core_app.deinit(); + global.alloc.destroy(core_app); } /// Update the focused state of the app. diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 6e131435d..b82771d75 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -50,7 +50,7 @@ pub const App = struct { pub const Options = struct {}; - pub fn init(core_app: *CoreApp, _: Options) !App { + pub fn init(self: *App, core_app: *CoreApp, _: Options) !void { if (comptime builtin.target.os.tag.isDarwin()) { log.warn("WARNING WARNING WARNING: GLFW ON MAC HAS BUGS.", .{}); log.warn("You should use the AppKit-based app instead. The official download", .{}); @@ -107,7 +107,7 @@ pub const App = struct { // We want the event loop to wake up instantly so we can process our tick. glfw.postEmptyEvent(); - return .{ + self.* = .{ .app = core_app, .config = config, .darwin = darwin, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 93e069376..7786f976a 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -110,7 +110,7 @@ quit_timer: union(enum) { expired: void, } = .{ .off = {} }, -pub fn init(core_app: *CoreApp, opts: Options) !App { +pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void { _ = opts; // Log our GTK version @@ -424,7 +424,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3, ); - return .{ + self.* = .{ .core_app = core_app, .app = adw_app, .config = config, diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 985c6c9bd..1eb91b6b2 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -98,11 +98,13 @@ pub fn main() !MainReturn { } // Create our app state - var app = try App.create(alloc); - defer app.destroy(); + var app: App = undefined; + defer app.deinit(); + try app.init(alloc); // Create our runtime app - var app_runtime = try apprt.App.init(app, .{}); + var app_runtime: apprt.App = undefined; + try app_runtime.init(&app, .{}); defer app_runtime.terminate(); // Since - by definition - there are no surfaces when first started, the From 3c49d8775106b2507ad21978c678eac5a1e33669 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 5 Feb 2025 11:30:46 -0600 Subject: [PATCH 032/114] fix order of defer --- src/main_ghostty.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 1eb91b6b2..ca63c8195 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -99,8 +99,8 @@ pub fn main() !MainReturn { // Create our app state var app: App = undefined; - defer app.deinit(); try app.init(alloc); + defer app.deinit(); // Create our runtime app var app_runtime: apprt.App = undefined; From 1979fb92f42086271f5d09fc5b4f24a378a611a6 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 5 Feb 2025 15:04:28 -0600 Subject: [PATCH 033/114] embedded: fix core app init --- src/apprt/embedded.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 2234fdc2d..307efd26a 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1318,6 +1318,7 @@ pub const CAPI = struct { config: *const Config, ) !*App { var core_app = try global.alloc.create(CoreApp); + try core_app.init(global.alloc); errdefer { core_app.deinit(); global.alloc.destroy(core_app); From 83690744b2540d3bc2fc828bfefe53865d499b85 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Jun 2025 09:09:54 -0700 Subject: [PATCH 034/114] reintroduce App.create --- src/App.zig | 17 +++++++++++++++++ src/apprt/embedded.zig | 11 +++-------- src/main_ghostty.zig | 5 ++--- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/App.zig b/src/App.zig index fceab0275..02089ae5b 100644 --- a/src/App.zig +++ b/src/App.zig @@ -76,6 +76,15 @@ first: bool = true, pub const CreateError = Allocator.Error || font.SharedGridSet.InitError; +/// Create a new app instance. This returns a stable pointer to the app +/// instance which is required for callbacks. +pub fn create(alloc: Allocator) CreateError!*App { + var app = try alloc.create(App); + errdefer alloc.destroy(app); + try app.init(alloc); + return app; +} + /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary /// "startup" logic. @@ -111,6 +120,14 @@ pub fn deinit(self: *App) void { self.font_grid_set.deinit(); } +pub fn destroy(self: *App) void { + // Deinitialize the app + self.deinit(); + + // Free the app memory + self.alloc.destroy(self); +} + /// Tick ticks the app loop. This will drain our mailbox and process those /// events. This should be called by the application runtime on every loop /// tick. diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 307efd26a..dec1e4135 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1317,12 +1317,8 @@ pub const CAPI = struct { opts: *const apprt.runtime.App.Options, config: *const Config, ) !*App { - var core_app = try global.alloc.create(CoreApp); - try core_app.init(global.alloc); - errdefer { - core_app.deinit(); - global.alloc.destroy(core_app); - } + const core_app = try CoreApp.create(global.alloc); + errdefer core_app.destroy(); // Create our runtime app var app = try global.alloc.create(App); @@ -1350,8 +1346,7 @@ pub const CAPI = struct { const core_app = v.core_app; v.terminate(); global.alloc.destroy(v); - core_app.deinit(); - global.alloc.destroy(core_app); + core_app.destroy(); } /// Update the focused state of the app. diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index ca63c8195..b75d8397c 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -98,9 +98,8 @@ pub fn main() !MainReturn { } // Create our app state - var app: App = undefined; - try app.init(alloc); - defer app.deinit(); + const app: *App = try App.create(alloc); + defer app.destroy(); // Create our runtime app var app_runtime: apprt.App = undefined; From 4b5ccf79a502efc40c83c1f2b65819b8df41b5d7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Jun 2025 09:16:00 -0700 Subject: [PATCH 035/114] fix compilation issue, tests should've caught this but GHA failed --- src/main_ghostty.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index b75d8397c..567eec5f9 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -103,7 +103,7 @@ pub fn main() !MainReturn { // Create our runtime app var app_runtime: apprt.App = undefined; - try app_runtime.init(&app, .{}); + try app_runtime.init(app, .{}); defer app_runtime.terminate(); // Since - by definition - there are no surfaces when first started, the From 52354b8becbd35f6977a65bc835a11cf8186ea3c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Jun 2025 09:40:29 -0700 Subject: [PATCH 036/114] core: only update selection clipboard on left mouse release Fixes #4800, supercedes #5995 This is a rewrite of #5995 (though the solution is mostly the same since this is pretty straightforward). The main difference is the rebase on the new mouse handling we've had since, and I also continue to update the selection clipboard on non-left-mouse events. --- src/Surface.zig | 56 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 6005635d9..24b1e5be8 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3094,15 +3094,33 @@ pub fn mouseButtonCallback( } } - // Handle link clicking. We want to do this before we do mouse - // reporting or any other mouse handling because a successfully - // clicked link will swallow the event. - if (button == .left and action == .release and self.mouse.over_link) { - const pos = try self.rt_surface.getCursorPos(); - if (self.processLinks(pos)) |processed| { - if (processed) return true; - } else |err| { - log.warn("error processing links err={}", .{err}); + if (button == .left and action == .release) { + // The selection clipboard is only updated for left-click drag when + // the left button is released. This is to avoid the clipboard + // being updated on every mouse move which would be noisy. + if (self.config.copy_on_select != .false) { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const prev_ = self.io.terminal.screen.selection; + if (prev_) |prev| { + try self.setSelection(terminal.Selection.init( + prev.start(), + prev.end(), + false, + )); + } + } + + // Handle link clicking. We want to do this before we do mouse + // reporting or any other mouse handling because a successfully + // clicked link will swallow the event. + if (self.mouse.over_link) { + const pos = try self.rt_surface.getCursorPos(); + if (self.processLinks(pos)) |processed| { + if (processed) return true; + } else |err| { + log.warn("error processing links err={}", .{err}); + } } } @@ -3238,12 +3256,16 @@ pub fn mouseButtonCallback( log.err("error reading time, mouse multi-click won't work err={}", .{err}); } + // In all cases below, we set the selection directly rather than use + // `setSelection` because we want to avoid copying the selection + // to the selection clipboard. For left mouse clicks we only set + // the clipboard on release. switch (self.mouse.left_click_count) { // Single click 1 => { // If we have a selection, clear it. This always happens. if (self.io.terminal.screen.selection != null) { - try self.setSelection(null); + try self.io.terminal.screen.select(null); try self.queueRender(); } }, @@ -3252,7 +3274,7 @@ pub fn mouseButtonCallback( 2 => { const sel_ = self.io.terminal.screen.selectWord(pin.*); if (sel_) |sel| { - try self.setSelection(sel); + try self.io.terminal.screen.select(sel); try self.queueRender(); } }, @@ -3264,7 +3286,7 @@ pub fn mouseButtonCallback( else self.io.terminal.screen.selectLine(.{ .pin = pin.* }); if (sel_) |sel| { - try self.setSelection(sel); + try self.io.terminal.screen.select(sel); try self.queueRender(); } }, @@ -3549,7 +3571,7 @@ pub fn mousePressureCallback( // to handle state inconsistency here. const pin = self.mouse.left_click_pin orelse break :select; const sel = self.io.terminal.screen.selectWord(pin.*) orelse break :select; - try self.setSelection(sel); + try self.io.terminal.screen.select(sel); try self.queueRender(); } } @@ -3768,13 +3790,13 @@ fn dragLeftClickDouble( // If our current mouse position is before the starting position, // then the selection start is the word nearest our current position. if (drag_pin.before(click_pin)) { - try self.setSelection(terminal.Selection.init( + try self.io.terminal.screen.select(.init( word_current.start(), word_start.end(), false, )); } else { - try self.setSelection(terminal.Selection.init( + try self.io.terminal.screen.select(.init( word_start.start(), word_current.end(), false, @@ -3806,7 +3828,7 @@ fn dragLeftClickTriple( } else { sel.endPtr().* = line.end(); } - try self.setSelection(sel); + try self.io.terminal.screen.select(sel); } fn dragLeftClickSingle( @@ -3815,7 +3837,7 @@ fn dragLeftClickSingle( drag_x: f64, ) !void { // This logic is in a separate function so that it can be unit tested. - try self.setSelection(mouseSelection( + try self.io.terminal.screen.select(mouseSelection( self.mouse.left_click_pin.?.*, drag_pin, @intFromFloat(@max(0.0, self.mouse.left_click_xpos)), From 591ef0f40f5fea31b399827bf2602ad85f44e869 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 27 Jun 2025 10:23:21 -0700 Subject: [PATCH 037/114] Move child exit handling logic to apprt thread Fixes #7500 Supersedes #7582 This commit moves the child exit handling logic from the IO thead to the apprt thread. The IO thread now only sends a `child_exited` message to the apprt thread with metadata about the exit conditions (exit code, runtime). From there, the apprt thread can handle the exit situation however is necessary. This commit doesn't change the behavior but it does fix the issue #7500. The behavior is: exit immediately, show abnormal exit message, wait for user input, etc. This also gets us closer to #7649. --- src/Surface.zig | 148 +++++++++++++++++++++++++++++++++++-- src/apprt/surface.zig | 10 ++- src/termio/Exec.zig | 163 +++-------------------------------------- src/termio/Termio.zig | 13 ---- src/termio/Thread.zig | 1 - src/termio/backend.zig | 8 +- src/termio/message.zig | 10 +-- 7 files changed, 163 insertions(+), 190 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 24b1e5be8..286d81383 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -237,6 +237,7 @@ const DerivedConfig = struct { /// For docs for these, see the associated config they are derived from. original_font_size: f32, keybind: configpkg.Keybinds, + abnormal_command_exit_runtime_ms: u32, clipboard_read: configpkg.ClipboardAccess, clipboard_write: configpkg.ClipboardAccess, clipboard_trim_trailing_spaces: bool, @@ -255,6 +256,7 @@ const DerivedConfig = struct { macos_option_as_alt: ?configpkg.OptionAsAlt, selection_clear_on_typing: bool, vt_kam_allowed: bool, + wait_after_command: bool, window_padding_top: u32, window_padding_bottom: u32, window_padding_left: u32, @@ -301,6 +303,7 @@ const DerivedConfig = struct { return .{ .original_font_size = config.@"font-size", .keybind = try config.keybind.clone(alloc), + .abnormal_command_exit_runtime_ms = config.@"abnormal-command-exit-runtime", .clipboard_read = config.@"clipboard-read", .clipboard_write = config.@"clipboard-write", .clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces", @@ -319,6 +322,7 @@ const DerivedConfig = struct { .macos_option_as_alt = config.@"macos-option-as-alt", .selection_clear_on_typing = config.@"selection-clear-on-typing", .vt_kam_allowed = config.@"vt-kam-allowed", + .wait_after_command = config.@"wait-after-command", .window_padding_top = config.@"window-padding-y".top_left, .window_padding_bottom = config.@"window-padding-y".bottom_right, .window_padding_left = config.@"window-padding-x".top_left, @@ -911,11 +915,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .close => self.close(), - // Close without confirmation. - .child_exited => { - self.child_exited = true; - self.close(); - }, + .child_exited => |v| self.childExited(v), .desktop_notification => |notification| { if (!self.config.desktop_notifications) { @@ -948,6 +948,136 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { } } +fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { + // Mark our flag that we exited immediately + self.child_exited = true; + + // If our runtime was below some threshold then we assume that this + // was an abnormal exit and we show an error message. + if (info.runtime_ms <= self.config.abnormal_command_exit_runtime_ms) runtime: { + // On macOS, our exit code detection doesn't work, possibly + // because of our `login` wrapper. More investigation required. + if (comptime builtin.target.os.tag.isDarwin()) break :runtime; + + // If the exit code is 0 then we it was a good exit. + if (info.exit_code == 0) break :runtime; + log.warn("abnormal process exit detected, showing error message", .{}); + + // Update our terminal to note the abnormal exit. In the future we + // may want the apprt to handle this to show some native GUI element. + self.childExitedAbnormally(info) catch |err| { + log.err("error handling abnormal child exit err={}", .{err}); + return; + }; + + return; + } + + // We output a message so that the user knows whats going on and + // doesn't think their terminal just froze. We show this unconditionally + // on close even if `wait_after_command` is false and the surface closes + // immediately because if a user does an `undo` to restore a closed + // surface then they will see this message and know the process has + // completed. + terminal: { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const t: *terminal.Terminal = self.renderer_state.terminal; + t.carriageReturn(); + t.linefeed() catch break :terminal; + t.printString("Process exited. Press any key to close the terminal.") catch + break :terminal; + t.modes.set(.cursor_visible, false); + } + + // Waiting after command we stop here. The terminal is updated, our + // state is updated, and now its up to the user to decide what to do. + if (self.config.wait_after_command) return; + + // If we aren't waiting after the command, then we exit immediately + // with no confirmation. + self.close(); +} + +/// Called when the child process exited abnormally. +fn childExitedAbnormally( + self: *Surface, + info: apprt.surface.Message.ChildExited, +) !void { + var arena = ArenaAllocator.init(self.alloc); + defer arena.deinit(); + const alloc = arena.allocator(); + + // Build up our command for the error message + const command = try std.mem.join(alloc, " ", switch (self.io.backend) { + .exec => |*exec| exec.subprocess.args, + }); + const runtime_str = try std.fmt.allocPrint(alloc, "{d} ms", .{info.runtime_ms}); + + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const t: *terminal.Terminal = self.renderer_state.terminal; + + // No matter what move the cursor back to the column 0. + t.carriageReturn(); + + // Reset styles + try t.setAttribute(.{ .unset = {} }); + + // If there is data in the viewport, we want to scroll down + // a little bit and write a horizontal rule before writing + // our message. This lets the use see the error message the + // command may have output. + const viewport_str = try t.plainString(alloc); + if (viewport_str.len > 0) { + try t.linefeed(); + for (0..t.cols) |_| try t.print(0x2501); + t.carriageReturn(); + try t.linefeed(); + try t.linefeed(); + } + + // Output our error message + try t.setAttribute(.{ .@"8_fg" = .bright_red }); + try t.setAttribute(.{ .bold = {} }); + try t.printString("Ghostty failed to launch the requested command:"); + try t.setAttribute(.{ .unset = {} }); + + t.carriageReturn(); + try t.linefeed(); + try t.linefeed(); + try t.printString(command); + try t.setAttribute(.{ .unset = {} }); + + t.carriageReturn(); + try t.linefeed(); + try t.linefeed(); + try t.printString("Runtime: "); + try t.setAttribute(.{ .@"8_fg" = .red }); + try t.printString(runtime_str); + try t.setAttribute(.{ .unset = {} }); + + // We don't print this on macOS because the exit code is always 0 + // due to the way we launch the process. + if (comptime !builtin.target.os.tag.isDarwin()) { + const exit_code_str = try std.fmt.allocPrint(alloc, "{d}", .{info.exit_code}); + t.carriageReturn(); + try t.linefeed(); + try t.printString("Exit Code: "); + try t.setAttribute(.{ .@"8_fg" = .red }); + try t.printString(exit_code_str); + try t.setAttribute(.{ .unset = {} }); + } + + t.carriageReturn(); + try t.linefeed(); + try t.linefeed(); + try t.printString("Press any key to close the window."); + + // Hide the cursor + t.modes.set(.cursor_visible, false); +} + /// Called when the terminal detects there is a password input prompt. fn passwordInput(self: *Surface, v: bool) !void { { @@ -1953,6 +2083,14 @@ pub fn keyCallback( if (self.io.terminal.modes.get(.disable_keyboard)) return .consumed; } + // If our process is exited and we press a key then we close the + // surface. We may want to eventually move this to the apprt rather + // than in core. + if (self.child_exited and event.action == .press) { + self.close(); + return .closed; + } + // If this input event has text, then we hide the mouse if configured. // We only do this on pressed events to avoid hiding the mouse when we // change focus due to a keybinding (i.e. switching tabs). diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index dce6a3a56..fcc67134b 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -43,8 +43,9 @@ pub const Message = union(enum) { close: void, /// The child process running in the surface has exited. This may trigger - /// a surface close, it may not. - child_exited: void, + /// a surface close, it may not. Additional details about the child + /// command are given in the `ChildExited` struct. + child_exited: ChildExited, /// Show a desktop notification. desktop_notification: struct { @@ -89,6 +90,11 @@ pub const Message = union(enum) { // This enum is a placeholder for future title styles. }; + + pub const ChildExited = struct { + exit_code: u32, + runtime_ms: u64, + }; }; /// A surface mailbox. diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index aed7cefb6..b8f838cf9 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -10,6 +10,7 @@ const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const posix = std.posix; const xev = @import("../global.zig").xev; +const apprt = @import("../apprt.zig"); const build_config = @import("../build_config.zig"); const configpkg = @import("../config.zig"); const crash = @import("../crash/main.zig"); @@ -153,8 +154,6 @@ pub fn threadEnter( // Setup our threadata backend state to be our own td.backend = .{ .exec = .{ .start = process_start, - .abnormal_runtime_threshold_ms = io.config.abnormal_runtime_threshold_ms, - .wait_after_command = io.config.wait_after_command, .write_stream = stream, .process = process, .read_thread = read_thread, @@ -273,83 +272,6 @@ pub fn resize( return try self.subprocess.resize(grid_size, screen_size); } -/// Called when the child process exited abnormally but before the surface -/// is notified. -pub fn childExitedAbnormally( - self: *Exec, - gpa: Allocator, - t: *terminal.Terminal, - exit_code: u32, - runtime_ms: u64, -) !void { - var arena = ArenaAllocator.init(gpa); - defer arena.deinit(); - const alloc = arena.allocator(); - - // Build up our command for the error message - const command = try std.mem.join(alloc, " ", self.subprocess.args); - const runtime_str = try std.fmt.allocPrint(alloc, "{d} ms", .{runtime_ms}); - - // No matter what move the cursor back to the column 0. - t.carriageReturn(); - - // Reset styles - try t.setAttribute(.{ .unset = {} }); - - // If there is data in the viewport, we want to scroll down - // a little bit and write a horizontal rule before writing - // our message. This lets the use see the error message the - // command may have output. - const viewport_str = try t.plainString(alloc); - if (viewport_str.len > 0) { - try t.linefeed(); - for (0..t.cols) |_| try t.print(0x2501); - t.carriageReturn(); - try t.linefeed(); - try t.linefeed(); - } - - // Output our error message - try t.setAttribute(.{ .@"8_fg" = .bright_red }); - try t.setAttribute(.{ .bold = {} }); - try t.printString("Ghostty failed to launch the requested command:"); - try t.setAttribute(.{ .unset = {} }); - - t.carriageReturn(); - try t.linefeed(); - try t.linefeed(); - try t.printString(command); - try t.setAttribute(.{ .unset = {} }); - - t.carriageReturn(); - try t.linefeed(); - try t.linefeed(); - try t.printString("Runtime: "); - try t.setAttribute(.{ .@"8_fg" = .red }); - try t.printString(runtime_str); - try t.setAttribute(.{ .unset = {} }); - - // We don't print this on macOS because the exit code is always 0 - // due to the way we launch the process. - if (comptime !builtin.target.os.tag.isDarwin()) { - const exit_code_str = try std.fmt.allocPrint(alloc, "{d}", .{exit_code}); - t.carriageReturn(); - try t.linefeed(); - try t.printString("Exit Code: "); - try t.setAttribute(.{ .@"8_fg" = .red }); - try t.printString(exit_code_str); - try t.setAttribute(.{ .unset = {} }); - } - - t.carriageReturn(); - try t.linefeed(); - try t.linefeed(); - try t.printString("Press any key to close the window."); - - // Hide the cursor - t.modes.set(.cursor_visible, false); -} - /// This outputs an error message when exec failed and we are the /// child process. This returns so the caller should probably exit /// after calling this. @@ -386,63 +308,13 @@ fn processExitCommon(td: *termio.Termio.ThreadData, exit_code: u32) void { .{ exit_code, runtime_ms orelse 0 }, ); - // If our runtime was below some threshold then we assume that this - // was an abnormal exit and we show an error message. - if (runtime_ms) |runtime| runtime: { - // On macOS, our exit code detection doesn't work, possibly - // because of our `login` wrapper. More investigation required. - if (comptime !builtin.target.os.tag.isDarwin()) { - // If our exit code is zero, then the command was successful - // and we don't ever consider it abnormal. - if (exit_code == 0) break :runtime; - } - - // Our runtime always has to be under the threshold to be - // considered abnormal. This is because a user can always - // manually do something like `exit 1` in their shell to - // force the exit code to be non-zero. We only want to detect - // abnormal exits that happen so quickly the user can't react. - if (runtime > execdata.abnormal_runtime_threshold_ms) break :runtime; - log.warn("abnormal process exit detected, showing error message", .{}); - - // Notify our main writer thread which has access to more - // information so it can show a better error message. - td.mailbox.send(.{ - .child_exited_abnormally = .{ - .exit_code = exit_code, - .runtime_ms = runtime, - }, - }, null); - td.mailbox.notify(); - - return; - } - - // We output a message so that the user knows whats going on and - // doesn't think their terminal just froze. We show this unconditionally - // on close even if `wait_after_command` is false and the surface closes - // immediately because if a user does an `undo` to restore a closed - // surface then they will see this message and know the process has - // completed. - terminal: { - td.renderer_state.mutex.lock(); - defer td.renderer_state.mutex.unlock(); - const t = td.renderer_state.terminal; - t.carriageReturn(); - t.linefeed() catch break :terminal; - t.printString("Process exited. Press any key to close the terminal.") catch - break :terminal; - t.modes.set(.cursor_visible, false); - } - - // If we're purposely waiting then we just return since the process - // exited flag is set to true. This allows the terminal window to remain - // open. - if (execdata.wait_after_command) return; - - // Notify our surface we want to close + // We always notify the surface immediately that the child has + // exited and some metadata about the exit. _ = td.surface_mailbox.push(.{ - .child_exited = {}, + .child_exited = .{ + .exit_code = exit_code, + .runtime_ms = runtime_ms orelse 0, + }, }, .{ .forever = {} }); } @@ -563,14 +435,8 @@ pub fn queueWrite( _ = self; const exec = &td.backend.exec; - // If our process is exited then we send our surface a message - // about it but we don't queue any more writes. - if (exec.exited) { - _ = td.surface_mailbox.push(.{ - .child_exited = {}, - }, .{ .forever = {} }); - return; - } + // If our process is exited then we don't send any more writes. + if (exec.exited) return; // We go through and chunk the data if necessary to fit into // our cached buffers that we can queue to the stream. @@ -658,17 +524,6 @@ pub const ThreadData = struct { start: std.time.Instant, exited: bool = false, - /// The number of milliseconds below which we consider a process - /// exit to be abnormal. This is used to show an error message - /// when the process exits too quickly. - abnormal_runtime_threshold_ms: u32, - - /// If true, do not immediately send a child exited message to the - /// surface to close the surface when the command exits. If this is - /// false we'll show a process exited message and wait for user input - /// to close the surface. - wait_after_command: bool, - /// The data stream is the main IO for the pty. write_stream: xev.Stream, diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index c474d55bb..865a2df86 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -168,8 +168,6 @@ pub const DerivedConfig = struct { foreground: configpkg.Config.Color, background: configpkg.Config.Color, osc_color_report_format: configpkg.Config.OSCColorReportFormat, - abnormal_runtime_threshold_ms: u32, - wait_after_command: bool, enquiry_response: []const u8, pub fn init( @@ -190,8 +188,6 @@ pub const DerivedConfig = struct { .foreground = config.foreground, .background = config.background, .osc_color_report_format = config.@"osc-color-report-format", - .abnormal_runtime_threshold_ms = config.@"abnormal-command-exit-runtime", - .wait_after_command = config.@"wait-after-command", .enquiry_response = try alloc.dupe(u8, config.@"enquiry-response"), // This has to be last so that we copy AFTER the arena allocations @@ -660,15 +656,6 @@ pub fn jumpToPrompt(self: *Termio, delta: isize) !void { try self.renderer_wakeup.notify(); } -/// Called when the child process exited abnormally but before -/// the surface is notified. -pub fn childExitedAbnormally(self: *Termio, exit_code: u32, runtime_ms: u64) !void { - self.renderer_state.mutex.lock(); - defer self.renderer_state.mutex.unlock(); - const t = self.renderer_state.terminal; - try self.backend.childExitedAbnormally(self.alloc, t, exit_code, runtime_ms); -} - /// Called when focus is gained or lost (when focus events are enabled) pub fn focusGained(self: *Termio, td: *ThreadData, focused: bool) !void { self.renderer_state.mutex.lock(); diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 58a04f5a7..a701a29f8 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -311,7 +311,6 @@ fn drainMailbox( .jump_to_prompt => |v| try io.jumpToPrompt(v), .start_synchronized_output => self.startSynchronizedOutput(cb), .linefeed_mode => |v| self.flags.linefeed_mode = v, - .child_exited_abnormally => |v| try io.childExitedAbnormally(v.exit_code, v.runtime_ms), .focused => |v| try io.focusGained(data, v), .write_small => |v| try io.queueWrite( data, diff --git a/src/termio/backend.zig b/src/termio/backend.zig index 46ed3431c..280fcbde1 100644 --- a/src/termio/backend.zig +++ b/src/termio/backend.zig @@ -122,11 +122,7 @@ pub const ThreadData = union(Kind) { } pub fn changeConfig(self: *ThreadData, config: *termio.DerivedConfig) void { - switch (self.*) { - .exec => |*exec| { - exec.abnormal_runtime_threshold_ms = config.abnormal_runtime_threshold_ms; - exec.wait_after_command = config.wait_after_command; - }, - } + _ = self; + _ = config; } }; diff --git a/src/termio/message.zig b/src/termio/message.zig index 42767e109..e497a298f 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -1,6 +1,7 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const apprt = @import("../apprt.zig"); const renderer = @import("../renderer.zig"); const terminal = @import("../terminal/main.zig"); const termio = @import("../termio.zig"); @@ -58,15 +59,6 @@ pub const Message = union(enum) { /// Enable or disable linefeed mode (mode 20). linefeed_mode: bool, - /// The child exited abnormally. The termio state is marked - /// as process exited but the surface hasn't been notified to - /// close because termio can use this to update the terminal - /// with an error message. - child_exited_abnormally: struct { - exit_code: u32, - runtime_ms: u64, - }, - /// The surface gained or lost focus. focused: bool, From 138f74524e1c909620ba1c3560e032ceea79695f Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Fri, 27 Jun 2025 21:11:41 -0400 Subject: [PATCH 038/114] terminal: fix unexpected line wrapping in tests These tests write specific lines into a 10-column-wide test screen. The "prompt3$ input3\n" writes exceed that column limit, and some of their characters wrap onto the following line. These tests' current assertions aren't sensitive to that overflow, but I spotted the problem while doing some related work, and I thought it worth making these corrections to avoid any future surprises. --- src/terminal/Screen.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 8cdaf3fa2..5b772ab84 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -7911,7 +7911,7 @@ test "Screen: selectOutput" { try s.testWriteString("input2\n"); // 3 try s.testWriteString("output2output2output2output2\n"); // 4, 5, 6 due to overflow try s.testWriteString("output2\n"); // 7 - try s.testWriteString("prompt3$ input3\n"); // 8 + try s.testWriteString("$ input3\n"); // 8 try s.testWriteString("output3\n"); // 9 try s.testWriteString("output3\n"); // 10 try s.testWriteString("output3"); // 11 @@ -7999,14 +7999,14 @@ test "Screen: selectOutput" { } }, s.pages.pointFromPin(.active, sel.start()).?); try testing.expectEqual(point.Point{ .active = .{ .x = 9, - .y = 12, + .y = 11, } }, s.pages.pointFromPin(.active, sel.end()).?); } // input / prompt at y = 0, pt.y = 0 { s.deinit(); s = try init(alloc, 10, 5, 0); - try s.testWriteString("prompt1$ input1\n"); + try s.testWriteString("$ input1\n"); try s.testWriteString("output1\n"); try s.testWriteString("prompt2\n"); { @@ -8042,7 +8042,7 @@ test "Screen: selectPrompt basics" { try s.testWriteString("input2\n"); // 3 try s.testWriteString("output2\n"); // 4 try s.testWriteString("output2\n"); // 5 - try s.testWriteString("prompt3$ input3\n"); // 6 + try s.testWriteString("$ input3\n"); // 6 try s.testWriteString("output3\n"); // 7 try s.testWriteString("output3\n"); // 8 try s.testWriteString("output3"); // 9 @@ -8257,7 +8257,7 @@ test "Screen: promptPath" { try s.testWriteString("input2\n"); // 3 try s.testWriteString("output2\n"); // 4 try s.testWriteString("output2\n"); // 5 - try s.testWriteString("prompt3$ input3\n"); // 6 + try s.testWriteString("$ input3\n"); // 6 try s.testWriteString("output3\n"); // 7 try s.testWriteString("output3\n"); // 8 try s.testWriteString("output3"); // 9 From 22a624e56006ff33b97507656039277146b8f0e1 Mon Sep 17 00:00:00 2001 From: Islam Sharabash Date: Sat, 28 Jun 2025 09:01:41 +0200 Subject: [PATCH 039/114] Equalize splits based on children oriented in the same direction This changes equalization so it only counts children oriented in the same direction. This makes splits a bit more aesthetic, and replicates how split equalization works in neovim. --- macos/Sources/Features/Splits/SplitTree.swift | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 1c4be7dd6..b353f6cbe 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -610,14 +610,18 @@ extension SplitTree.Node { return (self, 1) case .split(let split): - // Recursively equalize children - let (leftNode, leftWeight) = split.left.equalizeWithWeight() - let (rightNode, rightWeight) = split.right.equalizeWithWeight() - + // Calculate weights based on split direction + let leftWeight = split.left.weightForDirection(split.direction) + let rightWeight = split.right.weightForDirection(split.direction) + // Calculate new ratio based on relative weights let totalWeight = leftWeight + rightWeight let newRatio = Double(leftWeight) / Double(totalWeight) - + + // Recursively equalize children + let (leftNode, _) = split.left.equalizeWithWeight() + let (rightNode, _) = split.right.equalizeWithWeight() + // Create new split with equalized ratio let newSplit = Split( direction: split.direction, @@ -630,6 +634,23 @@ extension SplitTree.Node { } } + /// Calculate weight for equalization based on split direction. + /// Children with the same direction contribute their full weight, + /// children with different directions count as 1. + private func weightForDirection(_ direction: SplitTree.Direction) -> Int { + switch self { + case .leaf: + return 1 + case .split(let split): + if split.direction == direction { + return split.left.weightForDirection(direction) + split.right.weightForDirection(direction) + } else { + return 1 + } + } + } + + /// Calculate the bounds of all views in this subtree based on split ratios func calculateViewBounds(in bounds: CGRect) -> [(view: ViewType, bounds: CGRect)] { switch self { From 4ae75cc868e56d2c228bdd04420cfe12d42e58cb Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sat, 28 Jun 2025 00:21:38 -0700 Subject: [PATCH 040/114] Don't pass arena allocator to os.open --- src/config/edit.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/edit.zig b/src/config/edit.zig index 871a1a755..ae4394942 100644 --- a/src/config/edit.zig +++ b/src/config/edit.zig @@ -20,10 +20,10 @@ pub fn open(alloc_gpa: Allocator) !void { // Use an arena to make memory management easier in here. var arena = ArenaAllocator.init(alloc_gpa); defer arena.deinit(); - const alloc = arena.allocator(); + const alloc_arena = arena.allocator(); // Get the path we should open - const config_path = try configPath(alloc); + const config_path = try configPath(alloc_arena); // Create config directory recursively. if (std.fs.path.dirname(config_path)) |config_dir| { @@ -41,7 +41,7 @@ pub fn open(alloc_gpa: Allocator) !void { } }; - try internal_os.open(alloc, .text, config_path); + try internal_os.open(alloc_gpa, .text, config_path); } /// Returns the config path to use for open for the current OS. From 33e07c87c90242cf529dce6975538ced58242fdc Mon Sep 17 00:00:00 2001 From: azhn Date: Mon, 14 Apr 2025 19:56:22 +1000 Subject: [PATCH 041/114] deps: Default gtk4-layer-shell system integration to true We default system-integration to true as this is a shared library that must be installed on a library path and it is recommended to use the system package. If the system does not package gtk4-layer-shell then doing `zig build -fno-sys` will now correctly build and install the shared library under a lib/ subdirectory of the output prefix. The output prefix should be a default library path (`/lib`, `/usr/lib`, or a lib64 variant) otherwise a custom library path can be configured using ldconfig (see `man ld.so 8`) --- src/build/Config.zig | 10 +++++++++- src/build/SharedDeps.zig | 9 ++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/build/Config.zig b/src/build/Config.zig index e55da3860..5f8780af9 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -361,7 +361,6 @@ pub fn init(b: *std.Build) !Config { "libpng", "zlib", "oniguruma", - "gtk4-layer-shell", }) |dep| { _ = b.systemIntegrationOption( dep, @@ -387,6 +386,15 @@ pub fn init(b: *std.Build) !Config { }) |dep| { _ = b.systemIntegrationOption(dep, .{ .default = false }); } + + // These are dynamic libraries we default to true, preferring + // to use system packages over building and installing libs + // as they require additional ldconfig of library paths or + // patching the rpath of the program to discover the dynamic library + // at runtime + for (&[_][]const u8{"gtk4-layer-shell"}) |dep| { + _ = b.systemIntegrationOption(dep, .{ .default = true }); + } } return config; diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index acd3ed1d8..ec97a9c9f 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -652,14 +652,13 @@ fn addGTK( // IMPORTANT: gtk4-layer-shell must be linked BEFORE // wayland-client, as it relies on shimming libwayland's APIs. if (b.systemIntegrationOption("gtk4-layer-shell", .{})) { - step.linkSystemLibrary2( - "gtk4-layer-shell-0", - dynamic_link_opts, - ); + step.linkSystemLibrary2("gtk4-layer-shell-0", dynamic_link_opts); } else { // gtk4-layer-shell *must* be dynamically linked, // so we don't add it as a static library - step.linkLibrary(gtk4_layer_shell.artifact("gtk4-layer-shell")); + const shared_lib = gtk4_layer_shell.artifact("gtk4-layer-shell"); + b.installArtifact(shared_lib); + step.linkLibrary(shared_lib); } } From 46b86570f21f68a32f6ae29caeb8e816ba289f67 Mon Sep 17 00:00:00 2001 From: azhn Date: Mon, 14 Apr 2025 21:20:03 +1000 Subject: [PATCH 042/114] deps: Enable building gtk4-layer-shell without system integration --- snap/snapcraft.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index d7fc63712..7c8b6dc4f 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -76,9 +76,10 @@ parts: - git - patchelf - gettext + # TODO: Remove -fno-sys=gtk4-layer-shell when we upgrade to a version that packages it Ubuntu 24.10+ override-build: | craftctl set version=$(cat VERSION) - $CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast -Dcpu=baseline + $CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast -Dcpu=baseline -fno-sys=gtk4-layer-shell cp -rp zig-out/* $CRAFT_PART_INSTALL/ # Install libgtk4-layer-shell.so mkdir -p $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR From a8cad9831a09634526b612e4352153935f12b294 Mon Sep 17 00:00:00 2001 From: azhn Date: Sat, 28 Jun 2025 19:21:32 +1000 Subject: [PATCH 043/114] Remove copying libgtk4-layer-shell.so from cache since install is fixed --- snap/snapcraft.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 7c8b6dc4f..df8d6ae53 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -81,9 +81,6 @@ parts: craftctl set version=$(cat VERSION) $CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast -Dcpu=baseline -fno-sys=gtk4-layer-shell cp -rp zig-out/* $CRAFT_PART_INSTALL/ - # Install libgtk4-layer-shell.so - mkdir -p $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR - cp .zig-cache/*/*/libgtk4-layer-shell.so $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/ sed -i 's|Icon=com.mitchellh.ghostty|Icon=${SNAP}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop libs: From 5fa737834bddc3d0f37b85163a0640132813f759 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sat, 28 Jun 2025 11:33:02 +0200 Subject: [PATCH 044/114] gtk(wayland): prevent gtk4-layer-shell crash on old versions Supersedes #7154 In gtk4-layer-shell versions < 1.0.4, the app could crash upon opening a quick terminal window on certain compositors that implement the `xdg_wm_dialog_v1` protocol. The exact reason is a bit complicated, but is nicely summarized in the upstream issue (wmww/gtk4-layer-shell#50). The circumstances that could cause this crash to occur should gradually diminish as distros update to newer gtk4-layer-shell versions, but this is known to crash on Fedora 41 and Hyprland, which could be a sizable chunk of our userbase given that this would also occur on GNOME/Mutter and KDE/KWin. The diff should be minimal enough that this can be removed or reverted once this band-aid fix is no longer necessary. --- pkg/gtk4-layer-shell/src/main.zig | 10 ++++ src/apprt/gtk/winproto/wayland.zig | 80 ++++++++++++++++++++++-------- 2 files changed, 69 insertions(+), 21 deletions(-) diff --git a/pkg/gtk4-layer-shell/src/main.zig b/pkg/gtk4-layer-shell/src/main.zig index 06936bba2..f7848ea94 100644 --- a/pkg/gtk4-layer-shell/src/main.zig +++ b/pkg/gtk4-layer-shell/src/main.zig @@ -1,3 +1,5 @@ +const std = @import("std"); + const c = @cImport({ @cInclude("gtk4-layer-shell.h"); }); @@ -31,6 +33,14 @@ pub fn getProtocolVersion() c_uint { return c.gtk_layer_get_protocol_version(); } +pub fn getLibraryVersion() std.SemanticVersion { + return .{ + .major = c.gtk_layer_get_major_version(), + .minor = c.gtk_layer_get_minor_version(), + .patch = c.gtk_layer_get_micro_version(), + }; +} + pub fn initForWindow(window: *gtk.Window) void { c.gtk_layer_init_for_window(@ptrCast(window)); } diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index cbe8c01a4..ae3c871f2 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -37,6 +37,19 @@ pub const App = struct { default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null, xdg_activation: ?*xdg.ActivationV1 = null, + + /// Whether the xdg_wm_dialog_v1 protocol is present. + /// + /// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user + /// creates a quick terminal, and we need to ensure this fails + /// gracefully if this situation occurs. + /// + /// FIXME: This is a temporary workaround - we should remove this when + /// all of our supported distros drop support for affected old + /// gtk4-layer-shell versions. + /// + /// See https://github.com/wmww/gtk4-layer-shell/issues/50 + xdg_wm_dialog_present: bool = false, }; pub fn init( @@ -95,11 +108,21 @@ pub const App = struct { return null; } - pub fn supportsQuickTerminal(_: App) bool { + pub fn supportsQuickTerminal(self: App) bool { if (!layer_shell.isSupported()) { log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{}); return false; } + + if (self.context.xdg_wm_dialog_present and layer_shell.getLibraryVersion().order(.{ + .major = 1, + .minor = 0, + .patch = 4, + }) == .lt) { + log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{}); + return false; + } + return true; } @@ -111,26 +134,38 @@ pub const App = struct { layer_shell.setNamespace(window, "ghostty-quick-terminal"); } + fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type { + // Globals should be optional pointers + const T = switch (@typeInfo(field.type)) { + .optional => |o| switch (@typeInfo(o.child)) { + .pointer => |v| v.child, + else => return null, + }, + else => return null, + }; + + // Only process Wayland interfaces + if (!@hasDecl(T, "interface")) return null; + return T; + } + fn registryListener( registry: *wl.Registry, event: wl.Registry.Event, context: *Context, ) void { - inline for (@typeInfo(Context).@"struct".fields) |field| { - // Globals should be optional pointers - const T = switch (@typeInfo(field.type)) { - .optional => |o| switch (@typeInfo(o.child)) { - .pointer => |v| v.child, - else => continue, - }, - else => continue, - }; + const ctx_fields = @typeInfo(Context).@"struct".fields; - // Only process Wayland interfaces - if (!@hasDecl(T, "interface")) continue; + switch (event) { + .global => |v| global: { + // We don't actually do anything with this other than checking + // for its existence, so we process this separately. + if (std.mem.orderZ(u8, v.interface, "xdg_wm_dialog_v1") == .eq) + context.xdg_wm_dialog_present = true; + + inline for (ctx_fields) |field| { + const T = getInterfaceType(field) orelse continue; - switch (event) { - .global => |v| global: { if (std.mem.orderZ( u8, v.interface, @@ -148,19 +183,22 @@ pub const App = struct { ); return; }; - }, + } + }, - // This should be a rare occurrence, but in case a global - // is suddenly no longer available, we destroy and unset it - // as the protocol mandates. - .global_remove => |v| remove: { + // This should be a rare occurrence, but in case a global + // is suddenly no longer available, we destroy and unset it + // as the protocol mandates. + .global_remove => |v| remove: { + inline for (ctx_fields) |field| { + if (getInterfaceType(field) == null) continue; const global = @field(context, field.name) orelse break :remove; if (global.getId() == v.name) { global.destroy(); @field(context, field.name) = null; } - }, - } + } + }, } } From ce015899f36374ddb67c3b6ced631168c762bfaf Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 17 Mar 2025 14:22:47 +0100 Subject: [PATCH 045/114] gtk: add "remember choice" toggle for clipboard confirmation dialog --- src/apprt/gtk/ClipboardConfirmationWindow.zig | 36 +++++- src/apprt/gtk/adw_version.zig | 4 + src/apprt/gtk/style.css | 6 +- src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp | 104 ++++++++++-------- src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp | 94 +++++++++------- 5 files changed, 153 insertions(+), 91 deletions(-) diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig index fab1aa893..bf1549021 100644 --- a/src/apprt/gtk/ClipboardConfirmationWindow.zig +++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig @@ -17,7 +17,7 @@ const adw_version = @import("adw_version.zig"); const log = std.log.scoped(.gtk); -const DialogType = if (adw_version.atLeast(1, 5, 0)) adw.AlertDialog else adw.MessageDialog; +const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog; app: *App, dialog: *DialogType, @@ -28,6 +28,7 @@ text_view: *gtk.TextView, text_view_scroll: *gtk.ScrolledWindow, reveal_button: *gtk.Button, hide_button: *gtk.Button, +remember_choice: if (adw_version.supportsSwitchRow()) ?*adw.SwitchRow else ?*anyopaque, pub fn create( app: *App, @@ -89,6 +90,10 @@ fn init( const reveal_button = builder.getObject(gtk.Button, "reveal_button").?; const hide_button = builder.getObject(gtk.Button, "hide_button").?; const text_view_scroll = builder.getObject(gtk.ScrolledWindow, "text_view_scroll").?; + const remember_choice = if (adw_version.supportsSwitchRow()) + builder.getObject(adw.SwitchRow, "remember_choice") + else + null; const copy = try app.core_app.alloc.dupeZ(u8, data); errdefer app.core_app.alloc.free(copy); @@ -102,6 +107,7 @@ fn init( .text_view_scroll = text_view_scroll, .reveal_button = reveal_button, .hide_button = hide_button, + .remember_choice = remember_choice, }; const buffer = gtk.TextBuffer.new(null); @@ -152,8 +158,10 @@ fn init( } } -fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void { - if (std.mem.orderZ(u8, response, "ok") == .eq) { +fn handleResponse(self: *ClipboardConfirmation, response: [*:0]const u8) void { + const is_ok = std.mem.orderZ(u8, response, "ok") == .eq; + + if (is_ok) { self.core_surface.completeClipboardRequest( self.pending_req, self.data, @@ -162,8 +170,30 @@ fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) log.err("Failed to requeue clipboard request: {}", .{err}); }; } + + if (self.remember_choice) |remember| remember: { + if (!adw_version.supportsSwitchRow()) break :remember; + if (remember.getActive() == 0) break :remember; + + switch (self.pending_req) { + .osc_52_read => self.core_surface.config.clipboard_read = if (is_ok) .allow else .deny, + .osc_52_write => self.core_surface.config.clipboard_write = if (is_ok) .allow else .deny, + .paste => {}, + } + } + self.destroy(); } +fn gtkChoose(dialog_: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void { + const dialog = gobject.ext.cast(DialogType, dialog_.?).?; + const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud.?)); + const response = dialog.chooseFinish(result); + self.handleResponse(response); +} + +fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.C) void { + self.handleResponse(response); +} fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void { self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true)); diff --git a/src/apprt/gtk/adw_version.zig b/src/apprt/gtk/adw_version.zig index ff7439a21..7ce88f585 100644 --- a/src/apprt/gtk/adw_version.zig +++ b/src/apprt/gtk/adw_version.zig @@ -109,6 +109,10 @@ pub inline fn supportsTabOverview() bool { return atLeast(1, 4, 0); } +pub inline fn supportsSwitchRow() bool { + return atLeast(1, 4, 0); +} + pub inline fn supportsToolbarView() bool { return atLeast(1, 4, 0); } diff --git a/src/apprt/gtk/style.css b/src/apprt/gtk/style.css index 7c4b53d03..2051ab1e3 100644 --- a/src/apprt/gtk/style.css +++ b/src/apprt/gtk/style.css @@ -64,14 +64,18 @@ window.ssd.no-border-radius { padding: 0; } +.clipboard-overlay { + border-radius: 10px; +} + .clipboard-content-view { filter: blur(0px); transition: filter 0.3s ease; + border-radius: 10px; } .clipboard-content-view.blurred { filter: blur(5px); - transition: filter 0.3s ease; } .command-palette-search { diff --git a/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp b/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp index 640556535..ad0b5c01f 100644 --- a/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp +++ b/src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp @@ -14,58 +14,72 @@ Adw.AlertDialog clipboard_confirmation_window { default-response: "cancel"; close-response: "cancel"; - extra-child: Overlay { + extra-child: ListBox { + selection-mode: none; + styles [ - "osd", + "boxed-list-separate", ] - ScrolledWindow text_view_scroll { - width-request: 500; - height-request: 250; - - TextView text_view { - cursor-visible: false; - editable: false; - monospace: true; - top-margin: 8; - left-margin: 8; - bottom-margin: 8; - right-margin: 8; - - styles [ - "clipboard-content-view", - ] - } - } - - [overlay] - Button reveal_button { - visible: false; - halign: end; - valign: start; - margin-end: 12; - margin-top: 12; - - Image { - icon-name: "view-reveal-symbolic"; - } - } - - [overlay] - Button hide_button { - visible: false; - halign: end; - valign: start; - margin-end: 12; - margin-top: 12; - + Overlay { styles [ - "opaque", + "osd", + "clipboard-overlay", ] - Image { - icon-name: "view-conceal-symbolic"; + ScrolledWindow text_view_scroll { + width-request: 500; + height-request: 200; + + TextView text_view { + cursor-visible: false; + editable: false; + monospace: true; + top-margin: 8; + left-margin: 8; + bottom-margin: 8; + right-margin: 8; + + styles [ + "clipboard-content-view", + ] + } } + + [overlay] + Button reveal_button { + visible: false; + halign: end; + valign: start; + margin-end: 12; + margin-top: 12; + + Image { + icon-name: "view-reveal-symbolic"; + } + } + + [overlay] + Button hide_button { + visible: false; + halign: end; + valign: start; + margin-end: 12; + margin-top: 12; + + styles [ + "opaque", + ] + + Image { + icon-name: "view-conceal-symbolic"; + } + } + } + + Adw.SwitchRow remember_choice { + title: _("Remember choice for this split"); + subtitle: _("Reload configuration to show this prompt again"); } }; } diff --git a/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp b/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp index 2e28359ff..b71131940 100644 --- a/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp +++ b/src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp @@ -14,58 +14,68 @@ Adw.AlertDialog clipboard_confirmation_window { default-response: "cancel"; close-response: "cancel"; - extra-child: Overlay { + extra-child: ListBox { + selection-mode: none; + styles [ - "osd", + "boxed-list-separate", ] - ScrolledWindow text_view_scroll { - width-request: 500; - height-request: 250; + Overlay { + styles [ + "osd", + "clipboard-overlay", + ] - TextView text_view { - cursor-visible: false; - editable: false; - monospace: true; - top-margin: 8; - left-margin: 8; - bottom-margin: 8; - right-margin: 8; + ScrolledWindow text_view_scroll { + width-request: 500; + height-request: 200; + + TextView text_view { + cursor-visible: false; + editable: false; + monospace: true; + top-margin: 8; + left-margin: 8; + bottom-margin: 8; + right-margin: 8; + + styles [ + "clipboard-content-view", + ] + } + } + + [overlay] + Button reveal_button { + visible: false; + halign: end; + valign: start; + margin-end: 12; + margin-top: 12; + + Image { + icon-name: "view-reveal-symbolic"; + } + } + + [overlay] + Button hide_button { + visible: false; + halign: end; + valign: start; + margin-end: 12; + margin-top: 12; styles [ - "clipboard-content-view", + "opaque", ] } } - [overlay] - Button reveal_button { - visible: false; - halign: end; - valign: start; - margin-end: 12; - margin-top: 12; - - Image { - icon-name: "view-reveal-symbolic"; - } - } - - [overlay] - Button hide_button { - visible: false; - halign: end; - valign: start; - margin-end: 12; - margin-top: 12; - - styles [ - "opaque", - ] - - Image { - icon-name: "view-conceal-symbolic"; - } + Adw.SwitchRow remember_choice { + title: _("Remember choice for this split"); + subtitle: _("Reload configuration to show this prompt again"); } }; } From fbe94156f9de5b766872ff6f7750bfba9796163e Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sat, 28 Jun 2025 17:01:49 +0200 Subject: [PATCH 046/114] translations: update --- po/com.mitchellh.ghostty.pot | 53 ++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/po/com.mitchellh.ghostty.pot b/po/com.mitchellh.ghostty.pot index da0efbbee..7691f91b5 100644 --- a/po/com.mitchellh.ghostty.pot +++ b/po/com.mitchellh.ghostty.pot @@ -1,5 +1,5 @@ # SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR Mitchell Hashimoto, Ghostty contributors +# Copyright (C) YEAR "Mitchell Hashimoto, Ghostty contributors" # This file is distributed under the same license as the com.mitchellh.ghostty package. # FIRST AUTHOR , YEAR. # @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-23 16:58+0800\n" +"POT-Creation-Date: 2025-06-28 17:01+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title." msgstr "" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "" @@ -35,22 +36,26 @@ msgid "OK" msgstr "" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "" @@ -89,7 +94,7 @@ msgstr "" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "" @@ -119,7 +124,7 @@ msgstr "" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:255 +#: src/apprt/gtk/Window.zig:263 msgid "New Tab" msgstr "" @@ -160,7 +165,7 @@ msgid "Terminal Inspector" msgstr "" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 -#: src/apprt/gtk/Window.zig:1024 +#: src/apprt/gtk/Window.zig:1036 msgid "About Ghostty" msgstr "" @@ -170,10 +175,13 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -181,52 +189,67 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -#: src/apprt/gtk/Window.zig:208 +#: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "" -#: src/apprt/gtk/Window.zig:229 +#: src/apprt/gtk/Window.zig:238 msgid "View Open Tabs" msgstr "" -#: src/apprt/gtk/Window.zig:256 +#: src/apprt/gtk/Window.zig:264 msgid "New Split" msgstr "" -#: src/apprt/gtk/Window.zig:319 +#: src/apprt/gtk/Window.zig:327 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" -#: src/apprt/gtk/Window.zig:765 +#: src/apprt/gtk/Window.zig:773 msgid "Reloaded the configuration" msgstr "" -#: src/apprt/gtk/Window.zig:1005 +#: src/apprt/gtk/Window.zig:1017 msgid "Ghostty Developers" msgstr "" @@ -270,6 +293,6 @@ msgstr "" msgid "The currently running process in this split will be terminated." msgstr "" -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "" From 0973abf9f9ebb93fc30496525219187dea1ec23a Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sat, 28 Jun 2025 17:04:21 +0200 Subject: [PATCH 047/114] translations(zh_CN): add lines from #6783 --- po/zh_CN.UTF-8.po | 51 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po index 77be8a351..17a6dc921 100644 --- a/po/zh_CN.UTF-8.po +++ b/po/zh_CN.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-23 16:58+0800\n" +"POT-Creation-Date: 2025-06-28 17:01+0200\n" "PO-Revision-Date: 2025-02-27 09:16+0100\n" "Last-Translator: Leah \n" "Language-Team: Chinese (simplified) \n" @@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title." msgstr "留空以重置至默认标题。" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "取消" @@ -35,10 +36,12 @@ msgid "OK" msgstr "确认" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "配置错误" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -46,12 +49,14 @@ msgstr "" "加载配置时发现了以下错误。请仔细阅读错误信息,并选择忽略或重新加载配置文件。" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "忽略" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "重新加载配置" @@ -90,7 +95,7 @@ msgstr "复制" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "粘贴" @@ -120,7 +125,7 @@ msgstr "标签页" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:255 +#: src/apprt/gtk/Window.zig:263 msgid "New Tab" msgstr "新建标签页" @@ -161,7 +166,7 @@ msgid "Terminal Inspector" msgstr "终端调试器" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 -#: src/apprt/gtk/Window.zig:1024 +#: src/apprt/gtk/Window.zig:1036 msgid "About Ghostty" msgstr "关于 Ghostty" @@ -171,10 +176,13 @@ msgstr "退出" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "剪贴板访问授权" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -182,52 +190,67 @@ msgstr "一个应用正在试图从剪贴板读取内容。剪贴板目前的内 #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "拒绝" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "允许" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "为本分屏记住当前选择" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "本提示将在重载配置后再次出现" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "一个应用正在试图向剪贴板写入内容。剪贴板目前的内容如下:" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "警告:粘贴内容可能不安全" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "将以下内容粘贴至终端内将可能执行有害命令。" -#: src/apprt/gtk/Window.zig:208 +#: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "主菜单" -#: src/apprt/gtk/Window.zig:229 +#: src/apprt/gtk/Window.zig:238 msgid "View Open Tabs" msgstr "浏览标签页" -#: src/apprt/gtk/Window.zig:256 +#: src/apprt/gtk/Window.zig:264 msgid "New Split" msgstr "新建分屏" -#: src/apprt/gtk/Window.zig:319 +#: src/apprt/gtk/Window.zig:327 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。" -#: src/apprt/gtk/Window.zig:765 +#: src/apprt/gtk/Window.zig:773 msgid "Reloaded the configuration" msgstr "已重新加载配置" -#: src/apprt/gtk/Window.zig:1005 +#: src/apprt/gtk/Window.zig:1017 msgid "Ghostty Developers" msgstr "Ghostty 开发团队" @@ -271,6 +294,6 @@ msgstr "标签页内所有运行中的进程将被终止。" msgid "The currently running process in this split will be terminated." msgstr "分屏内正在运行中的进程将被终止。" -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "已复制至剪贴板" From 4fac5f3749e68752323759d08d14b5762623e89c Mon Sep 17 00:00:00 2001 From: -k Date: Sat, 28 Jun 2025 13:08:56 -0400 Subject: [PATCH 048/114] fix: enable `boo` on FreeBSD --- src/cli/boo.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/boo.zig b/src/cli/boo.zig index 7ecbf79fb..47c8ab741 100644 --- a/src/cli/boo.zig +++ b/src/cli/boo.zig @@ -176,7 +176,7 @@ const Boo = struct { pub fn run(gpa: Allocator) !u8 { // Disable on non-desktop systems. switch (builtin.os.tag) { - .windows, .macos, .linux => {}, + .windows, .macos, .linux, .freebsd => {}, else => return 1, } From 84432a7beb01ffdc87812a3f3ddde2dc74475010 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 28 Jun 2025 13:06:36 -0700 Subject: [PATCH 049/114] config: more general purpose backwards compatibility handlers Fixes #7706 We previously had a very specific backwards compatibility handler for handling renamed fields. We always knew that wouldn't scale but I wanted to wait for a real case. Well, #7706 is a real case, so here we are. This commit makes our backwards compatibility handler more general purpose, and makes a special-case handler for renamed fields built on top of this same general purpose system. The new system lets us do a lot more with regards to backwards compatibility. To start, this addresses #7706 by allowing us to handle a removed single enum value of a still-existing field. --- src/cli.zig | 2 + src/cli/args.zig | 190 ++++++++++++++++++++++++++++++------------ src/config/Config.zig | 37 +++++++- 3 files changed, 174 insertions(+), 55 deletions(-) diff --git a/src/cli.zig b/src/cli.zig index 4336501a8..151e6e648 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -2,6 +2,8 @@ const diags = @import("cli/diagnostics.zig"); pub const args = @import("cli/args.zig"); pub const Action = @import("cli/action.zig").Action; +pub const CompatibilityHandler = args.CompatibilityHandler; +pub const compatibilityRenamed = args.compatibilityRenamed; pub const DiagnosticList = diags.DiagnosticList; pub const Diagnostic = diags.Diagnostic; pub const Location = diags.Location; diff --git a/src/cli/args.zig b/src/cli/args.zig index 3c34e17fe..65e72636e 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -40,11 +40,14 @@ pub const Error = error{ /// "DiagnosticList" and any diagnostic messages will be added to that list. /// When diagnostics are present, only allocation errors will be returned. /// -/// If the destination type has a decl "renamed", it must be of type -/// std.StaticStringMap([]const u8) and contains a mapping from the old -/// field name to the new field name. This is used to allow renaming fields -/// while still supporting the old name. If a renamed field is set, parsing -/// will automatically set the new field name. +/// If the destination type has a decl "compatibility", it must be of type +/// std.StaticStringMap(CompatibilityHandler(T)), and it will be used to +/// handle backwards compatibility for fields with the given name. The +/// field name doesn't need to exist (so you can setup compatibility for +/// removed fields). The value is a function that will be called when +/// all other parsing fails for that field. If a field changes such that +/// the old values would NOT error, then the caller should handle that +/// downstream after parsing is done, not through this method. /// /// Note: If the arena is already non-null, then it will be used. In this /// case, in the case of an error some memory might be leaked into the arena. @@ -57,24 +60,6 @@ pub fn parse( const info = @typeInfo(T); assert(info == .@"struct"); - comptime { - // Verify all renamed fields are valid (source does not exist, - // destination does exist). - if (@hasDecl(T, "renamed")) { - for (T.renamed.keys(), T.renamed.values()) |key, value| { - if (@hasField(T, key)) { - @compileLog(key); - @compileError("renamed field source exists"); - } - - if (!@hasField(T, value)) { - @compileLog(value); - @compileError("renamed field destination does not exist"); - } - } - } - } - // Make an arena for all our allocations if we support it. Otherwise, // use an allocator that always fails. If the arena is already set on // the config, then we reuse that. See memory note in parse docs. @@ -148,6 +133,16 @@ pub fn parse( }; parseIntoField(T, arena_alloc, dst, key, value) catch |err| { + // If we get an error parsing a field, then we try to fall + // back to compatibility handlers if able. + if (@hasDecl(T, "compatibility")) { + // If we have a compatibility handler for this key, then + // we call it and see if it handles the error. + if (T.compatibility.get(key)) |handler| { + if (handler(dst, arena_alloc, key, value)) return; + } + } + if (comptime !canTrackDiags(T)) return err; // The error set is dependent on comptime T, so we always add @@ -177,6 +172,58 @@ pub fn parse( } } +/// The function type for a compatibility handler. The compatibility +/// handler is documented in the `parse` function documentation. +/// +/// The function type should return bool if the compatibility was +/// handled, and false otherwise. If false is returned then the +/// naturally occurring error will continue to be processed as if +/// this compatibility handler was not present. +/// +/// Compatibility handlers aren't allowed to return errors because +/// they're generally only called in error cases, so we already have +/// an error message to show users. If there is an error in handling +/// the compatibility, then the handler should return false. +pub fn CompatibilityHandler(comptime T: type) type { + return *const fn ( + dst: *T, + alloc: Allocator, + key: []const u8, + value: ?[]const u8, + ) bool; +} + +/// Convenience function to create a compatibility handler that +/// renames a field from `from` to `to`. +pub fn compatibilityRenamed( + comptime T: type, + comptime to: []const u8, +) CompatibilityHandler(T) { + comptime assert(@hasField(T, to)); + + return (struct { + fn compat( + dst: *T, + alloc: Allocator, + key: []const u8, + value: ?[]const u8, + ) bool { + _ = key; + + parseIntoField(T, alloc, dst, to, value) catch |err| { + log.warn("error parsing renamed field {s}: {}", .{ + to, + err, + }); + + return false; + }; + + return true; + } + }).compat; +} + fn formatValueRequired( comptime T: type, arena_alloc: std.mem.Allocator, @@ -401,16 +448,6 @@ pub fn parseIntoField( } } - // Unknown field, is the field renamed? - if (@hasDecl(T, "renamed")) { - for (T.renamed.keys(), T.renamed.values()) |old, new| { - if (mem.eql(u8, old, key)) { - try parseIntoField(T, alloc, dst, new, value); - return; - } - } - } - return error.InvalidField; } @@ -752,6 +789,75 @@ test "parse: diagnostic location" { } } +test "parse: compatibility handler" { + const testing = std.testing; + + var data: struct { + a: bool = false, + _arena: ?ArenaAllocator = null, + + pub const compatibility: std.StaticStringMap( + CompatibilityHandler(@This()), + ) = .initComptime(&.{ + .{ "a", compat }, + }); + + fn compat( + self: *@This(), + alloc: Allocator, + key: []const u8, + value: ?[]const u8, + ) bool { + _ = alloc; + if (std.mem.eql(u8, key, "a")) { + if (value) |v| { + if (mem.eql(u8, v, "yuh")) { + self.a = true; + return true; + } + } + } + + return false; + } + } = .{}; + defer if (data._arena) |arena| arena.deinit(); + + var iter = try std.process.ArgIteratorGeneral(.{}).init( + testing.allocator, + "--a=yuh", + ); + defer iter.deinit(); + try parse(@TypeOf(data), testing.allocator, &data, &iter); + try testing.expect(data._arena != null); + try testing.expect(data.a); +} + +test "parse: compatibility renamed" { + const testing = std.testing; + + var data: struct { + a: bool = false, + _arena: ?ArenaAllocator = null, + + pub const compatibility: std.StaticStringMap( + CompatibilityHandler(@This()), + ) = .initComptime(&.{ + .{ "old", compatibilityRenamed(@This(), "a") }, + }); + } = .{}; + defer if (data._arena) |arena| arena.deinit(); + + var iter = try std.process.ArgIteratorGeneral(.{}).init( + testing.allocator, + "--old=true", + ); + defer iter.deinit(); + try parse(@TypeOf(data), testing.allocator, &data, &iter); + try testing.expect(data._arena != null); + try testing.expect(data.a); +} + test "parseIntoField: ignore underscore-prefixed fields" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); @@ -1176,24 +1282,6 @@ test "parseIntoField: tagged union missing tag" { ); } -test "parseIntoField: renamed field" { - const testing = std.testing; - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var data: struct { - a: []const u8, - - const renamed = std.StaticStringMap([]const u8).initComptime(&.{ - .{ "old", "a" }, - }); - } = undefined; - - try parseIntoField(@TypeOf(data), alloc, &data, "old", "42"); - try testing.expectEqualStrings("42", data.a); -} - /// An iterator that considers its location to be CLI args. It /// iterates through an underlying iterator and increments a counter /// to track the current CLI arg index. diff --git a/src/config/Config.zig b/src/config/Config.zig index 44089fb57..3cb61fc76 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -46,14 +46,22 @@ const c = @cImport({ @cInclude("unistd.h"); }); -/// Renamed fields, used by cli.parse -pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{ +pub const compatibility = std.StaticStringMap( + cli.CompatibilityHandler(Config), +).initComptime(&.{ // Ghostty 1.1 introduced background-blur support for Linux which // doesn't support a specific radius value. The renaming is to let // one field be used for both platforms (macOS retained the ability // to set a radius). - .{ "background-blur-radius", "background-blur" }, - .{ "adw-toolbar-style", "gtk-toolbar-style" }, + .{ "background-blur-radius", cli.compatibilityRenamed(Config, "background-blur") }, + + // Ghostty 1.2 renamed all our adw options to gtk because we now have + // a hard dependency on libadwaita. + .{ "adw-toolbar-style", cli.compatibilityRenamed(Config, "gtk-toolbar-style") }, + + // Ghostty 1.2 removed the `hidden` value from `gtk-tabs-location` and + // moved it to `window-show-tab-bar`. + .{ "gtk-tabs-location", compatGtkTabsLocation }, }); /// The font families to use. @@ -3792,6 +3800,27 @@ pub fn parseManuallyHook( return true; } +/// parseFieldManuallyFallback is a fallback called only when +/// parsing the field directly failed. It can be used to implement +/// backward compatibility. Since this is only called when parsing +/// fails, it doesn't impact happy-path performance. +fn compatGtkTabsLocation( + self: *Config, + alloc: Allocator, + key: []const u8, + value: ?[]const u8, +) bool { + _ = alloc; + assert(std.mem.eql(u8, key, "gtk-tabs-location")); + + if (std.mem.eql(u8, value orelse "", "hidden")) { + self.@"window-show-tab-bar" = .never; + return true; + } + + return false; +} + /// Create a shallow copy of this config. This will share all the memory /// allocated with the previous config but will have a new arena for /// any changes or new allocations. The config should have `deinit` From 2f978fbdcf583c5f82081eed374fe723de1d10b4 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 29 Jun 2025 00:15:18 +0000 Subject: [PATCH 050/114] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index e85958aaf..43986637f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -103,8 +103,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz", - .hash = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz", + .hash = "N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 4217c17aa..d9f43a766 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX": { + "N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz", - "hash": "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz", + "hash": "sha256-g9o2CIc/TfWYoUS/l/HP5KZECD7qNsdQUlFruaKkVz4=" }, "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "name": "libpng", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 46345871b..26209e778 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -170,11 +170,11 @@ in }; } { - name = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX"; + name = "N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz"; - hash = "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz"; + hash = "sha256-g9o2CIc/TfWYoUS/l/HP5KZECD7qNsdQUlFruaKkVz4="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index b7cb2772f..553b0fb06 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -27,7 +27,7 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5. https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 32bd8bd54..4990f794a 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -67,9 +67,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz", - "dest": "vendor/p/N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX", - "sha256": "0bddcc4b2360c81fae86fccc4044c35ebefcdfd845c97ecd7d3329e0750ab375" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz", + "dest": "vendor/p/N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS", + "sha256": "83da3608873f4df598a144bf97f1cfe4a644083eea36c75052516bb9a2a4573e" }, { "type": "archive", From 5ab7ceb589a9d763cf2e8c20702b259556afbfaf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 28 Jun 2025 19:24:03 -0700 Subject: [PATCH 051/114] config: fix regression where we halted parsing on deprecated field Fix regression from d44a6cde2c7ed59f0f28fad16e6b760d7529ebee where we halted parsing on deprecated fields, which was not the intended behavior. This commit fixes that and adds a test to verify it. --- src/cli/args.zig | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/cli/args.zig b/src/cli/args.zig index 65e72636e..1af74df69 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -132,14 +132,20 @@ pub fn parse( break :value null; }; - parseIntoField(T, arena_alloc, dst, key, value) catch |err| { + parseIntoField(T, arena_alloc, dst, key, value) catch |err| err: { // If we get an error parsing a field, then we try to fall // back to compatibility handlers if able. if (@hasDecl(T, "compatibility")) { // If we have a compatibility handler for this key, then // we call it and see if it handles the error. if (T.compatibility.get(key)) |handler| { - if (handler(dst, arena_alloc, key, value)) return; + if (handler(dst, arena_alloc, key, value)) { + log.info( + "compatibility handler for {s} handled error, you may be using a deprecated field: {}", + .{ key, err }, + ); + break :err; + } } } @@ -838,6 +844,7 @@ test "parse: compatibility renamed" { var data: struct { a: bool = false, + b: bool = false, _arena: ?ArenaAllocator = null, pub const compatibility: std.StaticStringMap( @@ -850,12 +857,13 @@ test "parse: compatibility renamed" { var iter = try std.process.ArgIteratorGeneral(.{}).init( testing.allocator, - "--old=true", + "--old=true --b=true", ); defer iter.deinit(); try parse(@TypeOf(data), testing.allocator, &data, &iter); try testing.expect(data._arena != null); try testing.expect(data.a); + try testing.expect(data.b); } test "parseIntoField: ignore underscore-prefixed fields" { From ef06e3d02c278aa42712d371e8172482bf3487b6 Mon Sep 17 00:00:00 2001 From: Troels Thomsen Date: Sun, 29 Jun 2025 09:15:40 +0200 Subject: [PATCH 052/114] Introduce action for copying into clipboard --- src/Surface.zig | 5 +++++ src/config/Config.zig | 6 ++++++ src/input/Binding.zig | 5 +++++ src/input/command.zig | 10 ++++++++++ 4 files changed, 26 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 286d81383..5acec8c00 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4845,6 +4845,11 @@ fn writeScreenFile( const path = try tmp_dir.dir.realpath(filename, &path_buf); switch (write_action) { + .copy => { + const pathZ = try self.alloc.dupeZ(u8, path); + defer self.alloc.free(pathZ); + try self.rt_surface.setClipboardString(pathZ, .standard, false); + }, .open => try internal_os.open(self.alloc, .text, path), .paste => self.io.queueMessage(try termio.Message.writeReq( self.alloc, diff --git a/src/config/Config.zig b/src/config/Config.zig index 3cb61fc76..14ab5219d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5032,6 +5032,12 @@ pub const Keybinds = struct { .{ .reset_font_size = {} }, ); + try self.set.put( + alloc, + .{ .key = .{ .unicode = 'j' }, .mods = .{ .shift = true, .ctrl = true, .super = true } }, + .{ .write_screen_file = .copy }, + ); + try self.set.put( alloc, .{ .key = .{ .unicode = 'j' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index cccf12ac4..7cdb8047c 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -379,6 +379,10 @@ pub const Action = union(enum) { /// /// Valid actions are: /// + /// - `copy` + /// + /// Copy the file path into the clipboard. + /// /// - `paste` /// /// Paste the file path into the terminal. @@ -813,6 +817,7 @@ pub const Action = union(enum) { }; pub const WriteScreenAction = enum { + copy, paste, open, }; diff --git a/src/input/command.zig b/src/input/command.zig index 8ae48eda1..693d5c8d4 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -204,6 +204,11 @@ fn actionCommands(action: Action.Key) []const Command { }}, .write_screen_file => comptime &.{ + .{ + .action = .{ .write_screen_file = .copy }, + .title = "Copy Screen to Temporary File and Copy Path", + .description = "Copy the screen contents to a temporary file and copy the path to the clipboard.", + }, .{ .action = .{ .write_screen_file = .paste }, .title = "Copy Screen to Temporary File and Paste Path", @@ -217,6 +222,11 @@ fn actionCommands(action: Action.Key) []const Command { }, .write_selection_file => comptime &.{ + .{ + .action = .{ .write_selection_file = .copy }, + .title = "Copy Selection to Temporary File and Copy Path", + .description = "Copy the selection contents to a temporary file and copy the path to the clipboard.", + }, .{ .action = .{ .write_selection_file = .paste }, .title = "Copy Selection to Temporary File and Paste Path", From ad5ab92333b648333fe9208dca481092e9b663ed Mon Sep 17 00:00:00 2001 From: RME Date: Sun, 29 Jun 2025 15:42:18 +0200 Subject: [PATCH 053/114] Update po/ko_KR.UTF-8.po Co-authored-by: Hojin You --- po/ko_KR.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po index 4b87a9d35..4fccb14f4 100644 --- a/po/ko_KR.UTF-8.po +++ b/po/ko_KR.UTF-8.po @@ -145,7 +145,7 @@ msgstr "설정 열기" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Terminal Inspector" -msgstr "터미널 조사 도구" +msgstr "터미널 인스펙터" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 #: src/apprt/gtk/Window.zig:960 From 7f0778bcf28e3ba773a4f2a8204be8fe9d0cda78 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Sun, 29 Jun 2025 15:32:17 +0100 Subject: [PATCH 054/114] termio: indicate support for OSC 52 in primary DA report This is an extension agreed upon by modern terminals to indicate that they support copying to the clipboard with XTerm's OSC 52 sequence. It is only reported when writing to the clipboard is actually allowed. --- src/termio/Termio.zig | 3 +++ src/termio/stream_handler.zig | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 865a2df86..8aaa87011 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -168,6 +168,7 @@ pub const DerivedConfig = struct { foreground: configpkg.Config.Color, background: configpkg.Config.Color, osc_color_report_format: configpkg.Config.OSCColorReportFormat, + clipboard_write: configpkg.ClipboardAccess, enquiry_response: []const u8, pub fn init( @@ -188,6 +189,7 @@ pub const DerivedConfig = struct { .foreground = config.foreground, .background = config.background, .osc_color_report_format = config.@"osc-color-report-format", + .clipboard_write = config.@"clipboard-write", .enquiry_response = try alloc.dupe(u8, config.@"enquiry-response"), // This has to be last so that we copy AFTER the arena allocations @@ -278,6 +280,7 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { .size = &self.size, .terminal = &self.terminal, .osc_color_report_format = opts.config.osc_color_report_format, + .clipboard_write = opts.config.clipboard_write, .enquiry_response = opts.config.enquiry_response, .default_foreground_color = opts.config.foreground.toTerminalRGB(), .default_background_color = opts.config.background.toTerminalRGB(), diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 90add84ae..1b4fdd3aa 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -74,6 +74,9 @@ pub const StreamHandler = struct { /// The color reporting format for OSC requests. osc_color_report_format: configpkg.Config.OSCColorReportFormat, + /// The clipboard write access configuration. + clipboard_write: configpkg.ClipboardAccess, + //--------------------------------------------------------------- // Internal state @@ -112,6 +115,7 @@ pub const StreamHandler = struct { /// Change the configuration for this handler. pub fn changeConfig(self: *StreamHandler, config: *termio.DerivedConfig) void { self.osc_color_report_format = config.osc_color_report_format; + self.clipboard_write = config.clipboard_write; self.enquiry_response = config.enquiry_response; self.default_foreground_color = config.foreground.toTerminalRGB(); self.default_background_color = config.background.toTerminalRGB(); @@ -723,7 +727,13 @@ pub const StreamHandler = struct { // a 420 because we don't support DCS sequences. switch (req) { .primary => self.messageWriter(.{ - .write_stable = "\x1B[?62;22c", + // 62 = Level 2 conformance + // 22 = Color text + // 52 = Clipboard access + .write_stable = if (self.clipboard_write != .deny) + "\x1B[?62;22;52c" + else + "\x1B[?62;22c", }), .secondary => self.messageWriter(.{ From 14ba7effcd6065ba38abb6140127804da1b14538 Mon Sep 17 00:00:00 2001 From: Alan Moyano Date: Tue, 20 May 2025 10:45:44 -0300 Subject: [PATCH 055/114] Fixing issues and making the translation more similar to the es_BO version --- po/es_AR.UTF-8.po | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/po/es_AR.UTF-8.po b/po/es_AR.UTF-8.po index 0cb99f6be..10aa7bbfe 100644 --- a/po/es_AR.UTF-8.po +++ b/po/es_AR.UTF-8.po @@ -23,7 +23,7 @@ msgstr "Cambiar el título de la terminal" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 msgid "Leave blank to restore the default title." -msgstr "Deja en blanco para restaurar el título predeterminado." +msgstr "Dejar en blanco para restaurar el título predeterminado." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 #: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 @@ -32,7 +32,7 @@ msgstr "Cancelar" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 msgid "OK" -msgstr Aceptar" +msgstr "Aceptar" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 msgid "Configuration Errors" @@ -60,25 +60,25 @@ msgstr "Recargar configuración" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 msgid "Split Up" -msgstr "Dividir hacia arriba" +msgstr "Dividir arriba" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 msgid "Split Down" -msgstr "Dividir hacia abajo" +msgstr "Dividir abajo" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 msgid "Split Left" -msgstr "Dividir hacia la izquierda" +msgstr "Dividir a la izquierda" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 msgid "Split Right" -msgstr "Dividir hacia la derecha" +msgstr "Dividir a la derecha" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" @@ -262,19 +262,19 @@ msgstr "¿Cerrar pestaña?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" -msgstr "msgstr "¿Cerrar división?" +msgstr "¿Cerrar división?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." -msgstr "Todas las sesiones de la terminal serán terminadas." +msgstr "Todas las sesiones de terminal serán terminadas." #: src/apprt/gtk/CloseDialog.zig:97 msgid "All terminal sessions in this window will be terminated." -msgstr "Todas las sesiones de la terminal en esta ventana serán terminadas." +msgstr "Todas las sesiones de terminal en esta ventana serán terminadas." #: src/apprt/gtk/CloseDialog.zig:98 msgid "All terminal sessions in this tab will be terminated." -msgstr "Todas las sesiones de la terminal en esta pestaña serán terminadas." +msgstr "Todas las sesiones de terminal en esta pestaña serán terminadas." #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." From ff599b5cf7f399d0211bfaa8a6dcafa3588a6b42 Mon Sep 17 00:00:00 2001 From: Alan Moyano Date: Tue, 20 May 2025 10:58:11 -0300 Subject: [PATCH 056/114] Improving Argentinian voseo --- po/es_AR.UTF-8.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/po/es_AR.UTF-8.po b/po/es_AR.UTF-8.po index 10aa7bbfe..b05f2c678 100644 --- a/po/es_AR.UTF-8.po +++ b/po/es_AR.UTF-8.po @@ -43,8 +43,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Se encontraron uno o más errores de configuración. Por favor revisa los " -"errores a continuación, y recarga tu configuración o ignora estos errores." +"Se encontraron uno o más errores de configuración. Por favor revisá los " +"errores a continuación, y recargá tu configuración o ignorá estos errores." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -229,7 +229,7 @@ msgstr "Nueva división" msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" -"⚠️ Está ejecutando una versión de depuración de Ghostty. El rendimiento no " +"⚠️ Estás ejecutando una versión de depuración de Ghostty. El rendimiento no " "será óptimo." #: src/apprt/gtk/Window.zig:765 From 046f21f2dc79820f682de00d96278f150d9698df Mon Sep 17 00:00:00 2001 From: Alan Moyano Date: Sat, 28 Jun 2025 16:14:36 +0000 Subject: [PATCH 057/114] Adding email address --- po/es_AR.UTF-8.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/po/es_AR.UTF-8.po b/po/es_AR.UTF-8.po index b05f2c678..3cd0625c8 100644 --- a/po/es_AR.UTF-8.po +++ b/po/es_AR.UTF-8.po @@ -1,7 +1,7 @@ # Spanish translations for com.mitchellh.ghostty package. # Copyright (C) 2025 Mitchell Hashimoto # This file is distributed under the same license as the com.mitchellh.ghostty package. -# Alan Moyano , 2025. +# Alan Moyano , 2025. # msgid "" msgstr "" @@ -9,7 +9,7 @@ msgstr "" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-04-23 16:58+0800\n" "PO-Revision-Date: 2025-05-19 20:17-0300\n" -"Last-Translator: Alan Moyano \n" +"Last-Translator: Alan Moyano \n" "Language-Team: Argentinian \n" "Language: es_AR\n" "MIME-Version: 1.0\n" From 5da461dc35a74079b76c2374338bc7b1e089c366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aindri=C3=BA=20Mac=20Giolla=20Eoin?= Date: Sun, 29 Jun 2025 21:20:20 +0100 Subject: [PATCH 058/114] Corrected 2 strings for better readability and consistency --- po/ga_IE.UTF-8.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po index 3f7d6b068..686d22d76 100644 --- a/po/ga_IE.UTF-8.po +++ b/po/ga_IE.UTF-8.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-04-23 16:58+0800\n" -"PO-Revision-Date: 2025-06-24 12:42+0100\n" +"PO-Revision-Date: 2025-06-29 21:15+0100\n" "Last-Translator: Aindriú Mac Giolla Eoin \n" "Language-Team: Irish \n" "Language: ga\n" @@ -16,7 +16,7 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n" -"X-Generator: Poedit 3.4.2\n" +"X-Generator: Poedit 3.4.4\n" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" @@ -212,7 +212,7 @@ msgid "" "commands may be executed." msgstr "" "D’fhéadfadh sé a bheith contúirteach an téacs seo a ghreamú isteach sa " -"chríochfort mar is cosúil go bhféadfaí roinnt orduithe a fhorghníomhú." +"teirminéal, toisc go d'fhéadfadh roinnt orduithe a fhorghníomhú." #: src/apprt/gtk/Window.zig:208 msgid "Main Menu" @@ -234,7 +234,7 @@ msgstr "" #: src/apprt/gtk/Window.zig:765 msgid "Reloaded the configuration" -msgstr "Athluchtaigh an chumraíocht" +msgstr "Tá an chumraíocht athlódáilte" #: src/apprt/gtk/Window.zig:1005 msgid "Ghostty Developers" From d0e12cc0821f188820675548d04c1fec1b52a93c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 29 Jun 2025 15:06:18 -0700 Subject: [PATCH 059/114] update libxev to workaround the io_uring regression in Linux 6.15.4 Fixes #7724 Background at the end of the commit message. The fix in libxev is described in the PR and commit we pin to here, but basically we swap read for poll for eventfd/timerfd. From Jens Axboe on X: > This will fix it: https://pastebin.com/n7JSZWpW which makes me suspicious > that it's an S_IFREG check somewhere else, as anon inodes are now listed as > regular files. Has potentially pretty broad implications... > I think I can already answer why that breaks things - io_uring checks if > this is a regular file, and if it is, it doesn't do short reads. Short > reads on regular files (or a bdev) will cause application issues, as > basically nobody expects them. > Now we have what acts like a char dev, but where io_uring will retry IO > because the application asked for more data than what was delivered. This > will cause the weird slowdowns as data isn't delivered as soon as it's > available. --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 43986637f..51e2e4538 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,8 +8,8 @@ .libxev = .{ // mitchellh/libxev - .url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz", - .hash = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3", + .url = "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz", + .hash = "libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw", .lazy = true, }, .vaxis = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index d9f43a766..1d95ed93a 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -64,10 +64,10 @@ "url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz", "hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo=" }, - "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3": { + "libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw": { "name": "libxev", - "url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz", - "hash": "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU=" + "url": "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz", + "hash": "sha256-/CSKSuZZfn0aIQlVZ0O8ch5O4gCajYBTTuoetRdo0n4=" }, "N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": { "name": "libxml2", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 26209e778..fffc639b4 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -186,11 +186,11 @@ in }; } { - name = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3"; + name = "libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw"; path = fetchZigArtifact { name = "libxev"; - url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz"; - hash = "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU="; + url = "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz"; + hash = "sha256-/CSKSuZZfn0aIQlVZ0O8ch5O4gCajYBTTuoetRdo0n4="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 553b0fb06..d032711e5 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -28,7 +28,7 @@ https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90 https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz -https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz +https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 4990f794a..81024bb26 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -79,9 +79,9 @@ }, { "type": "archive", - "url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz", - "dest": "vendor/p/libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3", - "sha256": "570141c83a29b6a88de5492894543b4db461c0c11145e0fd12bda98f14c26085" + "url": "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz", + "dest": "vendor/p/libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw", + "sha256": "fc248a4ae6597e7d1a2109556743bc721e4ee2009a8d80534eea1eb51768d27e" }, { "type": "archive", From a82223259acb4213af721844c8d996860906646c Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sun, 29 Jun 2025 18:54:40 -0400 Subject: [PATCH 060/114] terminal: introduce testWriteSemanticString This test-only function wraps testWriteString with semantic prompt marking. This replaces the manual, row-based semantic_prompt field manipulation we were doing in all of our prompt-related test setups. This function's heuristics are a little complex because it wraps testWriteString as a "black box"; we don't benefit from that function's own line-based logic to know which rows need to be updated with the semantic prompt flag. We need to infer them externally instead. I considered adding an options argument to testWriteString that would allow passing e.g. a semantic_prompt prompt. Given that it's called from 200+ places, that would involve a lot of unrelated changes, but it remains an "option" (ha!) if there's value there for other cases. I also have plans that move us from row-based to cell-based semantic tracking, where the current semantic type is tracked by the cursor. In that implementation, testWriteString can update the written cells directly, and testWriteSemanticString just helps manage the cursor's state. Introducing testWriteSemanticString here and now therefore helps bridge us to that world while maintaining test consistency. --- src/terminal/Screen.zig | 309 +++++++++++++--------------------------- 1 file changed, 96 insertions(+), 213 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 5b772ab84..079df37db 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -3068,6 +3068,29 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void { } } +/// Write text that's marked as a semantic prompt. +fn testWriteSemanticString(self: *Screen, text: []const u8, semantic_prompt: Row.SemanticPrompt) !void { + // Determine the first row using the cursor position. If we know that our + // first write is going to start on the next line because of a pending + // wrap, we'll proactively start there. + const start_y = if (self.cursor.pending_wrap) self.cursor.y + 1 else self.cursor.y; + + try self.testWriteString(text); + + // Determine the last row that we actually wrote by inspecting the cursor's + // position. If we're in the first column, we haven't actually written any + // characters to it, so we end at the preceding row instead. + const end_y = if (self.cursor.x > 0) self.cursor.y else self.cursor.y - 1; + + // Mark the full range of written rows with our semantic prompt. + var y = start_y; + while (y <= end_y) { + const pin = self.pages.pin(.{ .active = .{ .y = y } }).?; + pin.rowAndCell().row.semantic_prompt = semantic_prompt; + y += 1; + } +} + test "Screen read and write" { const testing = std.testing; const alloc = testing.allocator; @@ -3686,16 +3709,11 @@ test "Screen: clearPrompt" { var s = try init(alloc, 5, 3, 0); defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); // Set one of the rows to be a prompt - { - s.cursorAbsolute(0, 1); - s.cursor.page_row.semantic_prompt = .prompt; - s.cursorAbsolute(0, 2); - s.cursor.page_row.semantic_prompt = .input; - } + try s.testWriteSemanticString("1ABCD\n", .unknown); + try s.testWriteSemanticString("2EFGH\n", .prompt); + try s.testWriteSemanticString("3IJKL", .input); s.clearPrompt(); @@ -3712,18 +3730,12 @@ test "Screen: clearPrompt continuation" { var s = try init(alloc, 5, 4, 0); defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL\n4MNOP"; - try s.testWriteString(str); // Set one of the rows to be a prompt followed by a continuation row - { - s.cursorAbsolute(0, 1); - s.cursor.page_row.semantic_prompt = .prompt; - s.cursorAbsolute(0, 2); - s.cursor.page_row.semantic_prompt = .prompt_continuation; - s.cursorAbsolute(0, 3); - s.cursor.page_row.semantic_prompt = .input; - } + try s.testWriteSemanticString("1ABCD\n", .unknown); + try s.testWriteSemanticString("2EFGH\n", .prompt); + try s.testWriteSemanticString("3IJKL\n", .prompt_continuation); + try s.testWriteSemanticString("4MNOP", .input); s.clearPrompt(); @@ -3734,22 +3746,17 @@ test "Screen: clearPrompt continuation" { } } -test "Screen: clearPrompt consecutive prompts" { +test "Screen: clearPrompt consecutive inputs" { const testing = std.testing; const alloc = testing.allocator; var s = try init(alloc, 5, 3, 0); defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); - // Set both rows to be prompts - { - s.cursorAbsolute(0, 1); - s.cursor.page_row.semantic_prompt = .input; - s.cursorAbsolute(0, 2); - s.cursor.page_row.semantic_prompt = .input; - } + // Set both rows to be inputs + try s.testWriteSemanticString("1ABCD\n", .unknown); + try s.testWriteSemanticString("2EFGH\n", .input); + try s.testWriteSemanticString("3IJKL", .input); s.clearPrompt(); @@ -6057,26 +6064,24 @@ test "Screen: resize more cols no reflow preserves semantic prompt" { var s = try init(alloc, 5, 3, 0); defer s.deinit(); - const str = "1ABCD\n2EFGH\n3IJKL"; - try s.testWriteString(str); // Set one of the rows to be a prompt - { - s.cursorAbsolute(0, 1); - s.cursor.page_row.semantic_prompt = .prompt; - } + try s.testWriteSemanticString("1ABCD\n", .unknown); + try s.testWriteSemanticString("2EFGH\n", .prompt); + try s.testWriteSemanticString("3IJKL", .unknown); try s.resize(10, 3); + const expected = "1ABCD\n2EFGH\n3IJKL"; { const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); + try testing.expectEqualStrings(expected, contents); } { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); defer alloc.free(contents); - try testing.expectEqualStrings(str, contents); + try testing.expectEqualStrings(expected, contents); } // Our one row should still be a semantic prompt, the others should not. @@ -7507,7 +7512,9 @@ test "Screen: selectLine semantic prompt boundary" { var s = try init(alloc, 5, 10, 0); defer s.deinit(); - try s.testWriteString("ABCDE\nA > "); + try s.testWriteSemanticString("ABCDE\n", .unknown); + try s.testWriteSemanticString("A ", .prompt); + try s.testWriteSemanticString("> ", .unknown); { const contents = try s.dumpStringAlloc(alloc, .{ .screen = .{} }); @@ -7515,12 +7522,6 @@ test "Screen: selectLine semantic prompt boundary" { try testing.expectEqualStrings("ABCDE\nA \n> ", contents); } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 1 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - // Selecting output stops at the prompt even if soft-wrapped { var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{ @@ -7905,55 +7906,23 @@ test "Screen: selectOutput" { // zig fmt: off { // line number: - try s.testWriteString("output1\n"); // 0 - try s.testWriteString("output1\n"); // 1 - try s.testWriteString("prompt2\n"); // 2 - try s.testWriteString("input2\n"); // 3 - try s.testWriteString("output2output2output2output2\n"); // 4, 5, 6 due to overflow - try s.testWriteString("output2\n"); // 7 - try s.testWriteString("$ input3\n"); // 8 - try s.testWriteString("output3\n"); // 9 - try s.testWriteString("output3\n"); // 10 - try s.testWriteString("output3"); // 11 + try s.testWriteSemanticString("output1\n", .command); // 0 + try s.testWriteSemanticString("output1\n", .command); // 1 + try s.testWriteSemanticString("prompt2\n", .prompt); // 2 + try s.testWriteSemanticString("input2\n", .input); // 3 + try s.testWriteSemanticString( // + "output2output2output2output2\n", // 4, 5, 6 due to overflow + .command, // + ); // + try s.testWriteSemanticString("output2\n", .command); // 7 + try s.testWriteSemanticString("$ ", .prompt); // 8 prompt + try s.testWriteSemanticString("input3\n", .input); // 8 input + try s.testWriteSemanticString("output3\n", .command); // 9 + try s.testWriteSemanticString("output3\n", .command); // 10 + try s.testWriteSemanticString("output3", .command); // 11 } // zig fmt: on - { - const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 4 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 5 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 8 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 9 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - // No start marker, should select from the beginning { var sel = s.selectOutput(s.pages.pin(.{ .active = .{ @@ -8006,19 +7975,10 @@ test "Screen: selectOutput" { { s.deinit(); s = try init(alloc, 10, 5, 0); - try s.testWriteString("$ input1\n"); - try s.testWriteString("output1\n"); - try s.testWriteString("prompt2\n"); - { - const pin = s.pages.pin(.{ .screen = .{ .y = 0 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 1 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } + try s.testWriteSemanticString("$ ", .prompt); + try s.testWriteSemanticString("input1\n", .input); + try s.testWriteSemanticString("output1\n", .command); + try s.testWriteSemanticString("prompt2\n", .prompt); try testing.expect(s.selectOutput(s.pages.pin(.{ .active = .{ .x = 2, .y = 0, @@ -8035,46 +7995,21 @@ test "Screen: selectPrompt basics" { // zig fmt: off { - // line number: - try s.testWriteString("output1\n"); // 0 - try s.testWriteString("output1\n"); // 1 - try s.testWriteString("prompt2\n"); // 2 - try s.testWriteString("input2\n"); // 3 - try s.testWriteString("output2\n"); // 4 - try s.testWriteString("output2\n"); // 5 - try s.testWriteString("$ input3\n"); // 6 - try s.testWriteString("output3\n"); // 7 - try s.testWriteString("output3\n"); // 8 - try s.testWriteString("output3"); // 9 + // line number: + try s.testWriteSemanticString("output1\n", .command); // 0 + try s.testWriteSemanticString("output1\n", .command); // 1 + try s.testWriteSemanticString("prompt2\n", .prompt); // 2 + try s.testWriteSemanticString("input2\n", .input); // 3 + try s.testWriteSemanticString("output2\n", .command); // 4 + try s.testWriteSemanticString("output2\n", .command); // 5 + try s.testWriteSemanticString("$ ", .prompt); // 6 prompt + try s.testWriteSemanticString("input3\n", .input); // 6 input + try s.testWriteSemanticString("output3\n", .command); // 7 + try s.testWriteSemanticString("output3\n", .command); // 8 + try s.testWriteSemanticString("output3", .command); // 9 } // zig fmt: on - { - const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 4 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 7 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - // Not at a prompt { const sel = s.selectPrompt(s.pages.pin(.{ .active = .{ @@ -8135,30 +8070,14 @@ test "Screen: selectPrompt prompt at start" { // zig fmt: off { - // line number: - try s.testWriteString("prompt1\n"); // 0 - try s.testWriteString("input1\n"); // 1 - try s.testWriteString("output2\n"); // 2 - try s.testWriteString("output2\n"); // 3 + // line number: + try s.testWriteSemanticString("prompt1\n", .prompt); // 0 + try s.testWriteSemanticString("input1\n", .input); // 1 + try s.testWriteSemanticString("output2\n", .command); // 2 + try s.testWriteSemanticString("output2\n", .command); // 3 } // zig fmt: on - { - const pin = s.pages.pin(.{ .screen = .{ .y = 0 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 1 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - // Not at a prompt { const sel = s.selectPrompt(s.pages.pin(.{ .active = .{ @@ -8195,25 +8114,14 @@ test "Screen: selectPrompt prompt at end" { // zig fmt: off { - // line number: - try s.testWriteString("output2\n"); // 0 - try s.testWriteString("output2\n"); // 1 - try s.testWriteString("prompt1\n"); // 2 - try s.testWriteString("input1\n"); // 3 + // line number: + try s.testWriteSemanticString("output2\n", .command); // 0 + try s.testWriteSemanticString("output2\n", .command); // 1 + try s.testWriteSemanticString("prompt1\n", .prompt); // 2 + try s.testWriteSemanticString("input1\n", .input); // 3 } // zig fmt: on - { - const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - // Not at a prompt { const sel = s.selectPrompt(s.pages.pin(.{ .active = .{ @@ -8250,46 +8158,21 @@ test "Screen: promptPath" { // zig fmt: off { - // line number: - try s.testWriteString("output1\n"); // 0 - try s.testWriteString("output1\n"); // 1 - try s.testWriteString("prompt2\n"); // 2 - try s.testWriteString("input2\n"); // 3 - try s.testWriteString("output2\n"); // 4 - try s.testWriteString("output2\n"); // 5 - try s.testWriteString("$ input3\n"); // 6 - try s.testWriteString("output3\n"); // 7 - try s.testWriteString("output3\n"); // 8 - try s.testWriteString("output3"); // 9 + // line number: + try s.testWriteSemanticString("output1\n", .command); // 0 + try s.testWriteSemanticString("output1\n", .command); // 1 + try s.testWriteSemanticString("prompt2\n", .prompt); // 2 + try s.testWriteSemanticString("input2\n", .input); // 3 + try s.testWriteSemanticString("output2\n", .command); // 4 + try s.testWriteSemanticString("output2\n", .command); // 5 + try s.testWriteSemanticString("$ ", .prompt); // 6 prompt + try s.testWriteSemanticString("input3\n", .input); // 6 input + try s.testWriteSemanticString("output3\n", .command); // 7 + try s.testWriteSemanticString("output3\n", .command); // 8 + try s.testWriteSemanticString("output3", .command); // 9 } // zig fmt: on - { - const pin = s.pages.pin(.{ .screen = .{ .y = 2 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .prompt; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 3 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 4 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .input; - } - { - const pin = s.pages.pin(.{ .screen = .{ .y = 7 } }).?; - const row = pin.rowAndCell().row; - row.semantic_prompt = .command; - } - // From is not in the prompt { const path = s.promptPath( From e25029eff620f6f80272302caf953d530b3dd522 Mon Sep 17 00:00:00 2001 From: RME Date: Mon, 30 Jun 2025 15:00:15 +0200 Subject: [PATCH 061/114] add ko_KR i18n to CODEOWNERS --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 343e1dcc1..54600d1ad 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -177,6 +177,7 @@ /po/tr_TR.UTF-8.po @ghostty-org/tr_TR /po/uk_UA.UTF-8.po @ghostty-org/uk_UA /po/zh_CN.UTF-8.po @ghostty-org/zh_CN +/po/ko_KR.UTF-8.po @ghostty-org/ko_KR # Packaging - Snap /snap/ @ghostty-org/snap From 6484df913435377ef9aec6b9519d639af57a4ab4 Mon Sep 17 00:00:00 2001 From: RME Date: Mon, 30 Jun 2025 15:01:44 +0200 Subject: [PATCH 062/114] update debug build string, line 251 --- po/ko_KR.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po index 4fccb14f4..be7fd2502 100644 --- a/po/ko_KR.UTF-8.po +++ b/po/ko_KR.UTF-8.po @@ -248,7 +248,7 @@ msgstr "열린 탭 보기" #: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "⚠️ 디버그 빌드 실행 중! 성능이 저하됩니다." +msgstr "⚠️ Ghostty는 디버그 빌드 실행 중! 성능이 저하됩니다." #: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" From 2850c3b58a30c3f4716695dd1108cbb78b9115a3 Mon Sep 17 00:00:00 2001 From: Damyan Bogoev Date: Mon, 30 Jun 2025 16:51:11 +0300 Subject: [PATCH 063/114] Adding Bulgarian localization. --- po/bg_BG.UTF-8.po | 275 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 2 files changed, 276 insertions(+) create mode 100644 po/bg_BG.UTF-8.po diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po new file mode 100644 index 000000000..e240fd6e7 --- /dev/null +++ b/po/bg_BG.UTF-8.po @@ -0,0 +1,275 @@ +# Bulgarian translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Damyan Bogoev , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-04-23 16:58+0800\n" +"PO-Revision-Date: 2025-05-19 11:34+0300\n" +"Last-Translator: Damyan Bogoev \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Промяна на заглавието на терминала" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Оставете празно за възстановяване на заглавието по подразбиране." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Отказ" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "ОК" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Грешки в конфигурацията" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors " +"below, and either reload your configuration or ignore these errors." +msgstr "Открити са една или повече грешки в конфигурацията. Моля, прегледайте грешките по-долу и или презаредете конфигурацията си, или ги игнорирайте." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Игнорирай" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +msgid "Reload Configuration" +msgstr "Презареди на конфигурацията" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "Раздели нагоре" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "Раздели надолу" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "Раздели наляво" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "Раздели надясно" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "Изпълнение на команда…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "Копирай" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +msgid "Paste" +msgstr "Постави" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "Изчисти" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "Нулирай" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "Раздели" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "Промяна на заглавие…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Раздел" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:255 +msgid "New Tab" +msgstr "Нов раздел" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "Затвори раздел" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Прозорец" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "Нов прозорец" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "Затвори прозорец" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Конфигурация" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Open Configuration" +msgstr "Отвори на конфигурацията" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "Командна палитра" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Terminal Inspector" +msgstr "Инспектор на терминала" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1024 +msgid "About Ghostty" +msgstr "За Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 +msgid "Quit" +msgstr "Изход" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Разрешаване на достъп до клипборда" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "Приложение се опитва да чете от клипборда. Текущото съдържание на клипборда е показано по-долу." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Откажи" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Позволи" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "Приложение се опитва да запише в клипборда. Текущото съдържание на клипборда е показано по-долу." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Предупреждение: Потенциално опасно поставяне" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "Поставянето на този текст в терминала може да е опасно, тъй като изглежда, че може да бъдат изпълнени някои команди." + +#: src/apprt/gtk/Window.zig:208 +msgid "Main Menu" +msgstr "Главно меню" + +#: src/apprt/gtk/Window.zig:229 +msgid "View Open Tabs" +msgstr "Преглед на отворените раздели" + +#: src/apprt/gtk/Window.zig:256 +msgid "New Split" +msgstr "Ново разделяне" + +#: src/apprt/gtk/Window.zig:319 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ Използвате дебъг версия на Ghostty! Производителността ще бъде намалена." + +#: src/apprt/gtk/Window.zig:765 +msgid "Reloaded the configuration" +msgstr "Конфигурацията е презаредена" + +#: src/apprt/gtk/Window.zig:1005 +msgid "Ghostty Developers" +msgstr "Разработчици на Ghostty" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Инспектор на терминала" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Затвори" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Изход от Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Затваряне на прозореца?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Затваряне на раздела?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Затваряне на разделянето?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Всички терминални сесии ще бъдат прекратени." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Всички терминални сесии в този прозорец ще бъдат прекратени." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Всички терминални сесии в този раздел ще бъдат прекратени." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "Текущият процес в това разделяне ще бъде прекратен." + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Копирано в клипборда" \ No newline at end of file diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 6981d55a0..cc157611c 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -46,6 +46,7 @@ pub const locales = [_][:0]const u8{ "es_AR.UTF-8", "pt_BR.UTF-8", "ca_ES.UTF-8", + "bg_BG.UTF-8", "ga_IE.UTF-8", }; From beb961fb809ce0bb7dbb8c4ae243b19502742ed0 Mon Sep 17 00:00:00 2001 From: Daniel Patterson Date: Sun, 26 Jan 2025 00:30:18 +0000 Subject: [PATCH 064/114] Introduce `font-shaping-break` config option --- src/config.zig | 1 + src/config/Config.zig | 31 +++++ src/font/shaper/coretext.zig | 231 ++++++++++++++++++++++++++------- src/font/shaper/harfbuzz.zig | 226 +++++++++++++++++++++++++------- src/font/shaper/noop.zig | 3 + src/font/shaper/run.zig | 61 +++++---- src/font/shaper/web_canvas.zig | 3 + 7 files changed, 436 insertions(+), 120 deletions(-) diff --git a/src/config.zig b/src/config.zig index 7f390fb08..ac38eb89c 100644 --- a/src/config.zig +++ b/src/config.zig @@ -20,6 +20,7 @@ pub const ConfirmCloseSurface = Config.ConfirmCloseSurface; pub const CopyOnSelect = Config.CopyOnSelect; pub const CustomShaderAnimation = Config.CustomShaderAnimation; pub const FontSyntheticStyle = Config.FontSyntheticStyle; +pub const FontShapingBreak = Config.FontShapingBreak; pub const FontStyle = Config.FontStyle; pub const FreetypeLoadFlags = Config.FreetypeLoadFlags; pub const Keybinds = Config.Keybinds; diff --git a/src/config/Config.zig b/src/config/Config.zig index 14ab5219d..ef8f48ee9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -270,6 +270,32 @@ pub const compatibility = std.StaticStringMap( /// This is currently only supported on macOS. @"font-thicken-strength": u8 = 255, +/// Locations to break font shaping into multiple runs. +/// +/// A "run" is a contiguous segment of text that is shaped together. "Shaping" +/// is the process of converting text (codepoints) into glyphs (renderable +/// characters). This is how ligatures are formed, among other things. +/// For example, if a coding font turns "!=" into a single glyph, then it +/// must see "!" and "=" next to each other in a single run. When a run +/// is broken, the text is shaped separately. To continue our example, if +/// "!" is at the end of one run and "=" is at the start of the next run, +/// then the ligature will not be formed. +/// +/// Ghostty breaks runs at certain points to improve readability or usability. +/// For example, Ghostty by default will break runs under the cursor so that +/// text editing can see the individual characters rather than a ligature. +/// This configuration lets you configure this behavior. +/// +/// Combine values with a comma to set multiple options. Prefix an +/// option with "no-" to disable it. Enabling and disabling options +/// can be done at the same time. +/// +/// Available options: +/// +/// * `cursor` - Break runs under the cursor. +/// +@"font-shaping-break": FontShapingBreak = .{}, + /// What color space to use when performing alpha blending. /// /// This affects the appearance of text and of any images with transparency. @@ -6214,6 +6240,11 @@ pub const FontSyntheticStyle = packed struct { @"bold-italic": bool = true, }; +/// See "font-shaping-break" for documentation +pub const FontShapingBreak = packed struct { + cursor: bool = true, +}; + /// See "link" for documentation. pub const RepeatableLink = struct { const Self = @This(); diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 8e2c45c69..654af02d9 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -7,6 +7,7 @@ const trace = @import("tracy").trace; const font = @import("../main.zig"); const os = @import("../../os/main.zig"); const terminal = @import("../../terminal/main.zig"); +const config = @import("../../config.zig"); const Feature = font.shape.Feature; const FeatureList = font.shape.FeatureList; const default_features = font.shape.default_features; @@ -293,6 +294,7 @@ pub const Shaper = struct { row: terminal.Pin, selection: ?terminal.Selection, cursor_x: ?usize, + break_config: config.FontShapingBreak, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, @@ -301,6 +303,7 @@ pub const Shaper = struct { .row = row, .selection = selection, .cursor_x = cursor_x, + .break_config = break_config, }; } @@ -600,6 +603,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -619,6 +623,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -639,6 +644,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -660,6 +666,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -707,6 +714,7 @@ test "run iterator: empty cells with background set" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); { const run = (try it.next(alloc)).?; @@ -743,6 +751,7 @@ test "shape" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -778,6 +787,7 @@ test "shape nerd fonts" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -806,6 +816,7 @@ test "shape inconsolata ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -831,6 +842,7 @@ test "shape inconsolata ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -864,6 +876,7 @@ test "shape monaspace ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -898,6 +911,7 @@ test "shape left-replaced lig in last run" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -932,6 +946,7 @@ test "shape left-replaced lig in early run" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); const run = (try it.next(alloc)).?; @@ -963,6 +978,7 @@ test "shape U+3C9 with JB Mono" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var run_count: usize = 0; @@ -996,6 +1012,7 @@ test "shape emoji width" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1046,6 +1063,7 @@ test "shape emoji width long" { screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1082,6 +1100,7 @@ test "shape variation selector VS15" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1117,6 +1136,7 @@ test "shape variation selector VS16" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1149,6 +1169,7 @@ test "shape with empty cells in between" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1187,6 +1208,7 @@ test "shape Chinese characters" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1227,6 +1249,7 @@ test "shape box glyphs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1267,6 +1290,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1290,6 +1314,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1313,6 +1338,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1336,6 +1362,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1359,6 +1386,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1391,6 +1419,7 @@ test "shape cursor boundary" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1400,61 +1429,126 @@ test "shape cursor boundary" { try testing.expectEqual(@as(usize, 1), count); } - // Cursor at index 0 is two runs { - // Get our run iterator - var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at index 0 is two runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 0, + .{ .cursor = true }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 2), count); + } + // And without cursor splitting remains one + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 0, + .{ .cursor = false }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); } - try testing.expectEqual(@as(usize, 2), count); } - // Cursor at index 1 is three runs { - // Get our run iterator - var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at index 1 is three runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 1, + .{ .cursor = true }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 3), count); + } + // And without cursor splitting remains one + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 1, + .{ .cursor = false }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); } - try testing.expectEqual(@as(usize, 3), count); } - - // Cursor at last col is two runs { - // Get our run iterator - var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 9, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at last col is two runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 9, + .{ .cursor = true }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 2), count); + } + // And without cursor splitting remains one + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 9, + .{ .cursor = false }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); } - try testing.expectEqual(@as(usize, 2), count); } } @@ -1480,6 +1574,7 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1499,6 +1594,25 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, 0, + .{}, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); + } + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 0, + .{ .cursor = false }, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1516,6 +1630,25 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, 1, + .{}, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); + } + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 1, + .{ .cursor = false }, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1546,6 +1679,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1570,6 +1704,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1595,6 +1730,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1620,6 +1756,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1644,6 +1781,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1684,6 +1822,7 @@ test "shape high plane sprite font codepoint" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); // We should get one run const run = (try it.next(alloc)).?; diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 361cbbe93..56654e88f 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator; const harfbuzz = @import("harfbuzz"); const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); +const config = @import("../../config.zig"); const Feature = font.shape.Feature; const FeatureList = font.shape.FeatureList; const default_features = font.shape.default_features; @@ -94,6 +95,7 @@ pub const Shaper = struct { row: terminal.Pin, selection: ?terminal.Selection, cursor_x: ?usize, + break_config: config.FontShapingBreak, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, @@ -102,6 +104,7 @@ pub const Shaper = struct { .row = row, .selection = selection, .cursor_x = cursor_x, + .break_config = break_config, }; } @@ -231,6 +234,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -250,6 +254,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; @@ -270,6 +275,7 @@ test "run iterator" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |_| { @@ -322,6 +328,7 @@ test "run iterator: empty cells with background set" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); { const run = (try it.next(alloc)).?; @@ -359,6 +366,7 @@ test "shape" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -388,6 +396,7 @@ test "shape inconsolata ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -413,6 +422,7 @@ test "shape inconsolata ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -446,6 +456,7 @@ test "shape monaspace ligs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -482,6 +493,7 @@ test "shape arabic forced LTR" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -519,6 +531,7 @@ test "shape emoji width" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -571,6 +584,7 @@ test "shape emoji width long" { screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -609,6 +623,7 @@ test "shape variation selector VS15" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -646,6 +661,7 @@ test "shape variation selector VS16" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -680,6 +696,7 @@ test "shape with empty cells in between" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -718,6 +735,7 @@ test "shape Chinese characters" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -758,6 +776,7 @@ test "shape box glyphs" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -799,6 +818,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -822,6 +842,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -845,6 +866,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -868,6 +890,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -891,6 +914,7 @@ test "shape selection boundary" { false, ), null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -923,6 +947,7 @@ test "shape cursor boundary" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -932,61 +957,126 @@ test "shape cursor boundary" { try testing.expectEqual(@as(usize, 1), count); } - // Cursor at index 0 is two runs { - // Get our run iterator - var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at index 0 is two runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 0, + .{ .cursor = true }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 2), count); + } + // And without cursor splitting remains one + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 0, + .{ .cursor = false }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); } - try testing.expectEqual(@as(usize, 2), count); } - // Cursor at index 1 is three runs { - // Get our run iterator - var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at index 1 is three runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 1, + .{ .cursor = true }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 3), count); + } + // And without cursor splitting remains one + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 1, + .{ .cursor = false }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); } - try testing.expectEqual(@as(usize, 3), count); } - - // Cursor at last col is two runs { - // Get our run iterator - var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 9, - ); - var count: usize = 0; - while (try it.next(alloc)) |run| { - count += 1; - _ = try shaper.shape(run); + // Cursor at last col is two runs + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 9, + .{ .cursor = true }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 2), count); + } + // And without cursor splitting remains one + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 9, + .{ .cursor = false }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); } - try testing.expectEqual(@as(usize, 2), count); } } @@ -1012,6 +1102,7 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1031,6 +1122,25 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, 0, + .{ .cursor = true }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); + } + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 0, + .{ .cursor = false }, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1048,6 +1158,25 @@ test "shape cursor boundary and colored emoji" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, 1, + .{ .cursor = true }, + ); + var count: usize = 0; + while (try it.next(alloc)) |run| { + count += 1; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); + } + { + // Get our run iterator + var shaper = &testdata.shaper; + var it = shaper.runIterator( + testdata.grid, + &screen, + screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + null, + 1, + .{ .cursor = false }, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1078,6 +1207,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1102,6 +1232,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1127,6 +1258,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1152,6 +1284,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { @@ -1176,6 +1309,7 @@ test "shape cell attribute change" { screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, null, null, + .{}, ); var count: usize = 0; while (try it.next(alloc)) |run| { diff --git a/src/font/shaper/noop.zig b/src/font/shaper/noop.zig index f8988f4ee..1041954e6 100644 --- a/src/font/shaper/noop.zig +++ b/src/font/shaper/noop.zig @@ -3,6 +3,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const trace = @import("tracy").trace; const font = @import("../main.zig"); +const config = @import("../../config.zig"); const Face = font.Face; const Collection = font.Collection; const DeferredFace = font.DeferredFace; @@ -75,6 +76,7 @@ pub const Shaper = struct { row: terminal.Pin, selection: ?terminal.Selection, cursor_x: ?usize, + break_config: config.FontShapingBreak, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, @@ -83,6 +85,7 @@ pub const Shaper = struct { .row = row, .selection = selection, .cursor_x = cursor_x, + .break_config = break_config, }; } diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 18ddd4b56..a6ff79e39 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -6,6 +6,8 @@ const shape = @import("../shape.zig"); const terminal = @import("../../terminal/main.zig"); const autoHash = std.hash.autoHash; const Hasher = std.hash.Wyhash; +const configpkg = @import("../../config.zig"); +const Config = configpkg.Config; /// A single text run. A text run is only valid for one Shaper instance and /// until the next run is created. A text run never goes across multiple @@ -40,6 +42,7 @@ pub const RunIterator = struct { row: terminal.Pin, selection: ?terminal.Selection = null, cursor_x: ?usize = null, + break_config: configpkg.FontShapingBreak, i: usize = 0, pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { @@ -175,36 +178,38 @@ pub const RunIterator = struct { break :emoji null; }; - // If our cursor is on this line then we break the run around the - // cursor. This means that any row with a cursor has at least - // three breaks: before, exactly the cursor, and after. - // - // We do not break a cell that is exactly the grapheme. If there - // are cells following that contain joiners, we allow those to - // break. This creates an effect where hovering over an emoji - // such as a skin-tone emoji is fine, but hovering over the - // joiners will show the joiners allowing you to modify the - // emoji. - if (!cell.hasGrapheme()) { - if (self.cursor_x) |cursor_x| { - // Exactly: self.i is the cursor and we iterated once. This - // means that we started exactly at the cursor and did at - // exactly one iteration. Why exactly one? Because we may - // start at our cursor but do many if our cursor is exactly - // on an emoji. - if (self.i == cursor_x and j == self.i + 1) break; + if (self.break_config.cursor) { + // If our cursor is on this line then we break the run around the + // cursor. This means that any row with a cursor has at least + // three breaks: before, exactly the cursor, and after. + // + // We do not break a cell that is exactly the grapheme. If there + // are cells following that contain joiners, we allow those to + // break. This creates an effect where hovering over an emoji + // such as a skin-tone emoji is fine, but hovering over the + // joiners will show the joiners allowing you to modify the + // emoji. + if (!cell.hasGrapheme()) { + if (self.cursor_x) |cursor_x| { + // Exactly: self.i is the cursor and we iterated once. This + // means that we started exactly at the cursor and did at + // exactly one iteration. Why exactly one? Because we may + // start at our cursor but do many if our cursor is exactly + // on an emoji. + if (self.i == cursor_x and j == self.i + 1) break; - // Before: up to and not including the cursor. This means - // that we started before the cursor (self.i < cursor_x) - // and j is now at the cursor meaning we haven't yet processed - // the cursor. - if (self.i < cursor_x and j == cursor_x) { - assert(j > 0); - break; + // Before: up to and not including the cursor. This means + // that we started before the cursor (self.i < cursor_x) + // and j is now at the cursor meaning we haven't yet processed + // the cursor. + if (self.i < cursor_x and j == cursor_x) { + assert(j > 0); + break; + } + + // After: after the cursor. We don't need to do anything + // special, we just let the run complete. } - - // After: after the cursor. We don't need to do anything - // special, we just let the run complete. } } diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index f38ab885a..95e220b84 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator; const ziglyph = @import("ziglyph"); const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); +const config = @import("../../config.zig"); const log = std.log.scoped(.font_shaper); @@ -65,6 +66,7 @@ pub const Shaper = struct { row: terminal.Screen.Row, selection: ?terminal.Selection, cursor_x: ?usize, + break_config: config.FontShapingBreak, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, @@ -72,6 +74,7 @@ pub const Shaper = struct { .row = row, .selection = selection, .cursor_x = cursor_x, + .break_config = break_config, }; } From 73ff4b8f7472a3cccefbd900f24f3f0c604ad044 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 30 Jun 2025 08:22:49 -0700 Subject: [PATCH 065/114] move runIterator options to dedicated struct --- src/font/shape.zig | 35 ++ src/font/shaper/coretext.zig | 583 +++++++++++++-------------------- src/font/shaper/harfbuzz.zig | 518 ++++++++++++----------------- src/font/shaper/noop.zig | 15 +- src/font/shaper/run.zig | 101 +++--- src/font/shaper/web_canvas.zig | 13 +- src/renderer/generic.zig | 18 +- 7 files changed, 525 insertions(+), 758 deletions(-) diff --git a/src/font/shape.zig b/src/font/shape.zig index cc67fc7a0..5e1e30eec 100644 --- a/src/font/shape.zig +++ b/src/font/shape.zig @@ -2,6 +2,9 @@ const builtin = @import("builtin"); const options = @import("main.zig").options; const run = @import("shaper/run.zig"); const feature = @import("shaper/feature.zig"); +const configpkg = @import("../config.zig"); +const terminal = @import("../terminal/main.zig"); +const SharedGrid = @import("main.zig").SharedGrid; pub const noop = @import("shaper/noop.zig"); pub const harfbuzz = @import("shaper/harfbuzz.zig"); pub const coretext = @import("shaper/coretext.zig"); @@ -61,6 +64,38 @@ pub const Options = struct { features: []const []const u8 = &.{}, }; +/// Options for runIterator. +pub const RunOptions = struct { + /// The font state for the terminal screen. This is mutable because + /// cached values may be updated during shaping. + grid: *SharedGrid, + + /// The terminal screen to shape. + screen: *const terminal.Screen, + + /// The row within the screen to shape. This row must exist within + /// screen; it is not validated. + row: terminal.Pin, + + /// The selection boundaries. This is used to break shaping on + /// selection boundaries. This can be disabled by setting this to + /// null. + selection: ?terminal.Selection = null, + + /// The cursor position within this row. This is used to break shaping + /// on cursor boundaries. This can be disabled by setting this to + /// null. + cursor_x: ?usize = null, + + /// Apply the font break configuration to the run. + pub fn applyBreakConfig( + self: *RunOptions, + config: configpkg.FontShapingBreak, + ) void { + if (!config.cursor) self.cursor_x = null; + } +}; + test { _ = Cache; _ = Shaper; diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 654af02d9..1fd9719bb 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -289,21 +289,11 @@ pub const Shaper = struct { pub fn runIterator( self: *Shaper, - grid: *SharedGrid, - screen: *const terminal.Screen, - row: terminal.Pin, - selection: ?terminal.Selection, - cursor_x: ?usize, - break_config: config.FontShapingBreak, + opts: font.shape.RunOptions, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, - .grid = grid, - .screen = screen, - .row = row, - .selection = selection, - .cursor_x = cursor_x, - .break_config = break_config, + .opts = opts, }; } @@ -597,14 +587,11 @@ test "run iterator" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; try testing.expectEqual(@as(usize, 1), count); @@ -617,14 +604,11 @@ test "run iterator" { try screen.testWriteString("ABCD EFG"); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; try testing.expectEqual(@as(usize, 1), count); @@ -638,14 +622,11 @@ test "run iterator" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; try testing.expectEqual(@as(usize, 3), count); @@ -660,14 +641,11 @@ test "run iterator" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; try testing.expectEqual(@as(usize, 2), count); @@ -708,14 +686,11 @@ test "run iterator: empty cells with background set" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); { const run = (try it.next(alloc)).?; const cells = try shaper.shape(run); @@ -745,14 +720,11 @@ test "shape" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -781,14 +753,11 @@ test "shape nerd fonts" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -810,14 +779,11 @@ test "shape inconsolata ligs" { try screen.testWriteString(">="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -836,14 +802,11 @@ test "shape inconsolata ligs" { try screen.testWriteString("==="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -870,14 +833,11 @@ test "shape monaspace ligs" { try screen.testWriteString("==="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -905,14 +865,11 @@ test "shape left-replaced lig in last run" { try screen.testWriteString("!=="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -940,14 +897,11 @@ test "shape left-replaced lig in early run" { try screen.testWriteString("!==X"); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); const run = (try it.next(alloc)).?; @@ -972,14 +926,11 @@ test "shape U+3C9 with JB Mono" { try screen.testWriteString("\u{03C9} foo"); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var run_count: usize = 0; var cell_count: usize = 0; @@ -1006,14 +957,11 @@ test "shape emoji width" { try screen.testWriteString("👍"); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1057,14 +1005,11 @@ test "shape emoji width long" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1094,14 +1039,11 @@ test "shape variation selector VS15" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1130,14 +1072,11 @@ test "shape variation selector VS16" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1163,14 +1102,11 @@ test "shape with empty cells in between" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1202,14 +1138,11 @@ test "shape Chinese characters" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1243,14 +1176,11 @@ test "shape box glyphs" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1280,18 +1210,16 @@ test "shape selection boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - terminal.Selection.init( + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .selection = terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, false, ), - null, - .{}, - ); + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1304,18 +1232,16 @@ test "shape selection boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - terminal.Selection.init( + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .selection = terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, false, ), - null, - .{}, - ); + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1328,18 +1254,16 @@ test "shape selection boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - terminal.Selection.init( + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .selection = terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, false, ), - null, - .{}, - ); + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1352,18 +1276,16 @@ test "shape selection boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - terminal.Selection.init( + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .selection = terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, false, ), - null, - .{}, - ); + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1376,18 +1298,16 @@ test "shape selection boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - terminal.Selection.init( + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .selection = terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, false, ), - null, - .{}, - ); + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1413,14 +1333,11 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1434,14 +1351,12 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - .{ .cursor = true }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cursor_x = 0, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1453,14 +1368,11 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - .{ .cursor = false }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1475,14 +1387,12 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - .{ .cursor = true }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cursor_x = 1, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1494,14 +1404,11 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - .{ .cursor = false }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1515,14 +1422,12 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 9, - .{ .cursor = true }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cursor_x = 9, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1534,14 +1439,11 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 9, - .{ .cursor = false }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1568,14 +1470,11 @@ test "shape cursor boundary and colored emoji" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1588,14 +1487,12 @@ test "shape cursor boundary and colored emoji" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cursor_x = 0, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1606,14 +1503,11 @@ test "shape cursor boundary and colored emoji" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - .{ .cursor = false }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1624,14 +1518,12 @@ test "shape cursor boundary and colored emoji" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cursor_x = 1, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1642,14 +1534,11 @@ test "shape cursor boundary and colored emoji" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - .{ .cursor = false }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1673,14 +1562,11 @@ test "shape cell attribute change" { try screen.testWriteString(">="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1698,14 +1584,11 @@ test "shape cell attribute change" { try screen.testWriteString("="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1724,14 +1607,11 @@ test "shape cell attribute change" { try screen.testWriteString("="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1750,14 +1630,11 @@ test "shape cell attribute change" { try screen.testWriteString("="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1775,14 +1652,11 @@ test "shape cell attribute change" { try screen.testWriteString("="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1816,14 +1690,11 @@ test "shape high plane sprite font codepoint" { try screen.testWriteString("\u{1FB70}"); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); // We should get one run const run = (try it.next(alloc)).?; // The run state should have the UTF-16 encoding of the character. diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 56654e88f..4209f795c 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -90,21 +90,11 @@ pub const Shaper = struct { /// and assume the y value matches. pub fn runIterator( self: *Shaper, - grid: *SharedGrid, - screen: *const terminal.Screen, - row: terminal.Pin, - selection: ?terminal.Selection, - cursor_x: ?usize, - break_config: config.FontShapingBreak, + opts: font.shape.RunOptions, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, - .grid = grid, - .screen = screen, - .row = row, - .selection = selection, - .cursor_x = cursor_x, - .break_config = break_config, + .opts = opts, }; } @@ -228,14 +218,11 @@ test "run iterator" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; try testing.expectEqual(@as(usize, 1), count); @@ -248,14 +235,11 @@ test "run iterator" { try screen.testWriteString("ABCD EFG"); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |_| count += 1; try testing.expectEqual(@as(usize, 1), count); @@ -269,14 +253,11 @@ test "run iterator" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |_| { count += 1; @@ -322,14 +303,11 @@ test "run iterator: empty cells with background set" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); { const run = (try it.next(alloc)).?; try testing.expectEqual(@as(u32, 3), shaper.hb_buf.getLength()); @@ -360,14 +338,11 @@ test "shape" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -390,14 +365,11 @@ test "shape inconsolata ligs" { try screen.testWriteString(">="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -416,14 +388,11 @@ test "shape inconsolata ligs" { try screen.testWriteString("==="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -450,14 +419,11 @@ test "shape monaspace ligs" { try screen.testWriteString("==="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -487,14 +453,11 @@ test "shape arabic forced LTR" { try screen.testWriteString(@embedFile("testdata/arabic.txt")); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -525,14 +488,11 @@ test "shape emoji width" { try screen.testWriteString("👍"); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -578,14 +538,11 @@ test "shape emoji width long" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -617,14 +574,11 @@ test "shape variation selector VS15" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -655,14 +609,11 @@ test "shape variation selector VS16" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -690,14 +641,11 @@ test "shape with empty cells in between" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -729,14 +677,11 @@ test "shape Chinese characters" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -770,14 +715,11 @@ test "shape box glyphs" { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -808,18 +750,16 @@ test "shape selection boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - terminal.Selection.init( + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .selection = terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, false, ), - null, - .{}, - ); + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -832,18 +772,16 @@ test "shape selection boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - terminal.Selection.init( + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .selection = terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, false, ), - null, - .{}, - ); + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -856,18 +794,16 @@ test "shape selection boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - terminal.Selection.init( + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .selection = terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, false, ), - null, - .{}, - ); + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -880,18 +816,16 @@ test "shape selection boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - terminal.Selection.init( + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .selection = terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, false, ), - null, - .{}, - ); + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -904,18 +838,16 @@ test "shape selection boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - terminal.Selection.init( + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .selection = terminal.Selection.init( screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, false, ), - null, - .{}, - ); + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -941,14 +873,11 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -962,14 +891,12 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - .{ .cursor = true }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cursor_x = 0, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -981,14 +908,11 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - .{ .cursor = false }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1003,14 +927,12 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - .{ .cursor = true }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cursor_x = 1, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1022,14 +944,11 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - .{ .cursor = false }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1043,14 +962,12 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 9, - .{ .cursor = true }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cursor_x = 9, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1062,14 +979,11 @@ test "shape cursor boundary" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 9, - .{ .cursor = false }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1096,14 +1010,11 @@ test "shape cursor boundary and colored emoji" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1116,14 +1027,12 @@ test "shape cursor boundary and colored emoji" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - .{ .cursor = true }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cursor_x = 0, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1134,14 +1043,11 @@ test "shape cursor boundary and colored emoji" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 0, - .{ .cursor = false }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1152,14 +1058,12 @@ test "shape cursor boundary and colored emoji" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - .{ .cursor = true }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + .cursor_x = 1, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1170,14 +1074,11 @@ test "shape cursor boundary and colored emoji" { { // Get our run iterator var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - 1, - .{ .cursor = false }, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1201,14 +1102,11 @@ test "shape cell attribute change" { try screen.testWriteString(">="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1226,14 +1124,11 @@ test "shape cell attribute change" { try screen.testWriteString("="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1252,14 +1147,11 @@ test "shape cell attribute change" { try screen.testWriteString("="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1278,14 +1170,11 @@ test "shape cell attribute change" { try screen.testWriteString("="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; @@ -1303,14 +1192,11 @@ test "shape cell attribute change" { try screen.testWriteString("="); var shaper = &testdata.shaper; - var it = shaper.runIterator( - testdata.grid, - &screen, - screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, - null, - null, - .{}, - ); + var it = shaper.runIterator(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); var count: usize = 0; while (try it.next(alloc)) |run| { count += 1; diff --git a/src/font/shaper/noop.zig b/src/font/shaper/noop.zig index 1041954e6..8723071d7 100644 --- a/src/font/shaper/noop.zig +++ b/src/font/shaper/noop.zig @@ -3,7 +3,6 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const trace = @import("tracy").trace; const font = @import("../main.zig"); -const config = @import("../../config.zig"); const Face = font.Face; const Collection = font.Collection; const DeferredFace = font.DeferredFace; @@ -71,21 +70,11 @@ pub const Shaper = struct { pub fn runIterator( self: *Shaper, - grid: *SharedGrid, - screen: *const terminal.Screen, - row: terminal.Pin, - selection: ?terminal.Selection, - cursor_x: ?usize, - break_config: config.FontShapingBreak, + opts: font.shape.RunOptions, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, - .grid = grid, - .screen = screen, - .row = row, - .selection = selection, - .cursor_x = cursor_x, - .break_config = break_config, + .opts = opts, }; } diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index a6ff79e39..92e629e19 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -6,8 +6,6 @@ const shape = @import("../shape.zig"); const terminal = @import("../../terminal/main.zig"); const autoHash = std.hash.autoHash; const Hasher = std.hash.Wyhash; -const configpkg = @import("../../config.zig"); -const Config = configpkg.Config; /// A single text run. A text run is only valid for one Shaper instance and /// until the next run is created. A text run never goes across multiple @@ -37,16 +35,11 @@ pub const TextRun = struct { /// RunIterator is an iterator that yields text runs. pub const RunIterator = struct { hooks: font.Shaper.RunIteratorHook, - grid: *font.SharedGrid, - screen: *const terminal.Screen, - row: terminal.Pin, - selection: ?terminal.Selection = null, - cursor_x: ?usize = null, - break_config: configpkg.FontShapingBreak, + opts: shape.RunOptions, i: usize = 0, pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { - const cells = self.row.cells(.all); + const cells = self.opts.row.cells(.all); // Trim the right side of a row that might be empty const max: usize = max: { @@ -61,7 +54,7 @@ pub const RunIterator = struct { // Invisible cells don't have any glyphs rendered, // so we explicitly skip them in the shaping process. while (self.i < max and - self.row.style(&cells[self.i]).flags.invisible) + self.opts.row.style(&cells[self.i]).flags.invisible) { self.i += 1; } @@ -79,7 +72,7 @@ pub const RunIterator = struct { var hasher = Hasher.init(0); // Let's get our style that we'll expect for the run. - const style = self.row.style(&cells[self.i]); + const style = self.opts.row.style(&cells[self.i]); // Go through cell by cell and accumulate while we build our run. var j: usize = self.i; @@ -89,9 +82,9 @@ pub const RunIterator = struct { // If we have a selection and we're at a boundary point, then // we break the run here. - if (self.selection) |unordered_sel| { + if (self.opts.selection) |unordered_sel| { if (j > self.i) { - const sel = unordered_sel.ordered(self.screen, .forward); + const sel = unordered_sel.ordered(self.opts.screen, .forward); const start_x = sel.start().x; const end_x = sel.end().x; @@ -145,7 +138,7 @@ pub const RunIterator = struct { // The style is different. We allow differing background // styles but any other change results in a new run. const c1 = comparableStyle(style); - const c2 = comparableStyle(self.row.style(&cells[j])); + const c2 = comparableStyle(self.opts.row.style(&cells[j])); if (!c1.eql(c2)) break; } @@ -165,7 +158,7 @@ pub const RunIterator = struct { const presentation: ?font.Presentation = if (cell.hasGrapheme()) p: { // We only check the FIRST codepoint because I believe the // presentation format must be directly adjacent to the codepoint. - const cps = self.row.grapheme(cell) orelse break :p null; + const cps = self.opts.row.grapheme(cell) orelse break :p null; assert(cps.len > 0); if (cps[0] == 0xFE0E) break :p .text; if (cps[0] == 0xFE0F) break :p .emoji; @@ -178,38 +171,36 @@ pub const RunIterator = struct { break :emoji null; }; - if (self.break_config.cursor) { - // If our cursor is on this line then we break the run around the - // cursor. This means that any row with a cursor has at least - // three breaks: before, exactly the cursor, and after. - // - // We do not break a cell that is exactly the grapheme. If there - // are cells following that contain joiners, we allow those to - // break. This creates an effect where hovering over an emoji - // such as a skin-tone emoji is fine, but hovering over the - // joiners will show the joiners allowing you to modify the - // emoji. - if (!cell.hasGrapheme()) { - if (self.cursor_x) |cursor_x| { - // Exactly: self.i is the cursor and we iterated once. This - // means that we started exactly at the cursor and did at - // exactly one iteration. Why exactly one? Because we may - // start at our cursor but do many if our cursor is exactly - // on an emoji. - if (self.i == cursor_x and j == self.i + 1) break; + // If our cursor is on this line then we break the run around the + // cursor. This means that any row with a cursor has at least + // three breaks: before, exactly the cursor, and after. + // + // We do not break a cell that is exactly the grapheme. If there + // are cells following that contain joiners, we allow those to + // break. This creates an effect where hovering over an emoji + // such as a skin-tone emoji is fine, but hovering over the + // joiners will show the joiners allowing you to modify the + // emoji. + if (!cell.hasGrapheme()) { + if (self.opts.cursor_x) |cursor_x| { + // Exactly: self.i is the cursor and we iterated once. This + // means that we started exactly at the cursor and did at + // exactly one iteration. Why exactly one? Because we may + // start at our cursor but do many if our cursor is exactly + // on an emoji. + if (self.i == cursor_x and j == self.i + 1) break; - // Before: up to and not including the cursor. This means - // that we started before the cursor (self.i < cursor_x) - // and j is now at the cursor meaning we haven't yet processed - // the cursor. - if (self.i < cursor_x and j == cursor_x) { - assert(j > 0); - break; - } - - // After: after the cursor. We don't need to do anything - // special, we just let the run complete. + // Before: up to and not including the cursor. This means + // that we started before the cursor (self.i < cursor_x) + // and j is now at the cursor meaning we haven't yet processed + // the cursor. + if (self.i < cursor_x and j == cursor_x) { + assert(j > 0); + break; } + + // After: after the cursor. We don't need to do anything + // special, we just let the run complete. } } @@ -232,7 +223,7 @@ pub const RunIterator = struct { // Otherwise we need a fallback character. Prefer the // official replacement character. - if (try self.grid.getIndex( + if (try self.opts.grid.getIndex( alloc, 0xFFFD, // replacement char font_style, @@ -240,7 +231,7 @@ pub const RunIterator = struct { )) |idx| break :font_info .{ .idx = idx, .fallback = 0xFFFD }; // Fallback to space - if (try self.grid.getIndex( + if (try self.opts.grid.getIndex( alloc, ' ', font_style, @@ -278,7 +269,7 @@ pub const RunIterator = struct { @intCast(cluster), ); if (cell.hasGrapheme()) { - const cps = self.row.grapheme(cell).?; + const cps = self.opts.row.grapheme(cell).?; for (cps) |cp| { // Do not send presentation modifiers if (cp == 0xFE0E or cp == 0xFE0F) continue; @@ -303,7 +294,7 @@ pub const RunIterator = struct { .hash = hasher.final(), .offset = @intCast(self.i), .cells = @intCast(j - self.i), - .grid = self.grid, + .grid = self.opts.grid, .font_index = current_font, }; } @@ -331,7 +322,7 @@ pub const RunIterator = struct { cell.codepoint() == 0 or cell.codepoint() == terminal.kitty.graphics.unicode.placeholder) { - return try self.grid.getIndex( + return try self.opts.grid.getIndex( alloc, ' ', style, @@ -341,7 +332,7 @@ pub const RunIterator = struct { // Get the font index for the primary codepoint. const primary_cp: u32 = cell.codepoint(); - const primary = try self.grid.getIndex( + const primary = try self.opts.grid.getIndex( alloc, primary_cp, style, @@ -354,7 +345,7 @@ pub const RunIterator = struct { // If this is a grapheme, we need to find a font that supports // all of the codepoints in the grapheme. - const cps = self.row.grapheme(cell) orelse return primary; + const cps = self.opts.row.grapheme(cell) orelse return primary; var candidates = try std.ArrayList(font.Collection.Index).initCapacity(alloc, cps.len + 1); defer candidates.deinit(); candidates.appendAssumeCapacity(primary); @@ -370,7 +361,7 @@ pub const RunIterator = struct { // to support the base presentation, since it is common for emoji // fonts to support the base emoji with emoji presentation but not // certain ZWJ-combined characters like the male and female signs. - const idx = try self.grid.getIndex( + const idx = try self.opts.grid.getIndex( alloc, cp, style, @@ -381,11 +372,11 @@ pub const RunIterator = struct { // We need to find a candidate that has ALL of our codepoints for (candidates.items) |idx| { - if (!self.grid.hasCodepoint(idx, primary_cp, presentation)) continue; + if (!self.opts.grid.hasCodepoint(idx, primary_cp, presentation)) continue; for (cps) |cp| { // Ignore Emoji ZWJs if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue; - if (!self.grid.hasCodepoint(idx, cp, null)) break; + if (!self.opts.grid.hasCodepoint(idx, cp, null)) break; } else { // If the while completed, then we have a candidate that // supports all of our codepoints. diff --git a/src/font/shaper/web_canvas.zig b/src/font/shaper/web_canvas.zig index 95e220b84..4ed4b7db6 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -4,7 +4,6 @@ const Allocator = std.mem.Allocator; const ziglyph = @import("ziglyph"); const font = @import("../main.zig"); const terminal = @import("../../terminal/main.zig"); -const config = @import("../../config.zig"); const log = std.log.scoped(.font_shaper); @@ -62,19 +61,11 @@ pub const Shaper = struct { /// for a Shaper struct since they share state. pub fn runIterator( self: *Shaper, - group: *font.GroupCache, - row: terminal.Screen.Row, - selection: ?terminal.Selection, - cursor_x: ?usize, - break_config: config.FontShapingBreak, + opts: font.shape.RunOptions, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, - .group = group, - .row = row, - .selection = selection, - .cursor_x = cursor_x, - .break_config = break_config, + .opts = opts, }; } diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index bf189fc4c..fba577231 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -513,6 +513,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { font_thicken_strength: u8, font_features: std.ArrayListUnmanaged([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, + font_shaping_break: configpkg.FontShapingBreak, cursor_color: ?terminal.color.RGB, cursor_invert: bool, cursor_opacity: f64, @@ -578,6 +579,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .font_thicken_strength = config.@"font-thicken-strength", .font_features = font_features.list, .font_styles = font_styles, + .font_shaping_break = config.@"font-shaping-break", .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) config.@"cursor-color".?.toTerminalRGB() @@ -2467,13 +2469,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type { } // Iterator of runs for shaping. - var run_iter = self.font_shaper.runIterator( - self.font_grid, - screen, - row, - row_selection, - if (shape_cursor) screen.cursor.x else null, - ); + var run_iter_opts: font.shape.RunOptions = .{ + .grid = self.font_grid, + .screen = screen, + .row = row, + .selection = row_selection, + .cursor_x = if (shape_cursor) screen.cursor.x else null, + }; + run_iter_opts.applyBreakConfig(self.config.font_shaping_break); + var run_iter = self.font_shaper.runIterator(run_iter_opts); var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); var shaper_cells: ?[]const font.shape.Cell = null; var shaper_cells_i: usize = 0; From c00b8740aa53ca1dfbd2bff42d92d740a1449bb3 Mon Sep 17 00:00:00 2001 From: moni-dz Date: Thu, 2 Jan 2025 13:12:11 +0800 Subject: [PATCH 066/114] termio: add selection scrolling callback --- src/termio/Thread.zig | 66 ++++++++++++++++++++++++++++++++++++++++++ src/termio/message.zig | 3 ++ 2 files changed, 69 insertions(+) diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index a701a29f8..e10387ef9 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -37,6 +37,9 @@ const Coalesce = struct { /// if the running program hasn't already. const sync_reset_ms = 1000; +/// The number of milliseconds between each movement during selection scrolling. +const selection_scroll_ms = 15; + /// Allocator used for some state alloc: std.mem.Allocator, @@ -53,6 +56,11 @@ wakeup_c: xev.Completion = .{}, stop: xev.Async, stop_c: xev.Completion = .{}, +/// This is used for timer-based selection scrolling. +scroll: xev.Timer, +scroll_c: xev.Completion = .{}, +scroll_active: bool = false, + /// This is used to coalesce resize events. coalesce: xev.Timer, coalesce_c: xev.Completion = .{}, @@ -92,6 +100,10 @@ pub fn init( var stop_h = try xev.Async.init(); errdefer stop_h.deinit(); + // This timer is used for selection scrolling. + var scroll_h = try xev.Timer.init(); + errdefer scroll_h.deinit(); + // This timer is used to coalesce resize events. var coalesce_h = try xev.Timer.init(); errdefer coalesce_h.deinit(); @@ -104,6 +116,7 @@ pub fn init( .alloc = alloc, .loop = loop, .stop = stop_h, + .scroll = scroll_h, .coalesce = coalesce_h, .sync_reset = sync_reset_h, }; @@ -112,6 +125,7 @@ pub fn init( /// Clean up the thread. This is only safe to call once the thread /// completes executing; the caller must join prior to this. pub fn deinit(self: *Thread) void { + self.scroll.deinit(); self.coalesce.deinit(); self.sync_reset.deinit(); self.stop.deinit(); @@ -308,6 +322,13 @@ fn drainMailbox( .size_report => |v| try io.sizeReport(data, v), .clear_screen => |v| try io.clearScreen(data, v.history), .scroll_viewport => |v| try io.scrollViewport(v), + .selection_scroll => |v| { + if (v) { + self.startScrollTimer(cb); + } else { + self.stopScrollTimer(); + } + }, .jump_to_prompt => |v| try io.jumpToPrompt(v), .start_synchronized_output => self.startSynchronizedOutput(cb), .linefeed_mode => |v| self.flags.linefeed_mode = v, @@ -446,3 +467,48 @@ fn stopCallback( cb_.?.self.loop.stop(); return .disarm; } + +fn startScrollTimer(self: *Thread, cb: *CallbackData) void { + self.scroll_active = true; + + // Start the timer which loops + self.scroll.run(&self.loop, &self.scroll_c, selection_scroll_ms, CallbackData, cb, selectionScrollCallback); +} + +fn stopScrollTimer(self: *Thread) void { + // This will stop the scrolling on the next iteration. + self.scroll_active = false; +} + +fn selectionScrollCallback( + cb_: ?*CallbackData, + _: *xev.Loop, + _: *xev.Completion, + r: xev.Timer.RunError!void, +) xev.CallbackAction { + _ = r catch |err| switch (err) { + error.Canceled => {}, + else => { + log.warn("error during selection scroll callback err={}", .{err}); + return .disarm; + }, + }; + + const cb = cb_ orelse return .disarm; + const surface = cb.io.surface_mailbox.surface; + const pos = try surface.rt_surface.getCursorPos(); + const delta: isize = if (pos.y < 0) -1 else 1; + + try cb.io.terminal.scrollViewport(.{ .delta = delta }); + + // Notify the renderer that it should repaint immediately after scrolling + cb.io.renderer_wakeup.notify() catch {}; + + const self = cb.self; + + if (self.scroll_active) { + self.scroll.run(&self.loop, &self.scroll_c, selection_scroll_ms, CallbackData, cb, selectionScrollCallback); + } + + return .disarm; +} diff --git a/src/termio/message.zig b/src/termio/message.zig index e497a298f..8057d226a 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -48,6 +48,9 @@ pub const Message = union(enum) { /// Scroll the viewport scroll_viewport: terminal.Terminal.ScrollViewport, + /// Selection scrolling + selection_scroll: bool, + /// Jump forward/backward n prompts. jump_to_prompt: isize, From f73c90bf5d146862f08e6ca1870a570d9c053019 Mon Sep 17 00:00:00 2001 From: moni-dz Date: Thu, 2 Jan 2025 13:12:11 +0800 Subject: [PATCH 067/114] surface: add timer-based scrolling during selection --- src/Surface.zig | 53 ++++++++++++++++++++++++++++++++++----- src/apprt/surface.zig | 3 +++ src/terminal/Terminal.zig | 3 +++ src/termio/Thread.zig | 8 ++---- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 5acec8c00..754807238 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -138,6 +138,9 @@ child_exited: bool = false, /// to let us know. focused: bool = true, +/// Used to determine whether to continuously scroll. +selection_scroll_active: bool = false, + /// The effect of an input event. This can be used by callers to take /// the appropriate action after an input event. For example, key /// input can be forwarded to the OS for further processing if it @@ -945,6 +948,34 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { log.warn("apprt failed to ring bell={}", .{err}); }; }, + + .selection_scroll => |active| { + self.selection_scroll_active = active; + + if (self.selection_scroll_active) { + const pos = try self.rt_surface.getCursorPos(); + const pos_vp = self.posToViewport(pos.x, pos.y); + const screen = &self.renderer_state.terminal.screen; + const delta: isize = if (pos.y < 0) -1 else 1; + + try self.io.terminal.scrollViewport(.{ .delta = delta }); + + // Always the case, but doesn't hurt to check + if (self.mouse.left_click_count == 1) { + const pin = screen.pages.pin(.{ + .viewport = .{ + .x = pos_vp.x, + .y = pos_vp.y, + }, + }) orelse { + if (comptime std.debug.runtime_safety) unreachable; + return; + }; + + try self.dragLeftClickSingle(pin, pos.x); + } + } + }, } } @@ -3260,6 +3291,12 @@ pub fn mouseButtonCallback( log.warn("error processing links err={}", .{err}); } } + + // Stop selection scrolling when releasing the left mouse button + // but only when selection scrolling is active. + if (self.selection_scroll_active) { + self.io.queueMessage(.{ .selection_scroll = false }, .unlocked); + } } // Report mouse events if enabled @@ -3767,6 +3804,12 @@ pub fn cursorPosCallback( self.renderer_state.terminal.screen.dirty.hyperlink_hover = true; } + // Stop selection scrolling when inside the viewport within a 1px buffer + // for fullscreen windows, but only when selection scrolling is active. + if (pos.x >= 1 and pos.y >= 1 and self.selection_scroll_active) { + self.io.queueMessage(.{ .selection_scroll = false }, .unlocked); + } + // Always show the mouse again if it is hidden if (self.mouse.hidden) self.showMouse(); @@ -3868,13 +3911,11 @@ pub fn cursorPosCallback( // Note: one day, we can change this from distance to time based if we want. //log.warn("CURSOR POS: {} {}", .{ pos, self.size.screen }); const max_y: f32 = @floatFromInt(self.size.screen.height); - if (pos.y <= 1 or pos.y > max_y - 1) { - const delta: isize = if (pos.y < 0) -1 else 1; - try self.io.terminal.scrollViewport(.{ .delta = delta }); - // TODO: We want a timer or something to repeat while we're still - // at this cursor position. Right now, the user has to jiggle their - // mouse in order to scroll. + // Only send a message when outside the viewport and + // selection scrolling is not currently active. + if ((pos.y <= 1 or pos.y > max_y - 1) and !self.selection_scroll_active) { + self.io.queueMessage(.{ .selection_scroll = true }, .locked); } // Convert to points diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index fcc67134b..f20e70393 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -79,6 +79,9 @@ pub const Message = union(enum) { color: terminal.color.RGB, }, + // Tell the surface to perform selection scrolling. + selection_scroll: bool, + /// The terminal has reported a change in the working directory. pwd_change: WriteReq, diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index be7a58f9b..dd7207f6d 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -124,6 +124,9 @@ flags: packed struct { /// to true based on termios state. password_input: bool = false, + /// True if the terminal should perform selection scrolling. + selection_scroll: bool = false, + /// Dirty flags for the renderer. dirty: Dirty = .{}, } = .{}, diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index e10387ef9..0cc6e1758 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -495,17 +495,13 @@ fn selectionScrollCallback( }; const cb = cb_ orelse return .disarm; - const surface = cb.io.surface_mailbox.surface; - const pos = try surface.rt_surface.getCursorPos(); - const delta: isize = if (pos.y < 0) -1 else 1; + const self = cb.self; - try cb.io.terminal.scrollViewport(.{ .delta = delta }); + _ = cb.io.surface_mailbox.push(.{ .selection_scroll = self.scroll_active }, .{ .instant = {} }); // Notify the renderer that it should repaint immediately after scrolling cb.io.renderer_wakeup.notify() catch {}; - const self = cb.self; - if (self.scroll_active) { self.scroll.run(&self.loop, &self.scroll_c, selection_scroll_ms, CallbackData, cb, selectionScrollCallback); } From 81cef6e63b996869d0e4696598562ea0b4f882a3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 30 Jun 2025 09:18:27 -0700 Subject: [PATCH 068/114] various cleanups around scroll timers --- src/Surface.zig | 107 +++++++++++++++++++++++++---------------- src/apprt/surface.zig | 8 ++- src/termio/Thread.zig | 29 ++++++++--- src/termio/message.zig | 5 +- 4 files changed, 97 insertions(+), 52 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 754807238..390adf91b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -949,36 +949,50 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { }; }, - .selection_scroll => |active| { + .selection_scroll_tick => |active| { self.selection_scroll_active = active; - - if (self.selection_scroll_active) { - const pos = try self.rt_surface.getCursorPos(); - const pos_vp = self.posToViewport(pos.x, pos.y); - const screen = &self.renderer_state.terminal.screen; - const delta: isize = if (pos.y < 0) -1 else 1; - - try self.io.terminal.scrollViewport(.{ .delta = delta }); - - // Always the case, but doesn't hurt to check - if (self.mouse.left_click_count == 1) { - const pin = screen.pages.pin(.{ - .viewport = .{ - .x = pos_vp.x, - .y = pos_vp.y, - }, - }) orelse { - if (comptime std.debug.runtime_safety) unreachable; - return; - }; - - try self.dragLeftClickSingle(pin, pos.x); - } - } + try self.selectionScrollTick(); }, } } +fn selectionScrollTick(self: *Surface) !void { + // If we're no longer active then we don't do anything. + if (!self.selection_scroll_active) return; + + // If we don't have a left mouse button down then we + // don't do anything. + if (self.mouse.left_click_count == 0) return; + + const pos = try self.rt_surface.getCursorPos(); + const pos_vp = self.posToViewport(pos.x, pos.y); + const delta: isize = if (pos.y < 0) -1 else 1; + + // We need our locked state for the remainder + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const t: *terminal.Terminal = self.renderer_state.terminal; + + // Scroll the viewport as required + try t.scrollViewport(.{ .delta = delta }); + + // Next, trigger our drag behavior + const pin = t.screen.pages.pin(.{ + .viewport = .{ + .x = pos_vp.x, + .y = pos_vp.y, + }, + }) orelse { + if (comptime std.debug.runtime_safety) unreachable; + return; + }; + try self.dragLeftClickSingle(pin, pos.x); + + // We modified our viewport and selection so we need to queue + // a render. + try self.queueRender(); +} + fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { // Mark our flag that we exited immediately self.child_exited = true; @@ -3264,6 +3278,15 @@ pub fn mouseButtonCallback( } if (button == .left and action == .release) { + // Stop selection scrolling when releasing the left mouse button + // but only when selection scrolling is active. + if (self.selection_scroll_active) { + self.io.queueMessage( + .{ .selection_scroll = false }, + .unlocked, + ); + } + // The selection clipboard is only updated for left-click drag when // the left button is released. This is to avoid the clipboard // being updated on every mouse move which would be noisy. @@ -3291,12 +3314,6 @@ pub fn mouseButtonCallback( log.warn("error processing links err={}", .{err}); } } - - // Stop selection scrolling when releasing the left mouse button - // but only when selection scrolling is active. - if (self.selection_scroll_active) { - self.io.queueMessage(.{ .selection_scroll = false }, .unlocked); - } } // Report mouse events if enabled @@ -3804,12 +3821,6 @@ pub fn cursorPosCallback( self.renderer_state.terminal.screen.dirty.hyperlink_hover = true; } - // Stop selection scrolling when inside the viewport within a 1px buffer - // for fullscreen windows, but only when selection scrolling is active. - if (pos.x >= 1 and pos.y >= 1 and self.selection_scroll_active) { - self.io.queueMessage(.{ .selection_scroll = false }, .unlocked); - } - // Always show the mouse again if it is hidden if (self.mouse.hidden) self.showMouse(); @@ -3829,6 +3840,15 @@ pub fn cursorPosCallback( self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); + // Stop selection scrolling when inside the viewport within a 1px buffer + // for fullscreen windows, but only when selection scrolling is active. + if (pos.x >= 1 and pos.y >= 1 and self.selection_scroll_active) { + self.io.queueMessage( + .{ .selection_scroll = false }, + .locked, + ); + } + // Update our mouse state. We set this to null initially because we only // want to set it when we're not selecting or doing any other mouse // event. @@ -3912,10 +3932,15 @@ pub fn cursorPosCallback( //log.warn("CURSOR POS: {} {}", .{ pos, self.size.screen }); const max_y: f32 = @floatFromInt(self.size.screen.height); - // Only send a message when outside the viewport and - // selection scrolling is not currently active. - if ((pos.y <= 1 or pos.y > max_y - 1) and !self.selection_scroll_active) { - self.io.queueMessage(.{ .selection_scroll = true }, .locked); + // If the mouse is outside the viewport and we have the left + // mouse button pressed then we need to start the scroll timer. + if ((pos.y <= 1 or pos.y > max_y - 1) and + !self.selection_scroll_active) + { + self.io.queueMessage( + .{ .selection_scroll = true }, + .locked, + ); } // Convert to points diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index f20e70393..9254b2fd5 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -79,8 +79,12 @@ pub const Message = union(enum) { color: terminal.color.RGB, }, - // Tell the surface to perform selection scrolling. - selection_scroll: bool, + /// Notifies the surface that a tick of the timer that is timing + /// out selection scrolling has occurred. "selection scrolling" + /// is when the user has clicked and dragged the mouse outside + /// the viewport of the terminal and the terminal is scrolling + /// the viewport to follow the mouse cursor. + selection_scroll_tick: bool, /// The terminal has reported a change in the working directory. pwd_change: WriteReq, diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index 0cc6e1758..7773ea7cd 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -472,7 +472,14 @@ fn startScrollTimer(self: *Thread, cb: *CallbackData) void { self.scroll_active = true; // Start the timer which loops - self.scroll.run(&self.loop, &self.scroll_c, selection_scroll_ms, CallbackData, cb, selectionScrollCallback); + self.scroll.run( + &self.loop, + &self.scroll_c, + selection_scroll_ms, + CallbackData, + cb, + selectionScrollCallback, + ); } fn stopScrollTimer(self: *Thread) void { @@ -497,14 +504,20 @@ fn selectionScrollCallback( const cb = cb_ orelse return .disarm; const self = cb.self; - _ = cb.io.surface_mailbox.push(.{ .selection_scroll = self.scroll_active }, .{ .instant = {} }); + // Send the tick to the main surface + _ = cb.io.surface_mailbox.push( + .{ .selection_scroll_tick = self.scroll_active }, + .{ .instant = {} }, + ); - // Notify the renderer that it should repaint immediately after scrolling - cb.io.renderer_wakeup.notify() catch {}; - - if (self.scroll_active) { - self.scroll.run(&self.loop, &self.scroll_c, selection_scroll_ms, CallbackData, cb, selectionScrollCallback); - } + if (self.scroll_active) self.scroll.run( + &self.loop, + &self.scroll_c, + selection_scroll_ms, + CallbackData, + cb, + selectionScrollCallback, + ); return .disarm; } diff --git a/src/termio/message.zig b/src/termio/message.zig index 8057d226a..ee6dbcc0f 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -48,7 +48,10 @@ pub const Message = union(enum) { /// Scroll the viewport scroll_viewport: terminal.Terminal.ScrollViewport, - /// Selection scrolling + /// Selection scrolling. If this is set to true then the termio + /// thread starts a timer that will trigger a `selection_scroll_tick` + /// message back to the surface. This ping/pong is because the + /// surface thread doesn't have access to an event loop from libghostty. selection_scroll: bool, /// Jump forward/backward n prompts. From 0653bcb16e38ba773d44025b8dc97987d587b4da Mon Sep 17 00:00:00 2001 From: Damyan Bogoev Date: Mon, 30 Jun 2025 19:47:30 +0300 Subject: [PATCH 069/114] Update po/bg_BG.UTF-8.po Co-authored-by: Pavel Atanasov <37866329+reo101@users.noreply.github.com> --- po/bg_BG.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po index e240fd6e7..d0ec4b803 100644 --- a/po/bg_BG.UTF-8.po +++ b/po/bg_BG.UTF-8.po @@ -52,7 +52,7 @@ msgstr "Игнорирай" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 msgid "Reload Configuration" -msgstr "Презареди на конфигурацията" +msgstr "Презареди конфигурацията" #: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 From 4fe3a01f1bec1295008fadc175bbaa07ecd3a4ab Mon Sep 17 00:00:00 2001 From: Damyan Bogoev Date: Mon, 30 Jun 2025 19:47:38 +0300 Subject: [PATCH 070/114] Update po/bg_BG.UTF-8.po Co-authored-by: Pavel Atanasov <37866329+reo101@users.noreply.github.com> --- po/bg_BG.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po index d0ec4b803..317c37a97 100644 --- a/po/bg_BG.UTF-8.po +++ b/po/bg_BG.UTF-8.po @@ -80,7 +80,7 @@ msgstr "Раздели надясно" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -msgstr "Изпълнение на команда…" +msgstr "Изпълни команда…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 From 87df0004c99fa2ba4d98755acdb68eb3bd9259b2 Mon Sep 17 00:00:00 2001 From: Damyan Bogoev Date: Mon, 30 Jun 2025 19:47:43 +0300 Subject: [PATCH 071/114] Update po/bg_BG.UTF-8.po Co-authored-by: Pavel Atanasov <37866329+reo101@users.noreply.github.com> --- po/bg_BG.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po index 317c37a97..9b2773ca5 100644 --- a/po/bg_BG.UTF-8.po +++ b/po/bg_BG.UTF-8.po @@ -149,7 +149,7 @@ msgstr "Конфигурация" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" -msgstr "Отвори на конфигурацията" +msgstr "Отвори конфигурацията" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" From 9ad4537d03b62a452f44afaa0e65d7e134477b57 Mon Sep 17 00:00:00 2001 From: Damyan Bogoev Date: Mon, 30 Jun 2025 19:47:58 +0300 Subject: [PATCH 072/114] Update po/bg_BG.UTF-8.po Co-authored-by: Pavel Atanasov <37866329+reo101@users.noreply.github.com> --- po/bg_BG.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po index 9b2773ca5..b371cb04d 100644 --- a/po/bg_BG.UTF-8.po +++ b/po/bg_BG.UTF-8.po @@ -111,7 +111,7 @@ msgstr "Раздели" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 msgid "Change Title…" -msgstr "Промяна на заглавие…" +msgstr "Промени заглавие…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" From 1377e6d22595e78762b7a9887f5d04cba69cfdc9 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 29 Jun 2025 15:33:58 -0600 Subject: [PATCH 073/114] font/sprite: rework sprite font drawing This is a fairly large rework of how we handle the sprite font drawing. Drawing routines are now context-less, provided only a canvas and some metrics. There is now a separate file per unicode block / PUA area. Sprites are now drawn on canvases with an extra quarter-cell of padding on each edge, and automatically cropped when sent to the atlas, this allows sprites to extend past cell boundaries which makes it possible to have, for example, diagonal box drawing characters that connect across cell diagonals instead of being pinched in. Most of the sprites the code is just directly ported from the old code, but I've rewritten a handful. Moving forward, I'd like to rewrite more of these since the way they're currently written isn't ideal. This rework, in addition to improving the packing efficiency of sprites on the atlas, and allowing for out-of-cell drawing, will make it a lot easier to add new sprites in the future, since all it takes now is to add a single function and an import (if it's a new file). I reworked the regression/change testing to be more robust as well, it now covers all sprite glyphs (except non-codepoint ones) and does so at 4 different sizes. Addition/removal of glyphs will no longer create diff noise in the generated diff image, since the position in the image of each glyph is now fixed. --- src/font/Atlas.zig | 31 + src/font/sprite.zig | 6 - src/font/sprite/Box.zig | 3397 ----------------- src/font/sprite/Face.zig | 675 +++- src/font/sprite/Powerline.zig | 564 --- src/font/sprite/canvas.zig | 449 ++- src/font/sprite/cursor.zig | 65 - src/font/sprite/draw/README.md | 50 + src/font/sprite/draw/block.zig | 184 + src/font/sprite/draw/box.zig | 947 +++++ src/font/sprite/draw/braille.zig | 148 + src/font/sprite/draw/branch.zig | 505 +++ src/font/sprite/draw/common.zig | 244 ++ src/font/sprite/draw/geometric_shapes.zig | 200 + src/font/sprite/{ => draw}/octants.txt | 0 src/font/sprite/draw/powerline.zig | 396 ++ src/font/sprite/draw/special.zig | 328 ++ .../draw/symbols_for_legacy_computing.zig | 1431 +++++++ ...ymbols_for_legacy_computing_supplement.zig | 193 + src/font/sprite/testdata/Box.ppm | Bin 1048593 -> 0 bytes .../testdata/U+1CC00...U+1CCFF-11x21+2.png | Bin 0 -> 403 bytes .../testdata/U+1CC00...U+1CCFF-12x24+3.png | Bin 0 -> 534 bytes .../testdata/U+1CC00...U+1CCFF-18x36+4.png | Bin 0 -> 1022 bytes .../testdata/U+1CC00...U+1CCFF-9x17+1.png | Bin 0 -> 316 bytes .../testdata/U+1CD00...U+1CDFF-11x21+2.png | Bin 0 -> 1275 bytes .../testdata/U+1CD00...U+1CDFF-12x24+3.png | Bin 0 -> 1870 bytes .../testdata/U+1CD00...U+1CDFF-18x36+4.png | Bin 0 -> 3404 bytes .../testdata/U+1CD00...U+1CDFF-9x17+1.png | Bin 0 -> 1101 bytes .../testdata/U+1FB00...U+1FBFF-11x21+2.png | Bin 0 -> 5450 bytes .../testdata/U+1FB00...U+1FBFF-12x24+3.png | Bin 0 -> 5724 bytes .../testdata/U+1FB00...U+1FBFF-18x36+4.png | Bin 0 -> 9997 bytes .../testdata/U+1FB00...U+1FBFF-9x17+1.png | Bin 0 -> 4298 bytes .../testdata/U+2500...U+25FF-11x21+2.png | Bin 0 -> 2223 bytes .../testdata/U+2500...U+25FF-12x24+3.png | Bin 0 -> 2638 bytes .../testdata/U+2500...U+25FF-18x36+4.png | Bin 0 -> 4541 bytes .../testdata/U+2500...U+25FF-9x17+1.png | Bin 0 -> 1848 bytes .../testdata/U+2800...U+28FF-11x21+2.png | Bin 0 -> 1022 bytes .../testdata/U+2800...U+28FF-12x24+3.png | Bin 0 -> 1547 bytes .../testdata/U+2800...U+28FF-18x36+4.png | Bin 0 -> 2490 bytes .../testdata/U+2800...U+28FF-9x17+1.png | Bin 0 -> 917 bytes .../testdata/U+E000...U+E0FF-11x21+2.png | Bin 0 -> 1104 bytes .../testdata/U+E000...U+E0FF-12x24+3.png | Bin 0 -> 1251 bytes .../testdata/U+E000...U+E0FF-18x36+4.png | Bin 0 -> 2228 bytes .../testdata/U+E000...U+E0FF-9x17+1.png | Bin 0 -> 895 bytes .../testdata/U+F500...U+F5FF-11x21+2.png | Bin 0 -> 1114 bytes .../testdata/U+F500...U+F5FF-12x24+3.png | Bin 0 -> 1423 bytes .../testdata/U+F500...U+F5FF-18x36+4.png | Bin 0 -> 2470 bytes .../testdata/U+F500...U+F5FF-9x17+1.png | Bin 0 -> 871 bytes .../testdata/U+F600...U+F6FF-11x21+2.png | Bin 0 -> 493 bytes .../testdata/U+F600...U+F6FF-12x24+3.png | Bin 0 -> 636 bytes .../testdata/U+F600...U+F6FF-18x36+4.png | Bin 0 -> 1218 bytes .../testdata/U+F600...U+F6FF-9x17+1.png | Bin 0 -> 394 bytes src/font/sprite/underline.zig | 312 -- typos.toml | 2 + 54 files changed, 5474 insertions(+), 4653 deletions(-) delete mode 100644 src/font/sprite/Box.zig delete mode 100644 src/font/sprite/Powerline.zig delete mode 100644 src/font/sprite/cursor.zig create mode 100644 src/font/sprite/draw/README.md create mode 100644 src/font/sprite/draw/block.zig create mode 100644 src/font/sprite/draw/box.zig create mode 100644 src/font/sprite/draw/braille.zig create mode 100644 src/font/sprite/draw/branch.zig create mode 100644 src/font/sprite/draw/common.zig create mode 100644 src/font/sprite/draw/geometric_shapes.zig rename src/font/sprite/{ => draw}/octants.txt (100%) create mode 100644 src/font/sprite/draw/powerline.zig create mode 100644 src/font/sprite/draw/special.zig create mode 100644 src/font/sprite/draw/symbols_for_legacy_computing.zig create mode 100644 src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig delete mode 100644 src/font/sprite/testdata/Box.ppm create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png delete mode 100644 src/font/sprite/underline.zig diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index 969318943..aac2e7e8d 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -251,6 +251,37 @@ pub fn set(self: *Atlas, reg: Region, data: []const u8) void { _ = self.modified.fetchAdd(1, .monotonic); } +/// Like `set` but allows specifying a width for the source data and an +/// offset x and y, so that a section of a larger buffer may be copied +/// in to the atlas. +pub fn setFromLarger( + self: *Atlas, + reg: Region, + src: []const u8, + src_width: u32, + src_x: u32, + src_y: u32, +) void { + assert(reg.x < (self.size - 1)); + assert((reg.x + reg.width) <= (self.size - 1)); + assert(reg.y < (self.size - 1)); + assert((reg.y + reg.height) <= (self.size - 1)); + + const depth = self.format.depth(); + var i: u32 = 0; + while (i < reg.height) : (i += 1) { + const tex_offset = (((reg.y + i) * self.size) + reg.x) * depth; + const src_offset = (((src_y + i) * src_width) + src_x) * depth; + fastmem.copy( + u8, + self.data[tex_offset..], + src[src_offset .. src_offset + (reg.width * depth)], + ); + } + + _ = self.modified.fetchAdd(1, .monotonic); +} + // Grow the texture to the new size, preserving all previously written data. pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void { assert(size_new >= self.size); diff --git a/src/font/sprite.zig b/src/font/sprite.zig index 6485d6008..4be06a918 100644 --- a/src/font/sprite.zig +++ b/src/font/sprite.zig @@ -33,12 +33,6 @@ pub const Sprite = enum(u32) { cursor_hollow_rect, cursor_bar, - // Note: we don't currently put the box drawing glyphs in here because - // there are a LOT and I'm lazy. What I want to do is spend more time - // studying the patterns to see if we can programmatically build our - // enum perhaps and comptime generate the drawing code at the same time. - // I'm not sure if that's advisable yet though. - test { const testing = std.testing; try testing.expectEqual(start, @intFromEnum(Sprite.underline)); diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig deleted file mode 100644 index f5140091d..000000000 --- a/src/font/sprite/Box.zig +++ /dev/null @@ -1,3397 +0,0 @@ -//! This file contains functions for drawing the box drawing characters -//! (https://en.wikipedia.org/wiki/Box-drawing_character) and related -//! characters that are provided by the terminal. -//! -//! The box drawing logic is based off similar logic in Kitty and Foot. -//! The primary drawing code was originally ported directly and slightly -//! modified from Foot (https://codeberg.org/dnkl/foot/). Foot is licensed -//! under the MIT license and is copyright 2019 Daniel Eklöf. -//! -//! The modifications made were primarily around spacing, DPI calculations, -//! and adapting the code to our atlas model. Further, more extensive changes -//! were made, refactoring the line characters to all share a single unified -//! function (draw_lines), as well as many of the fractional block characters -//! which now use draw_block instead of dedicated separate functions. -//! -//! Additional characters from Unicode 16.0 and beyond are original work. -const Box = @This(); - -const std = @import("std"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; - -const z2d = @import("z2d"); - -const font = @import("../main.zig"); -const Sprite = @import("../sprite.zig").Sprite; - -const log = std.log.scoped(.box_font); - -/// Grid metrics for the rendering. -metrics: font.Metrics, - -/// The thickness of a line. -const Thickness = enum { - super_light, - light, - heavy, - - /// Calculate the real height of a line based on its thickness - /// and a base thickness value. The base thickness value is expected - /// to be in pixels. - fn height(self: Thickness, base: u32) u32 { - return switch (self) { - .super_light => @max(base / 2, 1), - .light => base, - .heavy => base * 2, - }; - } -}; - -/// Specification of a traditional intersection-style line/box-drawing char, -/// which can have a different style of line from each edge to the center. -const Lines = packed struct(u8) { - up: Style = .none, - right: Style = .none, - down: Style = .none, - left: Style = .none, - - const Style = enum(u2) { - none, - light, - heavy, - double, - }; -}; - -/// Specification of a quadrants char, which has each of the -/// 4 quadrants of the character cell either filled or empty. -const Quads = packed struct(u4) { - tl: bool = false, - tr: bool = false, - bl: bool = false, - br: bool = false, -}; - -/// Specification of a branch drawing node, which consists of a -/// circle which is either empty or filled, and lines connecting -/// optionally between the circle and each of the 4 edges. -const BranchNode = packed struct(u5) { - up: bool = false, - right: bool = false, - down: bool = false, - left: bool = false, - filled: bool = false, -}; - -/// Alignment of a figure within a cell -const Alignment = struct { - horizontal: enum { - left, - right, - center, - } = .center, - - vertical: enum { - top, - bottom, - middle, - } = .middle, - - const upper: Alignment = .{ .vertical = .top }; - const lower: Alignment = .{ .vertical = .bottom }; - const left: Alignment = .{ .horizontal = .left }; - const right: Alignment = .{ .horizontal = .right }; - - const upper_left: Alignment = .{ .vertical = .top, .horizontal = .left }; - const upper_right: Alignment = .{ .vertical = .top, .horizontal = .right }; - const lower_left: Alignment = .{ .vertical = .bottom, .horizontal = .left }; - const lower_right: Alignment = .{ .vertical = .bottom, .horizontal = .right }; - - const center: Alignment = .{}; - - const upper_center = upper; - const lower_center = lower; - const middle_left = left; - const middle_right = right; - const middle_center: Alignment = center; - - const top = upper; - const bottom = lower; - const center_top = top; - const center_bottom = bottom; - - const top_left = upper_left; - const top_right = upper_right; - const bottom_left = lower_left; - const bottom_right = lower_right; -}; - -const Corner = enum(u2) { - tl, - tr, - bl, - br, -}; - -const Edge = enum(u2) { - top, - left, - bottom, - right, -}; - -const SmoothMosaic = packed struct(u10) { - tl: bool, - ul: bool, - ll: bool, - bl: bool, - bc: bool, - br: bool, - lr: bool, - ur: bool, - tr: bool, - tc: bool, - - fn from(comptime pattern: *const [15:0]u8) SmoothMosaic { - return .{ - .tl = pattern[0] == '#', - - .ul = pattern[4] == '#' and - (pattern[0] != '#' or pattern[8] != '#'), - - .ll = pattern[8] == '#' and - (pattern[4] != '#' or pattern[12] != '#'), - - .bl = pattern[12] == '#', - - .bc = pattern[13] == '#' and - (pattern[12] != '#' or pattern[14] != '#'), - - .br = pattern[14] == '#', - - .lr = pattern[10] == '#' and - (pattern[14] != '#' or pattern[6] != '#'), - - .ur = pattern[6] == '#' and - (pattern[10] != '#' or pattern[2] != '#'), - - .tr = pattern[2] == '#', - - .tc = pattern[1] == '#' and - (pattern[2] != '#' or pattern[0] != '#'), - }; - } -}; - -// Octant range, inclusive -const octant_min = 0x1cd00; -const octant_max = 0x1cde5; - -// Utility names for common fractions -const one_eighth: f64 = 0.125; -const one_quarter: f64 = 0.25; -const one_third: f64 = (1.0 / 3.0); -const three_eighths: f64 = 0.375; -const half: f64 = 0.5; -const five_eighths: f64 = 0.625; -const two_thirds: f64 = (2.0 / 3.0); -const three_quarters: f64 = 0.75; -const seven_eighths: f64 = 0.875; - -/// Shades -const Shade = enum(u8) { - off = 0x00, - light = 0x40, - medium = 0x80, - dark = 0xc0, - on = 0xff, - - _, -}; - -pub fn renderGlyph( - self: Box, - alloc: Allocator, - atlas: *font.Atlas, - cp: u32, -) !font.Glyph { - const metrics = self.metrics; - - // Create the canvas we'll use to draw - var canvas = try font.sprite.Canvas.init( - alloc, - metrics.cell_width, - metrics.cell_height, - ); - defer canvas.deinit(); - - // Perform the actual drawing - try self.draw(alloc, &canvas, cp); - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - // Our coordinates start at the BOTTOM for our renderers so we have to - // specify an offset of the full height because we rendered a full size - // cell. - const offset_y = @as(i32, @intCast(metrics.cell_height)); - - return font.Glyph{ - .width = metrics.cell_width, - .height = metrics.cell_height, - .offset_x = 0, - .offset_y = offset_y, - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(metrics.cell_width), - }; -} - -fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void { - _ = alloc; - switch (cp) { - // '─' - 0x2500 => self.draw_lines(canvas, .{ .left = .light, .right = .light }), - // '━' - 0x2501 => self.draw_lines(canvas, .{ .left = .heavy, .right = .heavy }), - // '│' - 0x2502 => self.draw_lines(canvas, .{ .up = .light, .down = .light }), - // '┃' - 0x2503 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy }), - // '┄' - 0x2504 => self.draw_light_triple_dash_horizontal(canvas), - // '┅' - 0x2505 => self.draw_heavy_triple_dash_horizontal(canvas), - // '┆' - 0x2506 => self.draw_light_triple_dash_vertical(canvas), - // '┇' - 0x2507 => self.draw_heavy_triple_dash_vertical(canvas), - // '┈' - 0x2508 => self.draw_light_quadruple_dash_horizontal(canvas), - // '┉' - 0x2509 => self.draw_heavy_quadruple_dash_horizontal(canvas), - // '┊' - 0x250a => self.draw_light_quadruple_dash_vertical(canvas), - // '┋' - 0x250b => self.draw_heavy_quadruple_dash_vertical(canvas), - // '┌' - 0x250c => self.draw_lines(canvas, .{ .down = .light, .right = .light }), - // '┍' - 0x250d => self.draw_lines(canvas, .{ .down = .light, .right = .heavy }), - // '┎' - 0x250e => self.draw_lines(canvas, .{ .down = .heavy, .right = .light }), - // '┏' - 0x250f => self.draw_lines(canvas, .{ .down = .heavy, .right = .heavy }), - - // '┐' - 0x2510 => self.draw_lines(canvas, .{ .down = .light, .left = .light }), - // '┑' - 0x2511 => self.draw_lines(canvas, .{ .down = .light, .left = .heavy }), - // '┒' - 0x2512 => self.draw_lines(canvas, .{ .down = .heavy, .left = .light }), - // '┓' - 0x2513 => self.draw_lines(canvas, .{ .down = .heavy, .left = .heavy }), - // '└' - 0x2514 => self.draw_lines(canvas, .{ .up = .light, .right = .light }), - // '┕' - 0x2515 => self.draw_lines(canvas, .{ .up = .light, .right = .heavy }), - // '┖' - 0x2516 => self.draw_lines(canvas, .{ .up = .heavy, .right = .light }), - // '┗' - 0x2517 => self.draw_lines(canvas, .{ .up = .heavy, .right = .heavy }), - // '┘' - 0x2518 => self.draw_lines(canvas, .{ .up = .light, .left = .light }), - // '┙' - 0x2519 => self.draw_lines(canvas, .{ .up = .light, .left = .heavy }), - // '┚' - 0x251a => self.draw_lines(canvas, .{ .up = .heavy, .left = .light }), - // '┛' - 0x251b => self.draw_lines(canvas, .{ .up = .heavy, .left = .heavy }), - // '├' - 0x251c => self.draw_lines(canvas, .{ .up = .light, .down = .light, .right = .light }), - // '┝' - 0x251d => self.draw_lines(canvas, .{ .up = .light, .down = .light, .right = .heavy }), - // '┞' - 0x251e => self.draw_lines(canvas, .{ .up = .heavy, .right = .light, .down = .light }), - // '┟' - 0x251f => self.draw_lines(canvas, .{ .down = .heavy, .right = .light, .up = .light }), - - // '┠' - 0x2520 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .right = .light }), - // '┡' - 0x2521 => self.draw_lines(canvas, .{ .down = .light, .right = .heavy, .up = .heavy }), - // '┢' - 0x2522 => self.draw_lines(canvas, .{ .up = .light, .right = .heavy, .down = .heavy }), - // '┣' - 0x2523 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .right = .heavy }), - // '┤' - 0x2524 => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .light }), - // '┥' - 0x2525 => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .heavy }), - // '┦' - 0x2526 => self.draw_lines(canvas, .{ .up = .heavy, .left = .light, .down = .light }), - // '┧' - 0x2527 => self.draw_lines(canvas, .{ .down = .heavy, .left = .light, .up = .light }), - // '┨' - 0x2528 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .light }), - // '┩' - 0x2529 => self.draw_lines(canvas, .{ .down = .light, .left = .heavy, .up = .heavy }), - // '┪' - 0x252a => self.draw_lines(canvas, .{ .up = .light, .left = .heavy, .down = .heavy }), - // '┫' - 0x252b => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy }), - // '┬' - 0x252c => self.draw_lines(canvas, .{ .down = .light, .left = .light, .right = .light }), - // '┭' - 0x252d => self.draw_lines(canvas, .{ .left = .heavy, .right = .light, .down = .light }), - // '┮' - 0x252e => self.draw_lines(canvas, .{ .right = .heavy, .left = .light, .down = .light }), - // '┯' - 0x252f => self.draw_lines(canvas, .{ .down = .light, .left = .heavy, .right = .heavy }), - - // '┰' - 0x2530 => self.draw_lines(canvas, .{ .down = .heavy, .left = .light, .right = .light }), - // '┱' - 0x2531 => self.draw_lines(canvas, .{ .right = .light, .left = .heavy, .down = .heavy }), - // '┲' - 0x2532 => self.draw_lines(canvas, .{ .left = .light, .right = .heavy, .down = .heavy }), - // '┳' - 0x2533 => self.draw_lines(canvas, .{ .down = .heavy, .left = .heavy, .right = .heavy }), - // '┴' - 0x2534 => self.draw_lines(canvas, .{ .up = .light, .left = .light, .right = .light }), - // '┵' - 0x2535 => self.draw_lines(canvas, .{ .left = .heavy, .right = .light, .up = .light }), - // '┶' - 0x2536 => self.draw_lines(canvas, .{ .right = .heavy, .left = .light, .up = .light }), - // '┷' - 0x2537 => self.draw_lines(canvas, .{ .up = .light, .left = .heavy, .right = .heavy }), - // '┸' - 0x2538 => self.draw_lines(canvas, .{ .up = .heavy, .left = .light, .right = .light }), - // '┹' - 0x2539 => self.draw_lines(canvas, .{ .right = .light, .left = .heavy, .up = .heavy }), - // '┺' - 0x253a => self.draw_lines(canvas, .{ .left = .light, .right = .heavy, .up = .heavy }), - // '┻' - 0x253b => self.draw_lines(canvas, .{ .up = .heavy, .left = .heavy, .right = .heavy }), - // '┼' - 0x253c => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .light, .right = .light }), - // '┽' - 0x253d => self.draw_lines(canvas, .{ .left = .heavy, .right = .light, .up = .light, .down = .light }), - // '┾' - 0x253e => self.draw_lines(canvas, .{ .right = .heavy, .left = .light, .up = .light, .down = .light }), - // '┿' - 0x253f => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .heavy, .right = .heavy }), - - // '╀' - 0x2540 => self.draw_lines(canvas, .{ .up = .heavy, .down = .light, .left = .light, .right = .light }), - // '╁' - 0x2541 => self.draw_lines(canvas, .{ .down = .heavy, .up = .light, .left = .light, .right = .light }), - // '╂' - 0x2542 => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }), - // '╃' - 0x2543 => self.draw_lines(canvas, .{ .left = .heavy, .up = .heavy, .right = .light, .down = .light }), - // '╄' - 0x2544 => self.draw_lines(canvas, .{ .right = .heavy, .up = .heavy, .left = .light, .down = .light }), - // '╅' - 0x2545 => self.draw_lines(canvas, .{ .left = .heavy, .down = .heavy, .right = .light, .up = .light }), - // '╆' - 0x2546 => self.draw_lines(canvas, .{ .right = .heavy, .down = .heavy, .left = .light, .up = .light }), - // '╇' - 0x2547 => self.draw_lines(canvas, .{ .down = .light, .up = .heavy, .left = .heavy, .right = .heavy }), - // '╈' - 0x2548 => self.draw_lines(canvas, .{ .up = .light, .down = .heavy, .left = .heavy, .right = .heavy }), - // '╉' - 0x2549 => self.draw_lines(canvas, .{ .right = .light, .left = .heavy, .up = .heavy, .down = .heavy }), - // '╊' - 0x254a => self.draw_lines(canvas, .{ .left = .light, .right = .heavy, .up = .heavy, .down = .heavy }), - // '╋' - 0x254b => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy, .right = .heavy }), - // '╌' - 0x254c => self.draw_light_double_dash_horizontal(canvas), - // '╍' - 0x254d => self.draw_heavy_double_dash_horizontal(canvas), - // '╎' - 0x254e => self.draw_light_double_dash_vertical(canvas), - // '╏' - 0x254f => self.draw_heavy_double_dash_vertical(canvas), - - // '═' - 0x2550 => self.draw_lines(canvas, .{ .left = .double, .right = .double }), - // '║' - 0x2551 => self.draw_lines(canvas, .{ .up = .double, .down = .double }), - // '╒' - 0x2552 => self.draw_lines(canvas, .{ .down = .light, .right = .double }), - // '╓' - 0x2553 => self.draw_lines(canvas, .{ .down = .double, .right = .light }), - // '╔' - 0x2554 => self.draw_lines(canvas, .{ .down = .double, .right = .double }), - // '╕' - 0x2555 => self.draw_lines(canvas, .{ .down = .light, .left = .double }), - // '╖' - 0x2556 => self.draw_lines(canvas, .{ .down = .double, .left = .light }), - // '╗' - 0x2557 => self.draw_lines(canvas, .{ .down = .double, .left = .double }), - // '╘' - 0x2558 => self.draw_lines(canvas, .{ .up = .light, .right = .double }), - // '╙' - 0x2559 => self.draw_lines(canvas, .{ .up = .double, .right = .light }), - // '╚' - 0x255a => self.draw_lines(canvas, .{ .up = .double, .right = .double }), - // '╛' - 0x255b => self.draw_lines(canvas, .{ .up = .light, .left = .double }), - // '╜' - 0x255c => self.draw_lines(canvas, .{ .up = .double, .left = .light }), - // '╝' - 0x255d => self.draw_lines(canvas, .{ .up = .double, .left = .double }), - // '╞' - 0x255e => self.draw_lines(canvas, .{ .up = .light, .down = .light, .right = .double }), - // '╟' - 0x255f => self.draw_lines(canvas, .{ .up = .double, .down = .double, .right = .light }), - - // '╠' - 0x2560 => self.draw_lines(canvas, .{ .up = .double, .down = .double, .right = .double }), - // '╡' - 0x2561 => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .double }), - // '╢' - 0x2562 => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .light }), - // '╣' - 0x2563 => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .double }), - // '╤' - 0x2564 => self.draw_lines(canvas, .{ .down = .light, .left = .double, .right = .double }), - // '╥' - 0x2565 => self.draw_lines(canvas, .{ .down = .double, .left = .light, .right = .light }), - // '╦' - 0x2566 => self.draw_lines(canvas, .{ .down = .double, .left = .double, .right = .double }), - // '╧' - 0x2567 => self.draw_lines(canvas, .{ .up = .light, .left = .double, .right = .double }), - // '╨' - 0x2568 => self.draw_lines(canvas, .{ .up = .double, .left = .light, .right = .light }), - // '╩' - 0x2569 => self.draw_lines(canvas, .{ .up = .double, .left = .double, .right = .double }), - // '╪' - 0x256a => self.draw_lines(canvas, .{ .up = .light, .down = .light, .left = .double, .right = .double }), - // '╫' - 0x256b => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .light, .right = .light }), - // '╬' - 0x256c => self.draw_lines(canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }), - // '╭' - 0x256d => try self.draw_arc(canvas, .br, .light), - // '╮' - 0x256e => try self.draw_arc(canvas, .bl, .light), - // '╯' - 0x256f => try self.draw_arc(canvas, .tl, .light), - - // '╰' - 0x2570 => try self.draw_arc(canvas, .tr, .light), - // '╱' - 0x2571 => self.draw_light_diagonal_upper_right_to_lower_left(canvas), - // '╲' - 0x2572 => self.draw_light_diagonal_upper_left_to_lower_right(canvas), - // '╳' - 0x2573 => self.draw_light_diagonal_cross(canvas), - // '╴' - 0x2574 => self.draw_lines(canvas, .{ .left = .light }), - // '╵' - 0x2575 => self.draw_lines(canvas, .{ .up = .light }), - // '╶' - 0x2576 => self.draw_lines(canvas, .{ .right = .light }), - // '╷' - 0x2577 => self.draw_lines(canvas, .{ .down = .light }), - // '╸' - 0x2578 => self.draw_lines(canvas, .{ .left = .heavy }), - // '╹' - 0x2579 => self.draw_lines(canvas, .{ .up = .heavy }), - // '╺' - 0x257a => self.draw_lines(canvas, .{ .right = .heavy }), - // '╻' - 0x257b => self.draw_lines(canvas, .{ .down = .heavy }), - // '╼' - 0x257c => self.draw_lines(canvas, .{ .left = .light, .right = .heavy }), - // '╽' - 0x257d => self.draw_lines(canvas, .{ .up = .light, .down = .heavy }), - // '╾' - 0x257e => self.draw_lines(canvas, .{ .left = .heavy, .right = .light }), - // '╿' - 0x257f => self.draw_lines(canvas, .{ .up = .heavy, .down = .light }), - - // '▀' UPPER HALF BLOCK - 0x2580 => self.draw_block(canvas, .upper, 1, half), - // '▁' LOWER ONE EIGHTH BLOCK - 0x2581 => self.draw_block(canvas, .lower, 1, one_eighth), - // '▂' LOWER ONE QUARTER BLOCK - 0x2582 => self.draw_block(canvas, .lower, 1, one_quarter), - // '▃' LOWER THREE EIGHTHS BLOCK - 0x2583 => self.draw_block(canvas, .lower, 1, three_eighths), - // '▄' LOWER HALF BLOCK - 0x2584 => self.draw_block(canvas, .lower, 1, half), - // '▅' LOWER FIVE EIGHTHS BLOCK - 0x2585 => self.draw_block(canvas, .lower, 1, five_eighths), - // '▆' LOWER THREE QUARTERS BLOCK - 0x2586 => self.draw_block(canvas, .lower, 1, three_quarters), - // '▇' LOWER SEVEN EIGHTHS BLOCK - 0x2587 => self.draw_block(canvas, .lower, 1, seven_eighths), - // '█' FULL BLOCK - 0x2588 => self.draw_full_block(canvas), - // '▉' LEFT SEVEN EIGHTHS BLOCK - 0x2589 => self.draw_block(canvas, .left, seven_eighths, 1), - // '▊' LEFT THREE QUARTERS BLOCK - 0x258a => self.draw_block(canvas, .left, three_quarters, 1), - // '▋' LEFT FIVE EIGHTHS BLOCK - 0x258b => self.draw_block(canvas, .left, five_eighths, 1), - // '▌' LEFT HALF BLOCK - 0x258c => self.draw_block(canvas, .left, half, 1), - // '▍' LEFT THREE EIGHTHS BLOCK - 0x258d => self.draw_block(canvas, .left, three_eighths, 1), - // '▎' LEFT ONE QUARTER BLOCK - 0x258e => self.draw_block(canvas, .left, one_quarter, 1), - // '▏' LEFT ONE EIGHTH BLOCK - 0x258f => self.draw_block(canvas, .left, one_eighth, 1), - - // '▐' RIGHT HALF BLOCK - 0x2590 => self.draw_block(canvas, .right, half, 1), - // '░' - 0x2591 => self.draw_light_shade(canvas), - // '▒' - 0x2592 => self.draw_medium_shade(canvas), - // '▓' - 0x2593 => self.draw_dark_shade(canvas), - // '▔' UPPER ONE EIGHTH BLOCK - 0x2594 => self.draw_block(canvas, .upper, 1, one_eighth), - // '▕' RIGHT ONE EIGHTH BLOCK - 0x2595 => self.draw_block(canvas, .right, one_eighth, 1), - // '▖' - 0x2596 => self.draw_quadrant(canvas, .{ .bl = true }), - // '▗' - 0x2597 => self.draw_quadrant(canvas, .{ .br = true }), - // '▘' - 0x2598 => self.draw_quadrant(canvas, .{ .tl = true }), - // '▙' - 0x2599 => self.draw_quadrant(canvas, .{ .tl = true, .bl = true, .br = true }), - // '▚' - 0x259a => self.draw_quadrant(canvas, .{ .tl = true, .br = true }), - // '▛' - 0x259b => self.draw_quadrant(canvas, .{ .tl = true, .tr = true, .bl = true }), - // '▜' - 0x259c => self.draw_quadrant(canvas, .{ .tl = true, .tr = true, .br = true }), - // '▝' - 0x259d => self.draw_quadrant(canvas, .{ .tr = true }), - // '▞' - 0x259e => self.draw_quadrant(canvas, .{ .tr = true, .bl = true }), - // '▟' - 0x259f => self.draw_quadrant(canvas, .{ .tr = true, .bl = true, .br = true }), - - // '◢' - 0x25e2 => self.draw_corner_triangle_shade(canvas, .br, .on), - // '◣' - 0x25e3 => self.draw_corner_triangle_shade(canvas, .bl, .on), - // '◤' - 0x25e4 => self.draw_corner_triangle_shade(canvas, .tl, .on), - // '◥' - 0x25e5 => self.draw_corner_triangle_shade(canvas, .tr, .on), - - // '◸' - 0x25f8 => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // top edge - self.rect( - canvas, - 0, - 0, - self.metrics.cell_width, - thickness_px, - ); - // left edge - self.rect( - canvas, - 0, - 0, - thickness_px, - self.metrics.cell_height -| 1, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .lower_left, - .upper_right, - ); - }, - // '◹' - 0x25f9 => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // top edge - self.rect( - canvas, - 0, - 0, - self.metrics.cell_width, - thickness_px, - ); - // right edge - self.rect( - canvas, - self.metrics.cell_width -| thickness_px, - 0, - self.metrics.cell_width, - self.metrics.cell_height -| 1, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_right, - ); - }, - // '◺' - 0x25fa => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // bottom edge - self.rect( - canvas, - 0, - self.metrics.cell_height -| thickness_px, - self.metrics.cell_width, - self.metrics.cell_height, - ); - // left edge - self.rect( - canvas, - 0, - 1, - thickness_px, - self.metrics.cell_height, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_right, - ); - }, - // '◿' - 0x25ff => { - const thickness_px = Thickness.light.height(self.metrics.box_thickness); - // bottom edge - self.rect( - canvas, - 0, - self.metrics.cell_height -| thickness_px, - self.metrics.cell_width, - self.metrics.cell_height, - ); - // right edge - self.rect( - canvas, - self.metrics.cell_width -| thickness_px, - 1, - self.metrics.cell_width, - self.metrics.cell_height, - ); - // diagonal - self.draw_cell_diagonal( - canvas, - .lower_left, - .upper_right, - ); - }, - - 0x2800...0x28ff => self.draw_braille(canvas, cp), - - 0x1fb00...0x1fb3b => self.draw_sextant(canvas, cp), - - octant_min...octant_max => self.draw_octant(canvas, cp), - - // '🬼' - 0x1fb3c => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\#.. - \\##. - )), - // '🬽' - 0x1fb3d => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\#\. - \\### - )), - // '🬾' - 0x1fb3e => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\#.. - \\#\. - \\##. - )), - // '🬿' - 0x1fb3f => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\#.. - \\##. - \\### - )), - // '🭀' - 0x1fb40 => try self.draw_smooth_mosaic(canvas, .from( - \\#.. - \\#.. - \\##. - \\##. - )), - - // '🭁' - 0x1fb41 => try self.draw_smooth_mosaic(canvas, .from( - \\/## - \\### - \\### - \\### - )), - // '🭂' - 0x1fb42 => try self.draw_smooth_mosaic(canvas, .from( - \\./# - \\### - \\### - \\### - )), - // '🭃' - 0x1fb43 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.## - \\### - \\### - )), - // '🭄' - 0x1fb44 => try self.draw_smooth_mosaic(canvas, .from( - \\..# - \\.## - \\### - \\### - )), - // '🭅' - 0x1fb45 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.## - \\.## - \\### - )), - // '🭆' - 0x1fb46 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\./# - \\### - \\### - )), - - // '🭇' - 0x1fb47 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\..# - \\.## - )), - // '🭈' - 0x1fb48 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\... - \\./# - \\### - )), - // '🭉' - 0x1fb49 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\..# - \\./# - \\.## - )), - // '🭊' - 0x1fb4a => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\..# - \\.## - \\### - )), - // '🭋' - 0x1fb4b => try self.draw_smooth_mosaic(canvas, .from( - \\..# - \\..# - \\.## - \\.## - )), - - // '🭌' - 0x1fb4c => try self.draw_smooth_mosaic(canvas, .from( - \\##\ - \\### - \\### - \\### - )), - // '🭍' - 0x1fb4d => try self.draw_smooth_mosaic(canvas, .from( - \\#\. - \\### - \\### - \\### - )), - // '🭎' - 0x1fb4e => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\##. - \\### - \\### - )), - // '🭏' - 0x1fb4f => try self.draw_smooth_mosaic(canvas, .from( - \\#.. - \\##. - \\### - \\### - )), - // '🭐' - 0x1fb50 => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\##. - \\##. - \\### - )), - // '🭑' - 0x1fb51 => try self.draw_smooth_mosaic(canvas, .from( - \\... - \\#\. - \\### - \\### - )), - - // '🭒' - 0x1fb52 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\\## - )), - // '🭓' - 0x1fb53 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\.\# - )), - // '🭔' - 0x1fb54 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\.## - \\.## - )), - // '🭕' - 0x1fb55 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\.## - \\..# - )), - // '🭖' - 0x1fb56 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\.## - \\.## - \\.## - )), - - // '🭗' - 0x1fb57 => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\#.. - \\... - \\... - )), - // '🭘' - 0x1fb58 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\#/. - \\... - \\... - )), - // '🭙' - 0x1fb59 => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\#/. - \\#.. - \\... - )), - // '🭚' - 0x1fb5a => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\##. - \\#.. - \\... - )), - // '🭛' - 0x1fb5b => try self.draw_smooth_mosaic(canvas, .from( - \\##. - \\##. - \\#.. - \\#.. - )), - - // '🭜' - 0x1fb5c => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\#/. - \\... - )), - // '🭝' - 0x1fb5d => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\##/ - )), - // '🭞' - 0x1fb5e => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\### - \\#/. - )), - // '🭟' - 0x1fb5f => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\##. - \\##. - )), - // '🭠' - 0x1fb60 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\##. - \\#.. - )), - // '🭡' - 0x1fb61 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\##. - \\##. - \\##. - )), - - // '🭢' - 0x1fb62 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\..# - \\... - \\... - )), - // '🭣' - 0x1fb63 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\.\# - \\... - \\... - )), - // '🭤' - 0x1fb64 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.\# - \\..# - \\... - )), - // '🭥' - 0x1fb65 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\.## - \\..# - \\... - )), - // '🭦' - 0x1fb66 => try self.draw_smooth_mosaic(canvas, .from( - \\.## - \\.## - \\..# - \\..# - )), - // '🭧' - 0x1fb67 => try self.draw_smooth_mosaic(canvas, .from( - \\### - \\### - \\.\# - \\... - )), - - // '🭨' - 0x1fb68 => { - try self.draw_edge_triangle(canvas, .left); - canvas.invert(); - }, - // '🭩' - 0x1fb69 => { - try self.draw_edge_triangle(canvas, .top); - canvas.invert(); - }, - // '🭪' - 0x1fb6a => { - try self.draw_edge_triangle(canvas, .right); - canvas.invert(); - }, - // '🭫' - 0x1fb6b => { - try self.draw_edge_triangle(canvas, .bottom); - canvas.invert(); - }, - // '🭬' - 0x1fb6c => try self.draw_edge_triangle(canvas, .left), - // '🭭' - 0x1fb6d => try self.draw_edge_triangle(canvas, .top), - // '🭮' - 0x1fb6e => try self.draw_edge_triangle(canvas, .right), - // '🭯' - 0x1fb6f => try self.draw_edge_triangle(canvas, .bottom), - - // '🭰' - 0x1fb70 => self.draw_vertical_one_eighth_block_n(canvas, 1), - // '🭱' - 0x1fb71 => self.draw_vertical_one_eighth_block_n(canvas, 2), - // '🭲' - 0x1fb72 => self.draw_vertical_one_eighth_block_n(canvas, 3), - // '🭳' - 0x1fb73 => self.draw_vertical_one_eighth_block_n(canvas, 4), - // '🭴' - 0x1fb74 => self.draw_vertical_one_eighth_block_n(canvas, 5), - // '🭵' - 0x1fb75 => self.draw_vertical_one_eighth_block_n(canvas, 6), - - // '🭶' - 0x1fb76 => self.draw_horizontal_one_eighth_block_n(canvas, 1), - // '🭷' - 0x1fb77 => self.draw_horizontal_one_eighth_block_n(canvas, 2), - // '🭸' - 0x1fb78 => self.draw_horizontal_one_eighth_block_n(canvas, 3), - // '🭹' - 0x1fb79 => self.draw_horizontal_one_eighth_block_n(canvas, 4), - // '🭺' - 0x1fb7a => self.draw_horizontal_one_eighth_block_n(canvas, 5), - // '🭻' - 0x1fb7b => self.draw_horizontal_one_eighth_block_n(canvas, 6), - - // '🮂' UPPER ONE QUARTER BLOCK - 0x1fb82 => self.draw_block(canvas, .upper, 1, one_quarter), - // '🮃' UPPER THREE EIGHTHS BLOCK - 0x1fb83 => self.draw_block(canvas, .upper, 1, three_eighths), - // '🮄' UPPER FIVE EIGHTHS BLOCK - 0x1fb84 => self.draw_block(canvas, .upper, 1, five_eighths), - // '🮅' UPPER THREE QUARTERS BLOCK - 0x1fb85 => self.draw_block(canvas, .upper, 1, three_quarters), - // '🮆' UPPER SEVEN EIGHTHS BLOCK - 0x1fb86 => self.draw_block(canvas, .upper, 1, seven_eighths), - - // '🭼' LEFT AND LOWER ONE EIGHTH BLOCK - 0x1fb7c => { - self.draw_block(canvas, .left, one_eighth, 1); - self.draw_block(canvas, .lower, 1, one_eighth); - }, - // '🭽' LEFT AND UPPER ONE EIGHTH BLOCK - 0x1fb7d => { - self.draw_block(canvas, .left, one_eighth, 1); - self.draw_block(canvas, .upper, 1, one_eighth); - }, - // '🭾' RIGHT AND UPPER ONE EIGHTH BLOCK - 0x1fb7e => { - self.draw_block(canvas, .right, one_eighth, 1); - self.draw_block(canvas, .upper, 1, one_eighth); - }, - // '🭿' RIGHT AND LOWER ONE EIGHTH BLOCK - 0x1fb7f => { - self.draw_block(canvas, .right, one_eighth, 1); - self.draw_block(canvas, .lower, 1, one_eighth); - }, - // '🮀' UPPER AND LOWER ONE EIGHTH BLOCK - 0x1fb80 => { - self.draw_block(canvas, .upper, 1, one_eighth); - self.draw_block(canvas, .lower, 1, one_eighth); - }, - // '🮁' - 0x1fb81 => self.draw_horizontal_one_eighth_1358_block(canvas), - - // '🮇' RIGHT ONE QUARTER BLOCK - 0x1fb87 => self.draw_block(canvas, .right, one_quarter, 1), - // '🮈' RIGHT THREE EIGHTHS BLOCK - 0x1fb88 => self.draw_block(canvas, .right, three_eighths, 1), - // '🮉' RIGHT FIVE EIGHTHS BLOCK - 0x1fb89 => self.draw_block(canvas, .right, five_eighths, 1), - // '🮊' RIGHT THREE QUARTERS BLOCK - 0x1fb8a => self.draw_block(canvas, .right, three_quarters, 1), - // '🮋' RIGHT SEVEN EIGHTHS BLOCK - 0x1fb8b => self.draw_block(canvas, .right, seven_eighths, 1), - // '🮌' - 0x1fb8c => self.draw_block_shade(canvas, .left, half, 1, .medium), - // '🮍' - 0x1fb8d => self.draw_block_shade(canvas, .right, half, 1, .medium), - // '🮎' - 0x1fb8e => self.draw_block_shade(canvas, .upper, 1, half, .medium), - // '🮏' - 0x1fb8f => self.draw_block_shade(canvas, .lower, 1, half, .medium), - - // '🮐' - 0x1fb90 => self.draw_medium_shade(canvas), - // '🮑' - 0x1fb91 => { - self.draw_medium_shade(canvas); - self.draw_block(canvas, .upper, 1, half); - }, - // '🮒' - 0x1fb92 => { - self.draw_medium_shade(canvas); - self.draw_block(canvas, .lower, 1, half); - }, - // '🮔' - 0x1fb94 => { - self.draw_medium_shade(canvas); - self.draw_block(canvas, .right, half, 1); - }, - // '🮕' - 0x1fb95 => self.draw_checkerboard_fill(canvas, 0), - // '🮖' - 0x1fb96 => self.draw_checkerboard_fill(canvas, 1), - // '🮗' - 0x1fb97 => { - self.draw_horizontal_one_eighth_block_n(canvas, 2); - self.draw_horizontal_one_eighth_block_n(canvas, 3); - self.draw_horizontal_one_eighth_block_n(canvas, 6); - self.draw_horizontal_one_eighth_block_n(canvas, 7); - }, - // '🮘' - 0x1fb98 => self.draw_upper_left_to_lower_right_fill(canvas), - // '🮙' - 0x1fb99 => self.draw_upper_right_to_lower_left_fill(canvas), - // '🮚' - 0x1fb9a => { - try self.draw_edge_triangle(canvas, .top); - try self.draw_edge_triangle(canvas, .bottom); - }, - // '🮛' - 0x1fb9b => { - try self.draw_edge_triangle(canvas, .left); - try self.draw_edge_triangle(canvas, .right); - }, - // '🮜' - 0x1fb9c => self.draw_corner_triangle_shade(canvas, .tl, .medium), - // '🮝' - 0x1fb9d => self.draw_corner_triangle_shade(canvas, .tr, .medium), - // '🮞' - 0x1fb9e => self.draw_corner_triangle_shade(canvas, .br, .medium), - // '🮟' - 0x1fb9f => self.draw_corner_triangle_shade(canvas, .bl, .medium), - - // '🮠' - 0x1fba0 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true }), - // '🮡' - 0x1fba1 => self.draw_corner_diagonal_lines(canvas, .{ .tr = true }), - // '🮢' - 0x1fba2 => self.draw_corner_diagonal_lines(canvas, .{ .bl = true }), - // '🮣' - 0x1fba3 => self.draw_corner_diagonal_lines(canvas, .{ .br = true }), - // '🮤' - 0x1fba4 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .bl = true }), - // '🮥' - 0x1fba5 => self.draw_corner_diagonal_lines(canvas, .{ .tr = true, .br = true }), - // '🮦' - 0x1fba6 => self.draw_corner_diagonal_lines(canvas, .{ .bl = true, .br = true }), - // '🮧' - 0x1fba7 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true }), - // '🮨' - 0x1fba8 => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .br = true }), - // '🮩' - 0x1fba9 => self.draw_corner_diagonal_lines(canvas, .{ .tr = true, .bl = true }), - // '🮪' - 0x1fbaa => self.draw_corner_diagonal_lines(canvas, .{ .tr = true, .bl = true, .br = true }), - // '🮫' - 0x1fbab => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .bl = true, .br = true }), - // '🮬' - 0x1fbac => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .br = true }), - // '🮭' - 0x1fbad => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .bl = true }), - // '🮮' - 0x1fbae => self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .bl = true, .br = true }), - // '🮯' - 0x1fbaf => self.draw_lines(canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }), - - // '🮽' - 0x1fbbd => { - self.draw_light_diagonal_cross(canvas); - canvas.invert(); - }, - // '🮾' - 0x1fbbe => { - self.draw_corner_diagonal_lines(canvas, .{ .br = true }); - canvas.invert(); - }, - // '🮿' - 0x1fbbf => { - self.draw_corner_diagonal_lines(canvas, .{ .tl = true, .tr = true, .bl = true, .br = true }); - canvas.invert(); - }, - - // '🯎' - 0x1fbce => self.draw_block(canvas, .left, two_thirds, 1), - // '🯏' - 0x1fbcf => self.draw_block(canvas, .left, one_third, 1), - // '🯐' - 0x1fbd0 => self.draw_cell_diagonal( - canvas, - .middle_right, - .lower_left, - ), - // '🯑' - 0x1fbd1 => self.draw_cell_diagonal( - canvas, - .upper_right, - .middle_left, - ), - // '🯒' - 0x1fbd2 => self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_right, - ), - // '🯓' - 0x1fbd3 => self.draw_cell_diagonal( - canvas, - .middle_left, - .lower_right, - ), - // '🯔' - 0x1fbd4 => self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_center, - ), - // '🯕' - 0x1fbd5 => self.draw_cell_diagonal( - canvas, - .upper_center, - .lower_right, - ), - // '🯖' - 0x1fbd6 => self.draw_cell_diagonal( - canvas, - .upper_right, - .lower_center, - ), - // '🯗' - 0x1fbd7 => self.draw_cell_diagonal( - canvas, - .upper_center, - .lower_left, - ), - // '🯘' - 0x1fbd8 => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .upper_right, - ); - }, - // '🯙' - 0x1fbd9 => { - self.draw_cell_diagonal( - canvas, - .upper_right, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .lower_right, - ); - }, - // '🯚' - 0x1fbda => { - self.draw_cell_diagonal( - canvas, - .lower_left, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .lower_right, - ); - }, - // '🯛' - 0x1fbdb => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_center, - ); - self.draw_cell_diagonal( - canvas, - .middle_center, - .lower_left, - ); - }, - // '🯜' - 0x1fbdc => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .lower_center, - ); - self.draw_cell_diagonal( - canvas, - .lower_center, - .upper_right, - ); - }, - // '🯝' - 0x1fbdd => { - self.draw_cell_diagonal( - canvas, - .upper_right, - .middle_left, - ); - self.draw_cell_diagonal( - canvas, - .middle_left, - .lower_right, - ); - }, - // '🯞' - 0x1fbde => { - self.draw_cell_diagonal( - canvas, - .lower_left, - .upper_center, - ); - self.draw_cell_diagonal( - canvas, - .upper_center, - .lower_right, - ); - }, - // '🯟' - 0x1fbdf => { - self.draw_cell_diagonal( - canvas, - .upper_left, - .middle_right, - ); - self.draw_cell_diagonal( - canvas, - .middle_right, - .lower_left, - ); - }, - - // '🯠' - 0x1fbe0 => self.draw_circle(canvas, .top, false), - // '🯡' - 0x1fbe1 => self.draw_circle(canvas, .right, false), - // '🯢' - 0x1fbe2 => self.draw_circle(canvas, .bottom, false), - // '🯣' - 0x1fbe3 => self.draw_circle(canvas, .left, false), - // '🯤' - 0x1fbe4 => self.draw_block(canvas, .upper_center, 0.5, 0.5), - // '🯥' - 0x1fbe5 => self.draw_block(canvas, .lower_center, 0.5, 0.5), - // '🯦' - 0x1fbe6 => self.draw_block(canvas, .middle_left, 0.5, 0.5), - // '🯧' - 0x1fbe7 => self.draw_block(canvas, .middle_right, 0.5, 0.5), - // '🯨' - 0x1fbe8 => self.draw_circle(canvas, .top, true), - // '🯩' - 0x1fbe9 => self.draw_circle(canvas, .right, true), - // '🯪' - 0x1fbea => self.draw_circle(canvas, .bottom, true), - // '🯫' - 0x1fbeb => self.draw_circle(canvas, .left, true), - // '🯬' - 0x1fbec => self.draw_circle(canvas, .top_right, true), - // '🯭' - 0x1fbed => self.draw_circle(canvas, .bottom_left, true), - // '🯮' - 0x1fbee => self.draw_circle(canvas, .bottom_right, true), - // '🯯' - 0x1fbef => self.draw_circle(canvas, .top_left, true), - - // (Below:) - // Branch drawing character set, used for drawing git-like - // graphs in the terminal. Originally implemented in Kitty. - // Ref: - // - https://github.com/kovidgoyal/kitty/pull/7681 - // - https://github.com/kovidgoyal/kitty/pull/7805 - // NOTE: Kitty is GPL licensed, and its code was not referenced - // for these characters, only the loose specification of - // the character set in the pull request descriptions. - // - // TODO(qwerasd): This should be in another file, but really the - // general organization of the sprite font code - // needs to be reworked eventually. - // - //           - //                     - //                     - //             - - // '' - 0x0f5d0 => self.hline_middle(canvas, .light), - // '' - 0x0f5d1 => self.vline_middle(canvas, .light), - // '' - 0x0f5d2 => self.draw_fading_line(canvas, .right, .light), - // '' - 0x0f5d3 => self.draw_fading_line(canvas, .left, .light), - // '' - 0x0f5d4 => self.draw_fading_line(canvas, .bottom, .light), - // '' - 0x0f5d5 => self.draw_fading_line(canvas, .top, .light), - // '' - 0x0f5d6 => try self.draw_arc(canvas, .br, .light), - // '' - 0x0f5d7 => try self.draw_arc(canvas, .bl, .light), - // '' - 0x0f5d8 => try self.draw_arc(canvas, .tr, .light), - // '' - 0x0f5d9 => try self.draw_arc(canvas, .tl, .light), - // '' - 0x0f5da => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - }, - // '' - 0x0f5db => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5dc => { - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5dd => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - }, - // '' - 0x0f5de => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5df => { - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .bl, .light); - }, - - // '' - 0x0f5e0 => { - try self.draw_arc(canvas, .bl, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e1 => { - try self.draw_arc(canvas, .br, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e2 => { - try self.draw_arc(canvas, .br, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5e3 => { - try self.draw_arc(canvas, .tl, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e4 => { - try self.draw_arc(canvas, .tr, .light); - self.hline_middle(canvas, .light); - }, - // '' - 0x0f5e5 => { - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .tl, .light); - }, - // '' - 0x0f5e6 => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .tr, .light); - }, - // '' - 0x0f5e7 => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .bl, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5e8 => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .bl, .light); - try self.draw_arc(canvas, .tl, .light); - }, - // '' - 0x0f5e9 => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5ea => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5eb => { - self.vline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5ec => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .tl, .light); - try self.draw_arc(canvas, .br, .light); - }, - // '' - 0x0f5ed => { - self.hline_middle(canvas, .light); - try self.draw_arc(canvas, .tr, .light); - try self.draw_arc(canvas, .bl, .light); - }, - // '' - 0x0f5ee => self.draw_branch_node(canvas, .{ .filled = true }, .light), - // '' - 0x0f5ef => self.draw_branch_node(canvas, .{}, .light), - - // '' - 0x0f5f0 => self.draw_branch_node(canvas, .{ - .right = true, - .filled = true, - }, .light), - // '' - 0x0f5f1 => self.draw_branch_node(canvas, .{ - .right = true, - }, .light), - // '' - 0x0f5f2 => self.draw_branch_node(canvas, .{ - .left = true, - .filled = true, - }, .light), - // '' - 0x0f5f3 => self.draw_branch_node(canvas, .{ - .left = true, - }, .light), - // '' - 0x0f5f4 => self.draw_branch_node(canvas, .{ - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f5f5 => self.draw_branch_node(canvas, .{ - .left = true, - .right = true, - }, .light), - // '' - 0x0f5f6 => self.draw_branch_node(canvas, .{ - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5f7 => self.draw_branch_node(canvas, .{ - .down = true, - }, .light), - // '' - 0x0f5f8 => self.draw_branch_node(canvas, .{ - .up = true, - .filled = true, - }, .light), - // '' - 0x0f5f9 => self.draw_branch_node(canvas, .{ - .up = true, - }, .light), - // '' - 0x0f5fa => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5fb => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - }, .light), - // '' - 0x0f5fc => self.draw_branch_node(canvas, .{ - .right = true, - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5fd => self.draw_branch_node(canvas, .{ - .right = true, - .down = true, - }, .light), - // '' - 0x0f5fe => self.draw_branch_node(canvas, .{ - .left = true, - .down = true, - .filled = true, - }, .light), - // '' - 0x0f5ff => self.draw_branch_node(canvas, .{ - .left = true, - .down = true, - }, .light), - - // '' - 0x0f600 => self.draw_branch_node(canvas, .{ - .up = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f601 => self.draw_branch_node(canvas, .{ - .up = true, - .right = true, - }, .light), - // '' - 0x0f602 => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - .filled = true, - }, .light), - // '' - 0x0f603 => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - }, .light), - // '' - 0x0f604 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f605 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .right = true, - }, .light), - // '' - 0x0f606 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - .filled = true, - }, .light), - // '' - 0x0f607 => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - }, .light), - // '' - 0x0f608 => self.draw_branch_node(canvas, .{ - .down = true, - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f609 => self.draw_branch_node(canvas, .{ - .down = true, - .left = true, - .right = true, - }, .light), - // '' - 0x0f60a => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f60b => self.draw_branch_node(canvas, .{ - .up = true, - .left = true, - .right = true, - }, .light), - // '' - 0x0f60c => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - .right = true, - .filled = true, - }, .light), - // '' - 0x0f60d => self.draw_branch_node(canvas, .{ - .up = true, - .down = true, - .left = true, - .right = true, - }, .light), - - // '𜰡' - SEPARATED BLOCK QUADRANT-1 - 0x1cc21 => try self.draw_separated_block_quadrant(canvas, "1"), - // '𜰢' - SEPARATED BLOCK QUADRANT-2 - 0x1cc22 => try self.draw_separated_block_quadrant(canvas, "2"), - // '𜰣' - SEPARATED BLOCK QUADRANT-12 - 0x1cc23 => try self.draw_separated_block_quadrant(canvas, "12"), - // '𜰤' - SEPARATED BLOCK QUADRANT-3 - 0x1cc24 => try self.draw_separated_block_quadrant(canvas, "3"), - // '𜰥' - SEPARATED BLOCK QUADRANT-13 - 0x1cc25 => try self.draw_separated_block_quadrant(canvas, "13"), - // '𜰦' - SEPARATED BLOCK QUADRANT-23 - 0x1cc26 => try self.draw_separated_block_quadrant(canvas, "23"), - // '𜰧' - SEPARATED BLOCK QUADRANT-123 - 0x1cc27 => try self.draw_separated_block_quadrant(canvas, "123"), - // '𜰨' - SEPARATED BLOCK QUADRANT-4 - 0x1cc28 => try self.draw_separated_block_quadrant(canvas, "4"), - // '𜰩' - SEPARATED BLOCK QUADRANT-14 - 0x1cc29 => try self.draw_separated_block_quadrant(canvas, "14"), - // '𜰪' - SEPARATED BLOCK QUADRANT-24 - 0x1cc2a => try self.draw_separated_block_quadrant(canvas, "24"), - // '𜰫' - SEPARATED BLOCK QUADRANT-124 - 0x1cc2b => try self.draw_separated_block_quadrant(canvas, "124"), - // '𜰬' - SEPARATED BLOCK QUADRANT-34 - 0x1cc2c => try self.draw_separated_block_quadrant(canvas, "34"), - // '𜰭' - SEPARATED BLOCK QUADRANT-134 - 0x1cc2d => try self.draw_separated_block_quadrant(canvas, "134"), - // '𜰮' - SEPARATED BLOCK QUADRANT-234 - 0x1cc2e => try self.draw_separated_block_quadrant(canvas, "234"), - // '𜰯' - SEPARATED BLOCK QUADRANT-1234 - 0x1cc2f => try self.draw_separated_block_quadrant(canvas, "1234"), - - else => return error.InvalidCodepoint, - } -} - -fn draw_lines( - self: Box, - canvas: *font.sprite.Canvas, - lines: Lines, -) void { - const light_px = Thickness.light.height(self.metrics.box_thickness); - const heavy_px = Thickness.heavy.height(self.metrics.box_thickness); - - // Top of light horizontal strokes - const h_light_top = (self.metrics.cell_height -| light_px) / 2; - // Bottom of light horizontal strokes - const h_light_bottom = h_light_top +| light_px; - - // Top of heavy horizontal strokes - const h_heavy_top = (self.metrics.cell_height -| heavy_px) / 2; - // Bottom of heavy horizontal strokes - const h_heavy_bottom = h_heavy_top +| heavy_px; - - // Top of the top doubled horizontal stroke (bottom is `h_light_top`) - const h_double_top = h_light_top -| light_px; - // Bottom of the bottom doubled horizontal stroke (top is `h_light_bottom`) - const h_double_bottom = h_light_bottom +| light_px; - - // Left of light vertical strokes - const v_light_left = (self.metrics.cell_width -| light_px) / 2; - // Right of light vertical strokes - const v_light_right = v_light_left +| light_px; - - // Left of heavy vertical strokes - const v_heavy_left = (self.metrics.cell_width -| heavy_px) / 2; - // Right of heavy vertical strokes - const v_heavy_right = v_heavy_left +| heavy_px; - - // Left of the left doubled vertical stroke (right is `v_light_left`) - const v_double_left = v_light_left -| light_px; - // Right of the right doubled vertical stroke (left is `v_light_right`) - const v_double_right = v_light_right +| light_px; - - // The bottom of the up line - const up_bottom = if (lines.left == .heavy or lines.right == .heavy) - h_heavy_bottom - else if (lines.left != lines.right or lines.down == lines.up) - if (lines.left == .double or lines.right == .double) - h_double_bottom - else - h_light_bottom - else if (lines.left == .none and lines.right == .none) - h_light_bottom - else - h_light_top; - - // The top of the down line - const down_top = if (lines.left == .heavy or lines.right == .heavy) - h_heavy_top - else if (lines.left != lines.right or lines.up == lines.down) - if (lines.left == .double or lines.right == .double) - h_double_top - else - h_light_top - else if (lines.left == .none and lines.right == .none) - h_light_top - else - h_light_bottom; - - // The right of the left line - const left_right = if (lines.up == .heavy or lines.down == .heavy) - v_heavy_right - else if (lines.up != lines.down or lines.left == lines.right) - if (lines.up == .double or lines.down == .double) - v_double_right - else - v_light_right - else if (lines.up == .none and lines.down == .none) - v_light_right - else - v_light_left; - - // The left of the right line - const right_left = if (lines.up == .heavy or lines.down == .heavy) - v_heavy_left - else if (lines.up != lines.down or lines.right == lines.left) - if (lines.up == .double or lines.down == .double) - v_double_left - else - v_light_left - else if (lines.up == .none and lines.down == .none) - v_light_left - else - v_light_right; - - switch (lines.up) { - .none => {}, - .light => self.rect(canvas, v_light_left, 0, v_light_right, up_bottom), - .heavy => self.rect(canvas, v_heavy_left, 0, v_heavy_right, up_bottom), - .double => { - const left_bottom = if (lines.left == .double) h_light_top else up_bottom; - const right_bottom = if (lines.right == .double) h_light_top else up_bottom; - - self.rect(canvas, v_double_left, 0, v_light_left, left_bottom); - self.rect(canvas, v_light_right, 0, v_double_right, right_bottom); - }, - } - - switch (lines.right) { - .none => {}, - .light => self.rect(canvas, right_left, h_light_top, self.metrics.cell_width, h_light_bottom), - .heavy => self.rect(canvas, right_left, h_heavy_top, self.metrics.cell_width, h_heavy_bottom), - .double => { - const top_left = if (lines.up == .double) v_light_right else right_left; - const bottom_left = if (lines.down == .double) v_light_right else right_left; - - self.rect(canvas, top_left, h_double_top, self.metrics.cell_width, h_light_top); - self.rect(canvas, bottom_left, h_light_bottom, self.metrics.cell_width, h_double_bottom); - }, - } - - switch (lines.down) { - .none => {}, - .light => self.rect(canvas, v_light_left, down_top, v_light_right, self.metrics.cell_height), - .heavy => self.rect(canvas, v_heavy_left, down_top, v_heavy_right, self.metrics.cell_height), - .double => { - const left_top = if (lines.left == .double) h_light_bottom else down_top; - const right_top = if (lines.right == .double) h_light_bottom else down_top; - - self.rect(canvas, v_double_left, left_top, v_light_left, self.metrics.cell_height); - self.rect(canvas, v_light_right, right_top, v_double_right, self.metrics.cell_height); - }, - } - - switch (lines.left) { - .none => {}, - .light => self.rect(canvas, 0, h_light_top, left_right, h_light_bottom), - .heavy => self.rect(canvas, 0, h_heavy_top, left_right, h_heavy_bottom), - .double => { - const top_right = if (lines.up == .double) v_light_left else left_right; - const bottom_right = if (lines.down == .double) v_light_left else left_right; - - self.rect(canvas, 0, h_double_top, top_right, h_light_top); - self.rect(canvas, 0, h_light_bottom, bottom_right, h_double_bottom); - }, - } -} - -fn draw_light_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 3, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 3, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 3, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 3, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 4, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 4, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 4, - Thickness.light.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_heavy_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 4, - Thickness.heavy.height(self.metrics.box_thickness), - @max(4, Thickness.light.height(self.metrics.box_thickness)), - ); -} - -fn draw_light_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 2, - Thickness.light.height(self.metrics.box_thickness), - Thickness.light.height(self.metrics.box_thickness), - ); -} - -fn draw_heavy_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_horizontal( - canvas, - 2, - Thickness.heavy.height(self.metrics.box_thickness), - Thickness.heavy.height(self.metrics.box_thickness), - ); -} - -fn draw_light_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 2, - Thickness.light.height(self.metrics.box_thickness), - Thickness.heavy.height(self.metrics.box_thickness), - ); -} - -fn draw_heavy_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_dash_vertical( - canvas, - 2, - Thickness.heavy.height(self.metrics.box_thickness), - Thickness.heavy.height(self.metrics.box_thickness), - ); -} - -fn draw_light_diagonal_upper_right_to_lower_left(self: Box, canvas: *font.sprite.Canvas) void { - canvas.line(.{ - .p0 = .{ .x = @floatFromInt(self.metrics.cell_width), .y = 0 }, - .p1 = .{ .x = 0, .y = @floatFromInt(self.metrics.cell_height) }, - }, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {}; -} - -fn draw_light_diagonal_upper_left_to_lower_right(self: Box, canvas: *font.sprite.Canvas) void { - canvas.line(.{ - .p0 = .{ .x = 0, .y = 0 }, - .p1 = .{ - .x = @floatFromInt(self.metrics.cell_width), - .y = @floatFromInt(self.metrics.cell_height), - }, - }, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {}; -} - -fn draw_light_diagonal_cross(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_light_diagonal_upper_right_to_lower_left(canvas); - self.draw_light_diagonal_upper_left_to_lower_right(canvas); -} - -fn draw_block( - self: Box, - canvas: *font.sprite.Canvas, - comptime alignment: Alignment, - comptime width: f64, - comptime height: f64, -) void { - self.draw_block_shade(canvas, alignment, width, height, .on); -} - -fn draw_block_shade( - self: Box, - canvas: *font.sprite.Canvas, - comptime alignment: Alignment, - comptime width: f64, - comptime height: f64, - comptime shade: Shade, -) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - - const w: u32 = @intFromFloat(@round(float_width * width)); - const h: u32 = @intFromFloat(@round(float_height * height)); - - const x = switch (alignment.horizontal) { - .left => 0, - .right => self.metrics.cell_width - w, - .center => (self.metrics.cell_width - w) / 2, - }; - const y = switch (alignment.vertical) { - .top => 0, - .bottom => self.metrics.cell_height - h, - .middle => (self.metrics.cell_height - h) / 2, - }; - - canvas.rect(.{ - .x = x, - .y = y, - .width = w, - .height = h, - }, @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade)))); -} - -fn draw_corner_triangle_shade( - self: Box, - canvas: *font.sprite.Canvas, - comptime corner: Corner, - comptime shade: Shade, -) void { - const x0, const y0, const x1, const y1, const x2, const y2 = switch (corner) { - .tl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, 0 }, - .tr => .{ 0, 0, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 }, - .bl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height }, - .br => .{ 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 }, - }; - - canvas.triangle(.{ - .p0 = .{ .x = @floatFromInt(x0), .y = @floatFromInt(y0) }, - .p1 = .{ .x = @floatFromInt(x1), .y = @floatFromInt(y1) }, - .p2 = .{ .x = @floatFromInt(x2), .y = @floatFromInt(y2) }, - }, @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade)))) catch {}; -} - -fn draw_full_block(self: Box, canvas: *font.sprite.Canvas) void { - self.rect(canvas, 0, 0, self.metrics.cell_width, self.metrics.cell_height); -} - -fn draw_vertical_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void { - const x = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_width)) / 8))); - const w = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 8))); - self.rect(canvas, x, 0, x + w, self.metrics.cell_height); -} - -fn draw_checkerboard_fill(self: Box, canvas: *font.sprite.Canvas, parity: u1) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const x_size: usize = 4; - const y_size: usize = @intFromFloat(@round(4 * (float_height / float_width))); - for (0..x_size) |x| { - const x0 = (self.metrics.cell_width * x) / x_size; - const x1 = (self.metrics.cell_width * (x + 1)) / x_size; - for (0..y_size) |y| { - const y0 = (self.metrics.cell_height * y) / y_size; - const y1 = (self.metrics.cell_height * (y + 1)) / y_size; - if ((x + y) % 2 == parity) { - canvas.rect(.{ - .x = @intCast(x0), - .y = @intCast(y0), - .width = @intCast(x1 -| x0), - .height = @intCast(y1 -| y0), - }, .on); - } - } - } -} - -fn draw_upper_left_to_lower_right_fill(self: Box, canvas: *font.sprite.Canvas) void { - const thick_px = Thickness.light.height(self.metrics.box_thickness); - const line_count = self.metrics.cell_width / (2 * thick_px); - - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - const stride = @round(float_width / @as(f64, @floatFromInt(line_count))); - - for (0..line_count * 2 + 1) |_i| { - const i = @as(i32, @intCast(_i)) - @as(i32, @intCast(line_count)); - const top_x = @as(f64, @floatFromInt(i)) * stride; - const bottom_x = float_width + top_x; - canvas.line(.{ - .p0 = .{ .x = top_x, .y = 0 }, - .p1 = .{ .x = bottom_x, .y = float_height }, - }, float_thick, .on) catch {}; - } -} - -fn draw_upper_right_to_lower_left_fill(self: Box, canvas: *font.sprite.Canvas) void { - const thick_px = Thickness.light.height(self.metrics.box_thickness); - const line_count = self.metrics.cell_width / (2 * thick_px); - - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - const stride = @round(float_width / @as(f64, @floatFromInt(line_count))); - - for (0..line_count * 2 + 1) |_i| { - const i = @as(i32, @intCast(_i)) - @as(i32, @intCast(line_count)); - const bottom_x = @as(f64, @floatFromInt(i)) * stride; - const top_x = float_width + bottom_x; - canvas.line(.{ - .p0 = .{ .x = top_x, .y = 0 }, - .p1 = .{ .x = bottom_x, .y = float_height }, - }, float_thick, .on) catch {}; - } -} - -fn draw_corner_diagonal_lines( - self: Box, - canvas: *font.sprite.Canvas, - comptime corners: Quads, -) void { - const thick_px = Thickness.light.height(self.metrics.box_thickness); - - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - const center_x: f64 = @floatFromInt(self.metrics.cell_width / 2 + self.metrics.cell_width % 2); - const center_y: f64 = @floatFromInt(self.metrics.cell_height / 2 + self.metrics.cell_height % 2); - - if (corners.tl) canvas.line(.{ - .p0 = .{ .x = center_x, .y = 0 }, - .p1 = .{ .x = 0, .y = center_y }, - }, float_thick, .on) catch {}; - - if (corners.tr) canvas.line(.{ - .p0 = .{ .x = center_x, .y = 0 }, - .p1 = .{ .x = float_width, .y = center_y }, - }, float_thick, .on) catch {}; - - if (corners.bl) canvas.line(.{ - .p0 = .{ .x = center_x, .y = float_height }, - .p1 = .{ .x = 0, .y = center_y }, - }, float_thick, .on) catch {}; - - if (corners.br) canvas.line(.{ - .p0 = .{ .x = center_x, .y = float_height }, - .p1 = .{ .x = float_width, .y = center_y }, - }, float_thick, .on) catch {}; -} - -fn draw_cell_diagonal( - self: Box, - canvas: *font.sprite.Canvas, - comptime from: Alignment, - comptime to: Alignment, -) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - - const x0: f64 = switch (from.horizontal) { - .left => 0, - .right => float_width, - .center => float_width / 2, - }; - const y0: f64 = switch (from.vertical) { - .top => 0, - .bottom => float_height, - .middle => float_height / 2, - }; - const x1: f64 = switch (to.horizontal) { - .left => 0, - .right => float_width, - .center => float_width / 2, - }; - const y1: f64 = switch (to.vertical) { - .top => 0, - .bottom => float_height, - .middle => float_height / 2, - }; - - self.draw_line( - canvas, - .{ .x = x0, .y = y0 }, - .{ .x = x1, .y = y1 }, - .light, - ) catch {}; -} - -fn draw_fading_line( - self: Box, - canvas: *font.sprite.Canvas, - comptime to: Edge, - comptime thickness: Thickness, -) void { - const thick_px = thickness.height(self.metrics.box_thickness); - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - - // Top of horizontal strokes - const h_top = (self.metrics.cell_height -| thick_px) / 2; - // Bottom of horizontal strokes - const h_bottom = h_top +| thick_px; - // Left of vertical strokes - const v_left = (self.metrics.cell_width -| thick_px) / 2; - // Right of vertical strokes - const v_right = v_left +| thick_px; - - // If we're fading to the top or left, we start with 0.0 - // and increment up as we progress, otherwise we start - // at 255.0 and increment down (negative). - var color: f64 = switch (to) { - .top, .left => 0.0, - .bottom, .right => 255.0, - }; - const inc: f64 = 255.0 / switch (to) { - .top => float_height, - .bottom => -float_height, - .left => float_width, - .right => -float_width, - }; - - switch (to) { - .top, .bottom => { - for (0..self.metrics.cell_height) |y| { - for (v_left..v_right) |x| { - canvas.pixel( - @intCast(x), - @intCast(y), - @enumFromInt(@as(u8, @intFromFloat(@round(color)))), - ); - } - color += inc; - } - }, - .left, .right => { - for (0..self.metrics.cell_width) |x| { - for (h_top..h_bottom) |y| { - canvas.pixel( - @intCast(x), - @intCast(y), - @enumFromInt(@as(u8, @intFromFloat(@round(color)))), - ); - } - color += inc; - } - }, - } -} - -fn draw_branch_node( - self: Box, - canvas: *font.sprite.Canvas, - node: BranchNode, - comptime thickness: Thickness, -) void { - const thick_px = thickness.height(self.metrics.box_thickness); - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - - // Top of horizontal strokes - const h_top = (self.metrics.cell_height -| thick_px) / 2; - // Bottom of horizontal strokes - const h_bottom = h_top +| thick_px; - // Left of vertical strokes - const v_left = (self.metrics.cell_width -| thick_px) / 2; - // Right of vertical strokes - const v_right = v_left +| thick_px; - - // We calculate the center of the circle this way - // to ensure it aligns with box drawing characters - // since the lines are sometimes off center to - // make sure they aren't split between pixels. - const cx: f64 = @as(f64, @floatFromInt(v_left)) + float_thick / 2; - const cy: f64 = @as(f64, @floatFromInt(h_top)) + float_thick / 2; - // The radius needs to be the smallest distance from the center to an edge. - const r: f64 = @min( - @min(cx, cy), - @min(float_width - cx, float_height - cy), - ); - - var ctx = canvas.getContext(); - defer ctx.deinit(); - ctx.setSource(.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }); - ctx.setLineWidth(float_thick); - - // These @intFromFloat casts shouldn't ever fail since r can never - // be greater than cx or cy, so when subtracting it from them the - // result can never be negative. - if (node.up) - self.rect(canvas, v_left, 0, v_right, @intFromFloat(@ceil(cy - r))); - if (node.right) - self.rect(canvas, @intFromFloat(@floor(cx + r)), h_top, self.metrics.cell_width, h_bottom); - if (node.down) - self.rect(canvas, v_left, @intFromFloat(@floor(cy + r)), v_right, self.metrics.cell_height); - if (node.left) - self.rect(canvas, 0, h_top, @intFromFloat(@ceil(cx - r)), h_bottom); - - if (node.filled) { - ctx.arc(cx, cy, r, 0, std.math.pi * 2) catch return; - ctx.closePath() catch return; - ctx.fill() catch return; - } else { - ctx.arc(cx, cy, r - float_thick / 2, 0, std.math.pi * 2) catch return; - ctx.closePath() catch return; - ctx.stroke() catch return; - } -} - -fn draw_circle( - self: Box, - canvas: *font.sprite.Canvas, - comptime position: Alignment, - comptime filled: bool, -) void { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - - const x: f64 = switch (position.horizontal) { - .left => 0, - .right => float_width, - .center => float_width / 2, - }; - const y: f64 = switch (position.vertical) { - .top => 0, - .bottom => float_height, - .middle => float_height / 2, - }; - const r: f64 = 0.5 * @min(float_width, float_height); - - var ctx = canvas.getContext(); - defer ctx.deinit(); - ctx.setSource(.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }); - ctx.setLineWidth( - @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), - ); - - if (filled) { - ctx.arc(x, y, r, 0, std.math.pi * 2) catch return; - ctx.closePath() catch return; - ctx.fill() catch return; - } else { - ctx.arc(x, y, r - ctx.line_width / 2, 0, std.math.pi * 2) catch return; - ctx.closePath() catch return; - ctx.stroke() catch return; - } -} - -fn draw_line( - self: Box, - canvas: *font.sprite.Canvas, - p0: font.sprite.Point(f64), - p1: font.sprite.Point(f64), - comptime thickness: Thickness, -) !void { - canvas.line( - .{ .p0 = p0, .p1 = p1 }, - @floatFromInt(thickness.height(self.metrics.box_thickness)), - .on, - ) catch {}; -} - -fn draw_shade(self: Box, canvas: *font.sprite.Canvas, v: u16) void { - canvas.rect((font.sprite.Box(u32){ - .p0 = .{ .x = 0, .y = 0 }, - .p1 = .{ - .x = self.metrics.cell_width, - .y = self.metrics.cell_height, - }, - }).rect(), @as(font.sprite.Color, @enumFromInt(v))); -} - -fn draw_light_shade(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_shade(canvas, 0x40); -} - -fn draw_medium_shade(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_shade(canvas, 0x80); -} - -fn draw_dark_shade(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_shade(canvas, 0xc0); -} - -fn draw_horizontal_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void { - const h = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 8))); - const y = @min( - self.metrics.cell_height -| h, - @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_height)) / 8))), - ); - self.rect(canvas, 0, y, self.metrics.cell_width, y + h); -} - -fn draw_horizontal_one_eighth_1358_block(self: Box, canvas: *font.sprite.Canvas) void { - self.draw_horizontal_one_eighth_block_n(canvas, 0); - self.draw_horizontal_one_eighth_block_n(canvas, 2); - self.draw_horizontal_one_eighth_block_n(canvas, 4); - self.draw_horizontal_one_eighth_block_n(canvas, 7); -} - -fn draw_quadrant(self: Box, canvas: *font.sprite.Canvas, comptime quads: Quads) void { - const center_x = self.metrics.cell_width / 2 + self.metrics.cell_width % 2; - const center_y = self.metrics.cell_height / 2 + self.metrics.cell_height % 2; - - if (quads.tl) self.rect(canvas, 0, 0, center_x, center_y); - if (quads.tr) self.rect(canvas, center_x, 0, self.metrics.cell_width, center_y); - if (quads.bl) self.rect(canvas, 0, center_y, center_x, self.metrics.cell_height); - if (quads.br) self.rect(canvas, center_x, center_y, self.metrics.cell_width, self.metrics.cell_height); -} - -fn draw_braille(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { - var w: u32 = @min(self.metrics.cell_width / 4, self.metrics.cell_height / 8); - var x_spacing: u32 = self.metrics.cell_width / 4; - var y_spacing: u32 = self.metrics.cell_height / 8; - var x_margin: u32 = x_spacing / 2; - var y_margin: u32 = y_spacing / 2; - - var x_px_left: u32 = self.metrics.cell_width - 2 * x_margin - x_spacing - 2 * w; - var y_px_left: u32 = self.metrics.cell_height - 2 * y_margin - 3 * y_spacing - 4 * w; - - // First, try hard to ensure the DOT width is non-zero - if (x_px_left >= 2 and y_px_left >= 4 and w == 0) { - w += 1; - x_px_left -= 2; - y_px_left -= 4; - } - - // Second, prefer a non-zero margin - if (x_px_left >= 2 and x_margin == 0) { - x_margin = 1; - x_px_left -= 2; - } - if (y_px_left >= 2 and y_margin == 0) { - y_margin = 1; - y_px_left -= 2; - } - - // Third, increase spacing - if (x_px_left >= 1) { - x_spacing += 1; - x_px_left -= 1; - } - if (y_px_left >= 3) { - y_spacing += 1; - y_px_left -= 3; - } - - // Fourth, margins (“spacing”, but on the sides) - if (x_px_left >= 2) { - x_margin += 1; - x_px_left -= 2; - } - if (y_px_left >= 2) { - y_margin += 1; - y_px_left -= 2; - } - - // Last - increase dot width - if (x_px_left >= 2 and y_px_left >= 4) { - w += 1; - x_px_left -= 2; - y_px_left -= 4; - } - - assert(x_px_left <= 1 or y_px_left <= 1); - assert(2 * x_margin + 2 * w + x_spacing <= self.metrics.cell_width); - assert(2 * y_margin + 4 * w + 3 * y_spacing <= self.metrics.cell_height); - - const x = [2]u32{ x_margin, x_margin + w + x_spacing }; - const y = y: { - var y: [4]u32 = undefined; - y[0] = y_margin; - y[1] = y[0] + w + y_spacing; - y[2] = y[1] + w + y_spacing; - y[3] = y[2] + w + y_spacing; - break :y y; - }; - - assert(cp >= 0x2800); - assert(cp <= 0x28ff); - const sym = cp - 0x2800; - - // Left side - if (sym & 1 > 0) - self.rect(canvas, x[0], y[0], x[0] + w, y[0] + w); - if (sym & 2 > 0) - self.rect(canvas, x[0], y[1], x[0] + w, y[1] + w); - if (sym & 4 > 0) - self.rect(canvas, x[0], y[2], x[0] + w, y[2] + w); - - // Right side - if (sym & 8 > 0) - self.rect(canvas, x[1], y[0], x[1] + w, y[0] + w); - if (sym & 16 > 0) - self.rect(canvas, x[1], y[1], x[1] + w, y[1] + w); - if (sym & 32 > 0) - self.rect(canvas, x[1], y[2], x[1] + w, y[2] + w); - - // 8-dot patterns - if (sym & 64 > 0) - self.rect(canvas, x[0], y[3], x[0] + w, y[3] + w); - if (sym & 128 > 0) - self.rect(canvas, x[1], y[3], x[1] + w, y[3] + w); -} - -fn draw_sextant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { - const Sextants = packed struct(u6) { - tl: bool, - tr: bool, - ml: bool, - mr: bool, - bl: bool, - br: bool, - }; - - assert(cp >= 0x1fb00 and cp <= 0x1fb3b); - const idx = cp - 0x1fb00; - const sex: Sextants = @bitCast(@as(u6, @intCast( - idx + (idx / 0x14) + 1, - ))); - - const x_halfs = self.xHalfs(); - const y_thirds = self.yThirds(); - - if (sex.tl) self.rect(canvas, 0, 0, x_halfs[0], y_thirds[0]); - if (sex.tr) self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_thirds[0]); - if (sex.ml) self.rect(canvas, 0, y_thirds[1], x_halfs[0], y_thirds[2]); - if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[1], self.metrics.cell_width, y_thirds[2]); - if (sex.bl) self.rect(canvas, 0, y_thirds[3], x_halfs[0], self.metrics.cell_height); - if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[3], self.metrics.cell_width, self.metrics.cell_height); -} - -fn draw_octant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { - assert(cp >= octant_min and cp <= octant_max); - - // Octant representation. We use the funny numeric string keys - // so its easier to parse the actual name used in the Symbols for - // Legacy Computing spec. - const Octant = packed struct(u8) { - @"1": bool = false, - @"2": bool = false, - @"3": bool = false, - @"4": bool = false, - @"5": bool = false, - @"6": bool = false, - @"7": bool = false, - @"8": bool = false, - }; - - // Parse the octant data. This is all done at comptime so this is - // static data that is embedded in the binary. - const octants_len = octant_max - octant_min + 1; - const octants: [octants_len]Octant = comptime octants: { - @setEvalBranchQuota(10_000); - - var result: [octants_len]Octant = @splat(.{}); - var i: usize = 0; - - const data = @embedFile("octants.txt"); - var it = std.mem.splitScalar(u8, data, '\n'); - while (it.next()) |line| { - // Skip comments - if (line.len == 0 or line[0] == '#') continue; - - const current = &result[i]; - i += 1; - - // Octants are in the format "BLOCK OCTANT-1235". The numbers - // at the end are keys into our packed struct. Since we're - // at comptime we can metaprogram it all. - const idx = std.mem.indexOfScalar(u8, line, '-').?; - for (line[idx + 1 ..]) |c| @field(current, &.{c}) = true; - } - - assert(i == octants_len); - break :octants result; - }; - - const x_halfs = self.xHalfs(); - const y_quads = self.yQuads(); - const oct = octants[cp - octant_min]; - if (oct.@"1") self.rect(canvas, 0, 0, x_halfs[0], y_quads[0]); - if (oct.@"2") self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_quads[0]); - if (oct.@"3") self.rect(canvas, 0, y_quads[1], x_halfs[0], y_quads[2]); - if (oct.@"4") self.rect(canvas, x_halfs[1], y_quads[1], self.metrics.cell_width, y_quads[2]); - if (oct.@"5") self.rect(canvas, 0, y_quads[3], x_halfs[0], y_quads[4]); - if (oct.@"6") self.rect(canvas, x_halfs[1], y_quads[3], self.metrics.cell_width, y_quads[4]); - if (oct.@"7") self.rect(canvas, 0, y_quads[5], x_halfs[0], self.metrics.cell_height); - if (oct.@"8") self.rect(canvas, x_halfs[1], y_quads[5], self.metrics.cell_width, self.metrics.cell_height); -} - -/// xHalfs[0] should be used as the right edge of a left-aligned half. -/// xHalfs[1] should be used as the left edge of a right-aligned half. -fn xHalfs(self: Box) [2]u32 { - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const half_width: u32 = @intFromFloat(@round(0.5 * float_width)); - return .{ half_width, self.metrics.cell_width - half_width }; -} - -/// Use these values as such: -/// yThirds[0] bottom edge of the first third. -/// yThirds[1] top edge of the second third. -/// yThirds[2] bottom edge of the second third. -/// yThirds[3] top edge of the final third. -fn yThirds(self: Box) [4]u32 { - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const one_third_height: u32 = @intFromFloat(@round(one_third * float_height)); - const two_thirds_height: u32 = @intFromFloat(@round(two_thirds * float_height)); - return .{ - one_third_height, - self.metrics.cell_height - two_thirds_height, - two_thirds_height, - self.metrics.cell_height - one_third_height, - }; -} - -/// Use these values as such: -/// yQuads[0] bottom edge of first quarter. -/// yQuads[1] top edge of second quarter. -/// yQuads[2] bottom edge of second quarter. -/// yQuads[3] top edge of third quarter. -/// yQuads[4] bottom edge of third quarter -/// yQuads[5] top edge of fourth quarter. -fn yQuads(self: Box) [6]u32 { - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const quarter_height: u32 = @intFromFloat(@round(0.25 * float_height)); - const half_height: u32 = @intFromFloat(@round(0.50 * float_height)); - const three_quarters_height: u32 = @intFromFloat(@round(0.75 * float_height)); - return .{ - quarter_height, - self.metrics.cell_height - three_quarters_height, - half_height, - self.metrics.cell_height - half_height, - three_quarters_height, - self.metrics.cell_height - quarter_height, - }; -} - -fn draw_smooth_mosaic( - self: Box, - canvas: *font.sprite.Canvas, - mosaic: SmoothMosaic, -) !void { - const y_thirds = self.yThirds(); - const top: f64 = 0.0; - // We average the edge positions for the y_thirds boundaries here - // rather than having to deal with varying alignments depending on - // the surrounding pieces. The most this will be off by is half of - // a pixel, so hopefully it's not noticeable. - const upper: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[0])) + @as(f64, @floatFromInt(y_thirds[1]))); - const lower: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[2])) + @as(f64, @floatFromInt(y_thirds[3]))); - const bottom: f64 = @floatFromInt(self.metrics.cell_height); - const left: f64 = 0.0; - const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2); - const right: f64 = @floatFromInt(self.metrics.cell_width); - - var path: z2d.StaticPath(12) = .{}; - path.init(); // nodes.len = 0 - - if (mosaic.tl) path.lineTo(left, top); // +1, nodes.len = 1 - if (mosaic.ul) path.lineTo(left, upper); // +1, nodes.len = 2 - if (mosaic.ll) path.lineTo(left, lower); // +1, nodes.len = 3 - if (mosaic.bl) path.lineTo(left, bottom); // +1, nodes.len = 4 - if (mosaic.bc) path.lineTo(center, bottom); // +1, nodes.len = 5 - if (mosaic.br) path.lineTo(right, bottom); // +1, nodes.len = 6 - if (mosaic.lr) path.lineTo(right, lower); // +1, nodes.len = 7 - if (mosaic.ur) path.lineTo(right, upper); // +1, nodes.len = 8 - if (mosaic.tr) path.lineTo(right, top); // +1, nodes.len = 9 - if (mosaic.tc) path.lineTo(center, top); // +1, nodes.len = 10 - path.close(); // +2, nodes.len = 12 - - try z2d.painter.fill( - canvas.alloc, - &canvas.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }, - path.wrapped_path.nodes.items, - .{}, - ); -} - -fn draw_edge_triangle( - self: Box, - canvas: *font.sprite.Canvas, - comptime edge: Edge, -) !void { - const upper: f64 = 0.0; - const middle: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 2); - const lower: f64 = @floatFromInt(self.metrics.cell_height); - const left: f64 = 0.0; - const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2); - const right: f64 = @floatFromInt(self.metrics.cell_width); - - const x0, const y0, const x1, const y1 = switch (edge) { - .top => .{ right, upper, left, upper }, - .left => .{ left, upper, left, lower }, - .bottom => .{ left, lower, right, lower }, - .right => .{ right, lower, right, upper }, - }; - - var path: z2d.StaticPath(5) = .{}; - path.init(); // nodes.len = 0 - - path.moveTo(center, middle); // +1, nodes.len = 1 - path.lineTo(x0, y0); // +1, nodes.len = 2 - path.lineTo(x1, y1); // +1, nodes.len = 3 - path.close(); // +2, nodes.len = 5 - - try z2d.painter.fill( - canvas.alloc, - &canvas.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }, - path.wrapped_path.nodes.items, - .{}, - ); -} - -fn draw_arc( - self: Box, - canvas: *font.sprite.Canvas, - comptime corner: Corner, - comptime thickness: Thickness, -) !void { - const thick_px = thickness.height(self.metrics.box_thickness); - const float_width: f64 = @floatFromInt(self.metrics.cell_width); - const float_height: f64 = @floatFromInt(self.metrics.cell_height); - const float_thick: f64 = @floatFromInt(thick_px); - const center_x: f64 = @as(f64, @floatFromInt((self.metrics.cell_width -| thick_px) / 2)) + float_thick / 2; - const center_y: f64 = @as(f64, @floatFromInt((self.metrics.cell_height -| thick_px) / 2)) + float_thick / 2; - - const r = @min(float_width, float_height) / 2; - - // Fraction away from the center to place the middle control points, - const s: f64 = 0.25; - - var ctx = canvas.getContext(); - defer ctx.deinit(); - ctx.setSource(.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }); - ctx.setLineWidth(float_thick); - ctx.setLineCapMode(.round); - - switch (corner) { - .tl => { - try ctx.moveTo(center_x, 0); - try ctx.lineTo(center_x, center_y - r); - try ctx.curveTo( - center_x, - center_y - s * r, - center_x - s * r, - center_y, - center_x - r, - center_y, - ); - try ctx.lineTo(0, center_y); - }, - .tr => { - try ctx.moveTo(center_x, 0); - try ctx.lineTo(center_x, center_y - r); - try ctx.curveTo( - center_x, - center_y - s * r, - center_x + s * r, - center_y, - center_x + r, - center_y, - ); - try ctx.lineTo(float_width, center_y); - }, - .bl => { - try ctx.moveTo(center_x, float_height); - try ctx.lineTo(center_x, center_y + r); - try ctx.curveTo( - center_x, - center_y + s * r, - center_x - s * r, - center_y, - center_x - r, - center_y, - ); - try ctx.lineTo(0, center_y); - }, - .br => { - try ctx.moveTo(center_x, float_height); - try ctx.lineTo(center_x, center_y + r); - try ctx.curveTo( - center_x, - center_y + s * r, - center_x + s * r, - center_y, - center_x + r, - center_y, - ); - try ctx.lineTo(float_width, center_y); - }, - } - try ctx.stroke(); -} - -fn draw_dash_horizontal( - self: Box, - canvas: *font.sprite.Canvas, - count: u8, - thick_px: u32, - desired_gap: u32, -) void { - assert(count >= 2 and count <= 4); - - // +------------+ - // | | - // | | - // | | - // | | - // | -- -- -- | - // | | - // | | - // | | - // | | - // +------------+ - // Our dashed line should be made such that when tiled horizontally - // it creates one consistent line with no uneven gap or segment sizes. - // In order to make sure this is the case, we should have half-sized - // gaps on the left and right so that it is centered properly. - - // For N dashes, there are N - 1 gaps between them, but we also have - // half-sized gaps on either side, adding up to N total gaps. - const gap_count = count; - - // We need at least 1 pixel for each gap and each dash, if we don't - // have that then we can't draw our dashed line correctly so we just - // draw a solid line and return. - if (self.metrics.cell_width < count + gap_count) { - self.hline_middle(canvas, .light); - return; - } - - // We never want the gaps to take up more than 50% of the space, - // because if they do the dashes are too small and look wrong. - const gap_width = @min(desired_gap, self.metrics.cell_width / (2 * count)); - const total_gap_width = gap_count * gap_width; - const total_dash_width = self.metrics.cell_width - total_gap_width; - const dash_width = total_dash_width / count; - const remaining = total_dash_width % count; - - assert(dash_width * count + gap_width * gap_count + remaining == self.metrics.cell_width); - - // Our dashes should be centered vertically. - const y: u32 = (self.metrics.cell_height -| thick_px) / 2; - - // We start at half a gap from the left edge, in order to center - // our dashes properly. - var x: u32 = gap_width / 2; - - // We'll distribute the extra space in to dash widths, 1px at a - // time. We prefer this to making gaps larger since that is much - // more visually obvious. - var extra: u32 = remaining; - - for (0..count) |_| { - var x1 = x + dash_width; - // We distribute left-over size in to dash widths, - // since it's less obvious there than in the gaps. - if (extra > 0) { - extra -= 1; - x1 += 1; - } - self.hline(canvas, x, x1, y, thick_px); - // Advance by the width of the dash we drew and the width - // of a gap to get the the start of the next dash. - x = x1 + gap_width; - } -} - -fn draw_dash_vertical( - self: Box, - canvas: *font.sprite.Canvas, - comptime count: u8, - thick_px: u32, - desired_gap: u32, -) void { - assert(count >= 2 and count <= 4); - - // +-----------+ - // | | | - // | | | - // | | - // | | | - // | | | - // | | - // | | | - // | | | - // | | - // +-----------+ - // Our dashed line should be made such that when tiled vertically it - // it creates one consistent line with no uneven gap or segment sizes. - // In order to make sure this is the case, we should have an extra gap - // gap at the bottom. - // - // A single full-sized extra gap is preferred to two half-sized ones for - // vertical to allow better joining to solid characters without creating - // visible half-sized gaps. Unlike horizontal, centering is a lot less - // important, visually. - - // Because of the extra gap at the bottom, there are as many gaps as - // there are dashes. - const gap_count = count; - - // We need at least 1 pixel for each gap and each dash, if we don't - // have that then we can't draw our dashed line correctly so we just - // draw a solid line and return. - if (self.metrics.cell_height < count + gap_count) { - self.vline_middle(canvas, .light); - return; - } - - // We never want the gaps to take up more than 50% of the space, - // because if they do the dashes are too small and look wrong. - const gap_height = @min(desired_gap, self.metrics.cell_height / (2 * count)); - const total_gap_height = gap_count * gap_height; - const total_dash_height = self.metrics.cell_height - total_gap_height; - const dash_height = total_dash_height / count; - const remaining = total_dash_height % count; - - assert(dash_height * count + gap_height * gap_count + remaining == self.metrics.cell_height); - - // Our dashes should be centered horizontally. - const x: u32 = (self.metrics.cell_width -| thick_px) / 2; - - // We start at the top of the cell. - var y: u32 = 0; - - // We'll distribute the extra space in to dash heights, 1px at a - // time. We prefer this to making gaps larger since that is much - // more visually obvious. - var extra: u32 = remaining; - - inline for (0..count) |_| { - var y1 = y + dash_height; - // We distribute left-over size in to dash widths, - // since it's less obvious there than in the gaps. - if (extra > 0) { - extra -= 1; - y1 += 1; - } - self.vline(canvas, y, y1, x, thick_px); - // Advance by the height of the dash we drew and the height - // of a gap to get the the start of the next dash. - y = y1 + gap_height; - } -} - -fn vline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void { - const thick_px = thickness.height(self.metrics.box_thickness); - self.vline(canvas, 0, self.metrics.cell_height, (self.metrics.cell_width -| thick_px) / 2, thick_px); -} - -fn hline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void { - const thick_px = thickness.height(self.metrics.box_thickness); - self.hline(canvas, 0, self.metrics.cell_width, (self.metrics.cell_height -| thick_px) / 2, thick_px); -} - -fn vline( - self: Box, - canvas: *font.sprite.Canvas, - y1: u32, - y2: u32, - x: u32, - thickness_px: u32, -) void { - canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x, 0), self.metrics.cell_width), - .y = @min(@max(y1, 0), self.metrics.cell_height), - }, .p1 = .{ - .x = @min(@max(x + thickness_px, 0), self.metrics.cell_width), - .y = @min(@max(y2, 0), self.metrics.cell_height), - } }).rect(), .on); -} - -fn hline( - self: Box, - canvas: *font.sprite.Canvas, - x1: u32, - x2: u32, - y: u32, - thickness_px: u32, -) void { - canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x1, 0), self.metrics.cell_width), - .y = @min(@max(y, 0), self.metrics.cell_height), - }, .p1 = .{ - .x = @min(@max(x2, 0), self.metrics.cell_width), - .y = @min(@max(y + thickness_px, 0), self.metrics.cell_height), - } }).rect(), .on); -} - -fn rect( - self: Box, - canvas: *font.sprite.Canvas, - x1: u32, - y1: u32, - x2: u32, - y2: u32, -) void { - canvas.rect((font.sprite.Box(u32){ .p0 = .{ - .x = @min(@max(x1, 0), self.metrics.cell_width), - .y = @min(@max(y1, 0), self.metrics.cell_height), - }, .p1 = .{ - .x = @min(@max(x2, 0), self.metrics.cell_width), - .y = @min(@max(y2, 0), self.metrics.cell_height), - } }).rect(), .on); -} - -// Separated Block Quadrants from Symbols for Legacy Computing Supplement -// 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 -fn draw_separated_block_quadrant(self: Box, canvas: *font.sprite.Canvas, comptime fmt: []const u8) !void { - comptime { - if (fmt.len > 4) @compileError("cannot have more than four quadrants"); - var seen = [_]bool{false} ** (std.math.maxInt(u8) + 1); - for (fmt) |c| { - if (seen[c]) @compileError("repeated quadrants not allowed"); - seen[c] = true; - switch (c) { - '1'...'4' => {}, - else => @compileError("invalid quadrant"), - } - } - } - - var ctx = canvas.getContext(); - defer ctx.deinit(); - ctx.setSource(.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, - } }); - const gap: f64 = @max(1.0, @as(f64, @floatFromInt(self.metrics.cell_width)) * 0.10) / 2.0; - const left: f64 = gap; - const right = @as(f64, @floatFromInt(self.metrics.cell_width)) - gap; - const top: f64 = gap; - const bottom = @as(f64, @floatFromInt(self.metrics.cell_height)) - gap; - const center_x = @as(f64, @floatFromInt(self.metrics.cell_width)) / 2.0; - const center_left = center_x - gap; - const center_right = center_x + gap; - const center_y = @as(f64, @floatFromInt(self.metrics.cell_height)) / 2.0; - const center_top = center_y - gap; - const center_bottom = center_y + gap; - - inline for (fmt) |c| { - const x1, const y1, const x2, const y2 = switch (c) { - '1' => .{ - left, top, - center_left, center_top, - }, - '2' => .{ - center_right, top, - right, center_top, - }, - '3' => .{ - left, center_bottom, - center_left, bottom, - }, - '4' => .{ - center_right, center_bottom, - right, bottom, - }, - else => unreachable, - }; - try ctx.moveTo(x1, y1); - try ctx.lineTo(x2, y1); - try ctx.lineTo(x2, y2); - try ctx.lineTo(x1, y2); - try ctx.closePath(); - } - - try ctx.fill(); -} - -test "all" { - const testing = std.testing; - const alloc = testing.allocator; - - var cp: u32 = 0x2500; - const end = 0x259f; - while (cp <= end) : (cp += 1) { - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - const face: Box = .{ - .metrics = font.Metrics.calc(.{ - .cell_width = 18.0, - .ascent = 30.0, - .descent = -6.0, - .line_gap = 0.0, - }), - }; - const glyph = try face.renderGlyph( - alloc, - &atlas_grayscale, - cp, - ); - try testing.expectEqual(@as(u32, face.metrics.cell_width), glyph.width); - try testing.expectEqual(@as(u32, face.metrics.cell_height), glyph.height); - } -} - -fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void { - // Box Drawing and Block Elements. - var cp: u32 = 0x2500; - while (cp <= 0x259f) : (cp += 1) { - _ = try self.renderGlyph( - alloc, - atlas, - cp, - ); - } - - // Braille - cp = 0x2800; - while (cp <= 0x28ff) : (cp += 1) { - _ = try self.renderGlyph( - alloc, - atlas, - cp, - ); - } - - // Symbols for Legacy Computing. - cp = 0x1fb00; - while (cp <= 0x1fbef) : (cp += 1) { - switch (cp) { - // (Block Mosaics / "Sextants") - // 🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟 🬠 - // 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻 - // (Smooth Mosaics) - // 🬼 🬽 🬾 🬿 🭀 🭁 🭂 🭃 🭄 🭅 🭆 - // 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏 🭐 🭑 - // 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜 - // 🭝 🭞 🭟 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧 - // 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯 - // (Block Elements) - // 🭰 🭱 🭲 🭳 🭴 🭵 🭶 🭷 🭸 🭹 🭺 🭻 - // 🭼 🭽 🭾 🭿 🮀 🮁 - // 🮂 🮃 🮄 🮅 🮆 - // 🮇 🮈 🮉 🮊 🮋 - // (Rectangular Shade Characters) - // 🮌 🮍 🮎 🮏 🮐 🮑 🮒 - 0x1FB00...0x1FB92, - // (Rectangular Shade Characters) - // 🮔 - // (Fill Characters) - // 🮕 🮖 🮗 - // (Diagonal Fill Characters) - // 🮘 🮙 - // (Smooth Mosaics) - // 🮚 🮛 - // (Triangular Shade Characters) - // 🮜 🮝 🮞 🮟 - // (Character Cell Diagonals) - // 🮠 🮡 🮢 🮣 🮤 🮥 🮦 🮧 🮨 🮩 🮪 🮫 🮬 🮭 🮮 - // (Light Solid Line With Stroke) - // 🮯 - 0x1FB94...0x1FBAF, - // (Negative Terminal Characters) - // 🮽 🮾 🮿 - 0x1FBBD...0x1FBBF, - // (Block Elements) - // 🯎 🯏 - // (Character Cell Diagonals) - // 🯐 🯑 🯒 🯓 🯔 🯕 🯖 🯗 🯘 🯙 🯚 🯛 🯜 🯝 🯞 🯟 - // (Geometric Shapes) - // 🯠 🯡 🯢 🯣 🯤 🯥 🯦 🯧 🯨 🯩 🯪 🯫 🯬 🯭 🯮 🯯 - 0x1FBCE...0x1FBEF, - => _ = try self.renderGlyph( - alloc, - atlas, - cp, - ), - else => {}, - } - } - - // Branch drawing character set, used for drawing git-like - // graphs in the terminal. Originally implemented in Kitty. - // Ref: - // - https://github.com/kovidgoyal/kitty/pull/7681 - // - https://github.com/kovidgoyal/kitty/pull/7805 - // NOTE: Kitty is GPL licensed, and its code was not referenced - // for these characters, only the loose specification of - // the character set in the pull request descriptions. - // - // TODO(qwerasd): This should be in another file, but really the - // general organization of the sprite font code - // needs to be reworked eventually. - // - //           - //                     - //                     - //             - cp = 0xf5d0; - while (cp <= 0xf60d) : (cp += 1) { - _ = try self.renderGlyph( - alloc, - atlas, - cp, - ); - } - - // Symbols for Legacy Computing Supplement: Quadrants - // 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 - cp = 0x1cc21; - while (cp <= 0x1cc2f) : (cp += 1) { - switch (cp) { - 0x1cc21...0x1cc2f => _ = try self.renderGlyph( - alloc, - atlas, - cp, - ), - else => {}, - } - } - - // Symbols for Legacy Computing Supplement: Octants - cp = 0x1CD00; - while (cp <= 0x1CDE5) : (cp += 1) { - switch (cp) { - 0x1CD00...0x1CDE5 => _ = try self.renderGlyph( - alloc, - atlas, - cp, - ), - else => {}, - } - } - - // Geometric Shapes: filled and outlined corners - for ([_]u21{ '◢', '◣', '◤', '◥', '◸', '◹', '◺', '◿' }) |char| { - _ = try self.renderGlyph( - alloc, - atlas, - char, - ); - } -} - -test "render all sprites" { - // Renders all sprites to an atlas and compares - // it to a ground truth for regression testing. - - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 1024, .grayscale); - defer atlas_grayscale.deinit(alloc); - - // Even cell size and thickness (18 x 36) - try (Box{ - .metrics = font.Metrics.calc(.{ - .cell_width = 18.0, - .ascent = 30.0, - .descent = -6.0, - .line_gap = 0.0, - .underline_thickness = 2.0, - .strikethrough_thickness = 2.0, - }), - }).testRenderAll(alloc, &atlas_grayscale); - - // Odd cell size and thickness (9 x 15) - try (Box{ - .metrics = font.Metrics.calc(.{ - .cell_width = 9.0, - .ascent = 12.0, - .descent = -3.0, - .line_gap = 0.0, - .underline_thickness = 1.0, - .strikethrough_thickness = 1.0, - }), - }).testRenderAll(alloc, &atlas_grayscale); - - const ground_truth = @embedFile("./testdata/Box.ppm"); - - var stream = std.io.changeDetectionStream(ground_truth, std.io.null_writer); - try atlas_grayscale.dump(stream.writer()); - - if (stream.changeDetected()) { - log.err( - \\ - \\!! [Box.zig] Change detected from ground truth! - \\!! Dumping ./Box_test.ppm and ./Box_test_diff.ppm - \\!! Please check changes and update Box.ppm in testdata if intended. - , - .{}, - ); - - const ppm = try std.fs.cwd().createFile("Box_test.ppm", .{}); - defer ppm.close(); - try atlas_grayscale.dump(ppm.writer()); - - const diff = try std.fs.cwd().createFile("Box_test_diff.ppm", .{}); - defer diff.close(); - var writer = diff.writer(); - try writer.print( - \\P6 - \\{d} {d} - \\255 - \\ - , .{ atlas_grayscale.size, atlas_grayscale.size }); - for (ground_truth[try diff.getPos()..], atlas_grayscale.data) |a, b| { - if (a == b) { - try writer.writeByteNTimes(a / 3, 3); - } else { - try writer.writeByte(a); - try writer.writeByte(b); - try writer.writeByte(0); - } - } - } -} diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index af0c0af6a..25968e865 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -16,25 +16,154 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; +const wuffs = @import("wuffs"); +const z2d = @import("z2d"); const font = @import("../main.zig"); const Sprite = font.sprite.Sprite; -const Box = @import("Box.zig"); -const Powerline = @import("Powerline.zig"); -const underline = @import("underline.zig"); -const cursor = @import("cursor.zig"); + +const special = @import("draw/special.zig"); const log = std.log.scoped(.font_sprite); /// Grid metrics for rendering sprites. metrics: font.Metrics, +pub const DrawFnError = + Allocator.Error || + z2d.painter.FillError || + z2d.painter.StrokeError || + error{ + /// Something went wrong while doing math. + MathError, + }; + +/// A function that draws a glyph on the provided canvas. +pub const DrawFn = fn ( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) DrawFnError!void; + +const Range = struct { + min: u32, + max: u32, + draw: DrawFn, +}; + +/// Automatically collect ranges for functions with names +/// in the format `draw` or `draw_`. +const ranges = ranges: { + @setEvalBranchQuota(1_000_000); + + // Structs containing drawing functions for codepoint ranges. + const structs = [_]type{ + @import("draw/block.zig"), + @import("draw/box.zig"), + @import("draw/braille.zig"), + @import("draw/branch.zig"), + @import("draw/geometric_shapes.zig"), + @import("draw/powerline.zig"), + @import("draw/symbols_for_legacy_computing.zig"), + @import("draw/symbols_for_legacy_computing_supplement.zig"), + }; + + // Count how many draw fns we have + var range_count = 0; + for (structs) |s| { + for (@typeInfo(s).@"struct".decls) |decl| { + if (!@hasDecl(s, decl.name)) continue; + if (!std.mem.startsWith(u8, decl.name, "draw")) continue; + range_count += 1; + } + } + + // Make an array and collect ranges for each function. + var r: [range_count]Range = undefined; + var names: [range_count][:0]const u8 = undefined; + var i = 0; + for (structs) |s| { + for (@typeInfo(s).@"struct".decls) |decl| { + if (!@hasDecl(s, decl.name)) continue; + if (!std.mem.startsWith(u8, decl.name, "draw")) continue; + + const sep = std.mem.indexOfScalar(u8, decl.name, '_') orelse decl.name.len; + + const min = std.fmt.parseInt(u21, decl.name[4..sep], 16) catch unreachable; + + const max = if (sep == decl.name.len) + min + else + std.fmt.parseInt(u21, decl.name[sep + 1 ..], 16) catch unreachable; + + r[i] = .{ + .min = min, + .max = max, + .draw = @field(s, decl.name), + }; + names[i] = decl.name; + i += 1; + } + } + + // Sort ranges in ascending order + std.mem.sortUnstableContext(0, r.len, struct { + r: []Range, + names: [][:0]const u8, + pub fn lessThan(self: @This(), a: usize, b: usize) bool { + return self.r[a].min < self.r[b].min; + } + pub fn swap(self: @This(), a: usize, b: usize) void { + std.mem.swap(Range, &self.r[a], &self.r[b]); + std.mem.swap([:0]const u8, &self.names[a], &self.names[b]); + } + }{ + .r = &r, + .names = &names, + }); + + // Ensure there's no overlapping ranges + i = 0; + for (r, 0..) |n, k| { + if (n.min <= i) { + @compileError( + std.fmt.comptimePrint( + "Codepoint range for {s}(...) overlaps range for {s}(...), {X} <= {X} <= {X}", + .{ names[k], names[k - 1], r[k - 1].min, n.min, r[k - 1].max }, + ), + ); + } + i = n.max; + } + + break :ranges r; +}; + +fn getDrawFn(cp: u32) ?*const DrawFn { + // For special sprites (cursors, underlines, etc.) all sprites are drawn + // by functions from `Special` that share the name of the enum field. + if (cp >= Sprite.start) switch (@as(Sprite, @enumFromInt(cp))) { + inline else => |sprite| { + return @field(special, @tagName(sprite)); + }, + }; + + // Pray that the compiler is smart enough to + // turn this in to a jump table or something... + inline for (ranges) |range| { + if (cp >= range.min and cp <= range.max) return range.draw; + } + return null; +} + /// Returns true if the codepoint exists in our sprite font. pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool { - // We ignore presentation. No matter what presentation is requested - // we always provide glyphs for our codepoints. + // We ignore presentation. No matter what presentation is + // requested we always provide glyphs for our codepoints. _ = p; _ = self; - return Kind.init(cp) != null; + return getDrawFn(cp) != null; } /// Render the glyph. @@ -52,18 +181,10 @@ pub fn renderGlyph( } } - const metrics = self.metrics; - - // We adjust our sprite width based on the cell width. - const width = switch (opts.cell_width orelse 1) { - 0, 1 => metrics.cell_width, - else => |width| metrics.cell_width * width, - }; - // It should be impossible for this to be null and we assert that // in runtime safety modes but in case it is its not worth memory // corruption so we return a valid, blank glyph. - const kind = Kind.init(cp) orelse return .{ + const draw = getDrawFn(cp) orelse return .{ .width = 0, .height = 0, .offset_x = 0, @@ -73,217 +194,349 @@ pub fn renderGlyph( .advance_x = 0, }; - // Safe to ".?" because of the above assertion. - return switch (kind) { - .box => (Box{ .metrics = metrics }).renderGlyph(alloc, atlas, cp), + const metrics = self.metrics; - .underline => try underline.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cell_height, - metrics.underline_position, - metrics.underline_thickness, - ), + // We adjust our sprite width based on the cell width. + const width = switch (opts.cell_width orelse 1) { + 0, 1 => metrics.cell_width, + else => |width| metrics.cell_width * width, + }; - .strikethrough => try underline.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cell_height, - metrics.strikethrough_position, - metrics.strikethrough_thickness, - ), + const height = metrics.cell_height; - .overline => overline: { - var g = try underline.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cell_height, - 0, - metrics.overline_thickness, - ); + const padding_x = width / 4; + const padding_y = height / 4; - // We have to manually subtract the overline position - // on the rendered glyph since it can be negative. - g.offset_y -= metrics.overline_position; + // Make a canvas of the desired size + var canvas = try font.sprite.Canvas.init(alloc, width, height, padding_x, padding_y); + defer canvas.deinit(); - break :overline g; - }, + try draw(cp, &canvas, width, height, metrics); - .powerline => powerline: { - const f: Powerline = .{ - .width = metrics.cell_width, - .height = metrics.cell_height, - .thickness = metrics.box_thickness, - }; + // Write the drawing to the atlas + const region = try canvas.writeAtlas(alloc, atlas); - break :powerline try f.renderGlyph(alloc, atlas, cp); - }, - - .cursor => cursor: { - var g = try cursor.renderGlyph( - alloc, - atlas, - @enumFromInt(cp), - width, - metrics.cursor_height, - metrics.cursor_thickness, - ); - - // Cursors are drawn at their specified height - // and are centered vertically within the cell. - const cursor_height: i32 = @intCast(metrics.cursor_height); - const cell_height: i32 = @intCast(metrics.cell_height); - g.offset_y += @divTrunc(cell_height - cursor_height, 2); - - break :cursor g; - }, + return font.Glyph{ + .width = region.width, + .height = region.height, + .offset_x = @as(i32, @intCast(canvas.clip_left)) - @as(i32, @intCast(padding_x)), + .offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)), + .atlas_x = region.x, + .atlas_y = region.y, + .advance_x = @floatFromInt(width), }; } -/// Kind of sprites we have. Drawing is implemented separately for each kind. -const Kind = enum { - box, - underline, - overline, - strikethrough, - powerline, - cursor, +/// Used in `testDrawRanges`, checks for diff between the provided atlas +/// and the reference file for the range, returns true if there is a diff. +fn testDiffAtlas( + alloc: Allocator, + atlas: *z2d.Surface, + path: []const u8, + i: u32, + width: u32, + height: u32, + thickness: u32, +) !bool { + // Get the file contents, we compare the PNG data first in + // order to ensure that no one smuggles arbitrary binary + // data in to the reference PNGs. + const test_file = try std.fs.openFileAbsolute(path, .{ .mode = .read_only }); + defer test_file.close(); + const test_bytes = try test_file.readToEndAlloc( + alloc, + std.math.maxInt(usize), + ); + defer alloc.free(test_bytes); - pub fn init(cp: u32) ?Kind { - return switch (cp) { - Sprite.start...Sprite.end => switch (@as(Sprite, @enumFromInt(cp))) { - .underline, - .underline_double, - .underline_dotted, - .underline_dashed, - .underline_curly, - => .underline, + const cwd_absolute = try std.fs.cwd().realpathAlloc(alloc, "."); + defer alloc.free(cwd_absolute); - .overline, - => .overline, + // Get the reference file contents to compare. + const ref_path = try std.fmt.allocPrint( + alloc, + "./src/font/sprite/testdata/U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(ref_path); + const ref_file = + std.fs.cwd().openFile(ref_path, .{ .mode = .read_only }) catch |err| { + log.err("Can't open reference file {s}: {}\n", .{ + ref_path, + err, + }); - .strikethrough, - => .strikethrough, + // Copy the test PNG in to the CWD so it isn't + // cleaned up with the rest of the tmp dir files. + const test_path = try std.fmt.allocPrint( + alloc, + "{s}/sprite_face_test-U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ cwd_absolute, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(test_path); + try std.fs.copyFileAbsolute(path, test_path, .{}); - .cursor_rect, - .cursor_hollow_rect, - .cursor_bar, - => .cursor, - }, - - // == Box fonts == - - // "Box Drawing" block - // ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ ┠ - // ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ ╀ ╁ - // ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ ╠ ╡ ╢ - // ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ - 0x2500...0x257F, - - // "Block Elements" block - // ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟ - 0x2580...0x259F, - - // "Geometric Shapes" block - 0x25e2...0x25e5, // ◢◣◤◥ - 0x25f8...0x25fa, // ◸◹◺ - 0x25ff, // ◿ - - // "Braille" block - 0x2800...0x28FF, - - // "Symbols for Legacy Computing" block - // (Block Mosaics / "Sextants") - // 🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟 🬠 - // 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻 - // (Smooth Mosaics) - // 🬼 🬽 🬾 🬿 🭀 🭁 🭂 🭃 🭄 🭅 🭆 - // 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏 🭐 🭑 - // 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜 - // 🭝 🭞 🭟 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧 - // 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯 - // (Block Elements) - // 🭰 🭱 🭲 🭳 🭴 🭵 🭶 🭷 🭸 🭹 🭺 🭻 - // 🭼 🭽 🭾 🭿 🮀 🮁 - // 🮂 🮃 🮄 🮅 🮆 - // 🮇 🮈 🮉 🮊 🮋 - // (Rectangular Shade Characters) - // 🮌 🮍 🮎 🮏 🮐 🮑 🮒 - 0x1FB00...0x1FB92, - // (Rectangular Shade Characters) - // 🮔 - // (Fill Characters) - // 🮕 🮖 🮗 - // (Diagonal Fill Characters) - // 🮘 🮙 - // (Smooth Mosaics) - // 🮚 🮛 - // (Triangular Shade Characters) - // 🮜 🮝 🮞 🮟 - // (Character Cell Diagonals) - // 🮠 🮡 🮢 🮣 🮤 🮥 🮦 🮧 🮨 🮩 🮪 🮫 🮬 🮭 🮮 - // (Light Solid Line With Stroke) - // 🮯 - 0x1FB94...0x1FBAF, - // (Negative Terminal Characters) - // 🮽 🮾 🮿 - 0x1FBBD...0x1FBBF, - // (Block Elements) - // 🯎 🯏 - // (Character Cell Diagonals) - // 🯐 🯑 🯒 🯓 🯔 🯕 🯖 🯗 🯘 🯙 🯚 🯛 🯜 🯝 🯞 🯟 - // (Geometric Shapes) - // 🯠 🯡 🯢 🯣 🯤 🯥 🯦 🯧 🯨 🯩 🯪 🯫 🯬 🯭 🯮 🯯 - 0x1FBCE...0x1FBEF, - // (Octants) - 0x1CD00...0x1CDE5, - => .box, - - // Branch drawing character set, used for drawing git-like - // graphs in the terminal. Originally implemented in Kitty. - // Ref: - // - https://github.com/kovidgoyal/kitty/pull/7681 - // - https://github.com/kovidgoyal/kitty/pull/7805 - // NOTE: Kitty is GPL licensed, and its code was not referenced - // for these characters, only the loose specification of - // the character set in the pull request descriptions. - // - //           - //                     - //                     - //             - 0xF5D0...0xF60D => .box, - - // Separated Block Quadrants from Symbols for Legacy Computing Supplement - // 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 - 0x1CC21...0x1CC2F => .box, - - // Powerline fonts - 0xE0B0, - 0xE0B1, - 0xE0B3, - 0xE0B4, - 0xE0B6, - 0xE0B2, - 0xE0B8, - 0xE0BA, - 0xE0BC, - 0xE0BE, - 0xE0D2, - 0xE0D4, - => .powerline, - - else => null, + return true; }; + defer ref_file.close(); + const ref_bytes = try ref_file.readToEndAlloc( + alloc, + std.math.maxInt(usize), + ); + defer alloc.free(ref_bytes); + + // Do our PNG bytes comparison, if it's the same then we can + // move on, otherwise we'll decode the reference file and do + // a pixel-for-pixel diff. + if (std.mem.eql(u8, test_bytes, ref_bytes)) return false; + + // Copy the test PNG in to the CWD so it isn't + // cleaned up with the rest of the tmp dir files. + const test_path = try std.fmt.allocPrint( + alloc, + "{s}/sprite_face_test-U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ cwd_absolute, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(test_path); + try std.fs.copyFileAbsolute(path, test_path, .{}); + + // Use wuffs to decode the reference PNG to raw pixels. + // These will be RGBA, so when diffing we can just compare + // every fourth byte. + const ref_rgba = try wuffs.png.decode(alloc, ref_bytes); + defer alloc.free(ref_rgba.data); + + assert(ref_rgba.width == atlas.getWidth()); + assert(ref_rgba.height == atlas.getHeight()); + + // We'll make a visual representation of the diff using + // red for removed pixels and green for added. We make + // a z2d surface for that here. + var diff = try z2d.Surface.init( + .image_surface_rgb, + alloc, + atlas.getWidth(), + atlas.getHeight(), + ); + defer diff.deinit(alloc); + const diff_pix = diff.image_surface_rgb.buf; + + const test_gray = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf); + + var differs: bool = false; + for (0..test_gray.len) |j| { + const t = test_gray[j]; + const r = ref_rgba.data[j * 4]; + if (t == r) { + // If the pixels match, write it as a faded gray. + diff_pix[j].r = t / 3; + diff_pix[j].g = t / 3; + diff_pix[j].b = t / 3; + } else { + differs = true; + // Otherwise put the reference value in the red + // channel and the new value in the green channel. + diff_pix[j].r = r; + diff_pix[j].g = t; + } } -}; + + // If the PNG data differs but not the raw pixels, that's + // a big red flag, since it could mean someone is trying to + // smuggle binary data in to the test files. + if (!differs) { + log.err( + "!!! Test PNG data does not match reference, but pixels do match! " ++ + "Either z2d's PNG exporter changed or someone is " ++ + "trying to smuggle binary data in the test files!\n" ++ + "test={s}, reference={s}", + .{ test_path, ref_path }, + ); + return true; + } + + // Drop the diff image as a PNG in the cwd. + const diff_path = try std.fmt.allocPrint( + alloc, + "./sprite_face_diff-U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(diff_path); + try z2d.png_exporter.writeToPNGFile(diff, diff_path, .{}); + log.err( + "One or more glyphs differ from reference file in range U+{X}...U+{X}! " ++ + "test={s}, reference={s}, diff={s}", + .{ i, i + 0xFF, test_path, ref_path, diff_path }, + ); + + return true; +} + +/// Draws all ranges in to a set of 16x16 glyph atlases, checks for regressions +/// against reference files, logs errors and exposes a diff for any difference +/// between the reference and test atlas. +/// +/// Returns true if there was a diff. +fn testDrawRanges( + width: u32, + ascent: u32, + descent: u32, + thickness: u32, +) !bool { + const testing = std.testing; + const alloc = testing.allocator; + + const metrics: font.Metrics = .calc(.{ + .cell_width = @floatFromInt(width), + .ascent = @floatFromInt(ascent), + .descent = -@as(f64, @floatFromInt(descent)), + .line_gap = 0.0, + .underline_thickness = @floatFromInt(thickness), + .strikethrough_thickness = @floatFromInt(thickness), + }); + + const height = ascent + descent; + + const padding_x = width / 4; + const padding_y = height / 4; + + // Canvas to draw glyphs on, we'll re-use this for all glyphs. + var canvas = try font.sprite.Canvas.init( + alloc, + width, + height, + padding_x, + padding_y, + ); + defer canvas.deinit(); + + // We render glyphs in batches of 256, which we copy (including padding) to + // a 16 by 16 surface to be compared with the reference file for that range. + const stride_x = width + 2 * padding_x; + const stride_y = height + 2 * padding_y; + var atlas = try z2d.Surface.init( + .image_surface_alpha8, + alloc, + @intCast(stride_x * 16), + @intCast(stride_y * 16), + ); + defer atlas.deinit(alloc); + + var i: u32 = std.mem.alignBackward(u32, ranges[0].min, 0x100); + + // Try to make the sprite_face_test folder if it doesn't already exist. + var dir = testing.tmpDir(.{}); + defer dir.cleanup(); + const tmp_dir = try dir.dir.realpathAlloc(alloc, "."); + defer alloc.free(tmp_dir); + + // We set this to true if we have any fails so we can + // return an error after we're done comparing all glyphs. + var fail: bool = false; + + inline for (ranges) |range| { + for (range.min..range.max + 1) |cp| { + // If we've moved to a new batch of 256, check the + // current one and clear the surface for the next one. + if (cp - i >= 0x100) { + // Export to our tmp dir. + const path = try std.fmt.allocPrint( + alloc, + "{s}/U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ tmp_dir, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(path); + try z2d.png_exporter.writeToPNGFile(atlas, path, .{}); + + if (try testDiffAtlas( + alloc, + &atlas, + path, + i, + width, + height, + thickness, + )) fail = true; + + i = std.mem.alignBackward(u32, @intCast(cp), 0x100); + @memset(std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf), 0); + } + + try getDrawFn(@intCast(cp)).?( + @intCast(cp), + &canvas, + width, + height, + metrics, + ); + canvas.clearClippingRegions(); + atlas.composite( + &canvas.sfc, + .src, + @intCast(stride_x * ((cp - i) % 16)), + @intCast(stride_y * ((cp - i) / 16)), + .{}, + ); + @memset(std.mem.sliceAsBytes(canvas.sfc.image_surface_alpha8.buf), 0); + canvas.clip_top = 0; + canvas.clip_left = 0; + canvas.clip_right = 0; + canvas.clip_bottom = 0; + } + } + + const path = try std.fmt.allocPrint( + alloc, + "{s}/U+{X}...U+{X}-{d}x{d}+{d}.png", + .{ tmp_dir, i, i + 0xFF, width, height, thickness }, + ); + defer alloc.free(path); + try z2d.png_exporter.writeToPNGFile(atlas, path, .{}); + if (try testDiffAtlas( + alloc, + &atlas, + path, + i, + width, + height, + thickness, + )) fail = true; + + return fail; +} + +test "sprite face render all sprites" { + // Renders all sprites to an atlas and compares + // it to a ground truth for regression testing. + + var diff: bool = false; + + // testDrawRanges(width, ascent, descent, thickness): + // + // We compare 4 different sets of metrics; + // - even cell size / even thickness + // - even cell size / odd thickness + // - odd cell size / even thickness + // - odd cell size / odd thickness + // (Also a decreasing range of sizes.) + if (try testDrawRanges(18, 30, 6, 4)) diff = true; + if (try testDrawRanges(12, 20, 4, 3)) diff = true; + if (try testDrawRanges(11, 19, 2, 2)) diff = true; + if (try testDrawRanges(9, 15, 2, 1)) diff = true; + + try std.testing.expect(!diff); // There should be no diffs from reference. +} + +// test "sprite face print all sprites" { +// std.debug.print("\n\n", .{}); +// inline for (ranges) |range| { +// for (range.min..range.max + 1) |cp| { +// std.debug.print("{u}", .{ @as(u21, @intCast(cp)) }); +// } +// } +// std.debug.print("\n\n", .{}); +// } test { - @import("std").testing.refAllDecls(@This()); + std.testing.refAllDecls(@This()); } diff --git a/src/font/sprite/Powerline.zig b/src/font/sprite/Powerline.zig deleted file mode 100644 index eaa7554b1..000000000 --- a/src/font/sprite/Powerline.zig +++ /dev/null @@ -1,564 +0,0 @@ -//! This file contains functions for drawing certain characters from Powerline -//! Extra (https://github.com/ryanoasis/powerline-extra-symbols). These -//! characters are similarly to box-drawing characters (see Box.zig), so the -//! logic will be mainly the same, just with a much reduced character set. -//! -//! Note that this is not the complete list of Powerline glyphs that may be -//! needed, so this may grow to add other glyphs from the set. -const Powerline = @This(); - -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const font = @import("../main.zig"); -const Quad = @import("canvas.zig").Quad; - -const log = std.log.scoped(.powerline_font); - -/// The cell width and height because the boxes are fit perfectly -/// into a cell so that they all properly connect with zero spacing. -width: u32, -height: u32, - -/// Base thickness value for glyphs that are not completely solid (backslashes, -/// thin half-circles, etc). If you want to do any DPI scaling, it is expected -/// to be done earlier. -/// -/// TODO: this and Thickness are currently unused but will be when the -/// aforementioned glyphs are added. -thickness: u32, - -/// The thickness of a line. -const Thickness = enum { - super_light, - light, - heavy, - - /// Calculate the real height of a line based on its thickness - /// and a base thickness value. The base thickness value is expected - /// to be in pixels. - fn height(self: Thickness, base: u32) u32 { - return switch (self) { - .super_light => @max(base / 2, 1), - .light => base, - .heavy => base * 2, - }; - } -}; - -inline fn sq(x: anytype) @TypeOf(x) { - return x * x; -} - -pub fn renderGlyph( - self: Powerline, - alloc: Allocator, - atlas: *font.Atlas, - cp: u32, -) !font.Glyph { - // Create the canvas we'll use to draw - var canvas = try font.sprite.Canvas.init(alloc, self.width, self.height); - defer canvas.deinit(); - - // Perform the actual drawing - try self.draw(alloc, &canvas, cp); - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - // Our coordinates start at the BOTTOM for our renderers so we have to - // specify an offset of the full height because we rendered a full size - // cell. - const offset_y = @as(i32, @intCast(self.height)); - - return font.Glyph{ - .width = self.width, - .height = self.height, - .offset_x = 0, - .offset_y = offset_y, - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(self.width), - }; -} - -fn draw(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void { - switch (cp) { - // Hard dividers and triangles - 0xE0B0, - 0xE0B2, - 0xE0B8, - 0xE0BA, - 0xE0BC, - 0xE0BE, - => try self.draw_wedge_triangle(canvas, cp), - - // Soft Dividers - 0xE0B1, - 0xE0B3, - => try self.draw_chevron(canvas, cp), - - // Half-circles - 0xE0B4, - 0xE0B6, - => try self.draw_half_circle(alloc, canvas, cp), - - // Mirrored top-down trapezoids - 0xE0D2, - 0xE0D4, - => try self.draw_trapezoid_top_bottom(canvas, cp), - - else => return error.InvalidCodepoint, - } -} - -fn draw_chevron(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void { - const width = self.width; - const height = self.height; - - var p1_x: u32 = 0; - var p1_y: u32 = 0; - var p2_x: u32 = 0; - var p2_y: u32 = 0; - var p3_x: u32 = 0; - var p3_y: u32 = 0; - - switch (cp) { - 0xE0B1 => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = height / 2; - p3_x = 0; - p3_y = height; - }, - 0xE0B3 => { - p1_x = width; - p1_y = 0; - p2_x = 0; - p2_y = height / 2; - p3_x = width; - p3_y = height; - }, - - else => unreachable, - } - - try canvas.triangle_outline(.{ - .p0 = .{ .x = @floatFromInt(p1_x), .y = @floatFromInt(p1_y) }, - .p1 = .{ .x = @floatFromInt(p2_x), .y = @floatFromInt(p2_y) }, - .p2 = .{ .x = @floatFromInt(p3_x), .y = @floatFromInt(p3_y) }, - }, @floatFromInt(Thickness.light.height(self.thickness)), .on); -} - -fn draw_wedge_triangle(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void { - const width = self.width; - const height = self.height; - - var p1_x: u32 = 0; - var p2_x: u32 = 0; - var p3_x: u32 = 0; - var p1_y: u32 = 0; - var p2_y: u32 = 0; - var p3_y: u32 = 0; - - switch (cp) { - 0xE0B0 => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = height / 2; - p3_x = 0; - p3_y = height; - }, - - 0xE0B2 => { - p1_x = width; - p1_y = 0; - p2_x = 0; - p2_y = height / 2; - p3_x = width; - p3_y = height; - }, - - 0xE0B8 => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = height; - p3_x = 0; - p3_y = height; - }, - - 0xE0BA => { - p1_x = width; - p1_y = 0; - p2_x = width; - p2_y = height; - p3_x = 0; - p3_y = height; - }, - - 0xE0BC => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = 0; - p3_x = 0; - p3_y = height; - }, - - 0xE0BE => { - p1_x = 0; - p1_y = 0; - p2_x = width; - p2_y = 0; - p3_x = width; - p3_y = height; - }, - - else => unreachable, - } - - try canvas.triangle(.{ - .p0 = .{ .x = @floatFromInt(p1_x), .y = @floatFromInt(p1_y) }, - .p1 = .{ .x = @floatFromInt(p2_x), .y = @floatFromInt(p2_y) }, - .p2 = .{ .x = @floatFromInt(p3_x), .y = @floatFromInt(p3_y) }, - }, .on); -} - -fn draw_half_circle(self: Powerline, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void { - const supersample = 4; - - // We make a canvas big enough for the whole circle, with the supersample - // applied. - const width = self.width * 2 * supersample; - const height = self.height * supersample; - - // We set a minimum super-sampled canvas to assert on. The minimum cell - // size is 1x3px, and this looked safe in empirical testing. - std.debug.assert(width >= 8); // 1 * 2 * 4 - std.debug.assert(height >= 12); // 3 * 4 - - const center_x = width / 2 - 1; - const center_y = height / 2 - 1; - - // Our radii. We're technically drawing an ellipse here to ensure that this - // works for fonts with different aspect ratios than a typical 2:1 H*W, e.g. - // Iosevka (which is around 2.6:1). - const radius_x = width / 2 - 1; // This gives us a small margin for smoothing - const radius_y = height / 2; - - // Pre-allocate a matrix to plot the points on. - const cap = height * width; - var points = try alloc.alloc(u8, cap); - defer alloc.free(points); - @memset(points, 0); - - { - // This is a midpoint ellipse algorithm, similar to a midpoint circle - // algorithm in that we only draw the octants we need and then reflect - // the result across the other axes. Since an ellipse has two radii, we - // need to calculate two octants instead of one. There are variations - // on the algorithm and you can find many examples online. This one - // does use some floating point math in calculating the decision - // parameter, but I've found it clear in its implementation and it does - // not require adjustment for integer error. - // - // This algorithm has undergone some iterations, so the following - // references might be helpful for understanding: - // - // * "Drawing a circle, point by point, without floating point - // support" (Dennis Yurichev, - // https://yurichev.com/news/20220322_circle/), which describes the - // midpoint circle algorithm and implementation we initially adapted - // here. - // - // * "Ellipse-Generating Algorithms" (RTU Latvia, - // https://daugavpils.rtu.lv/wp-content/uploads/sites/34/2020/11/LEC_3.pdf), - // which was used to further adapt the algorithm for ellipses. - // - // * "An Effective Approach to Minimize Error in Midpoint Ellipse - // Drawing Algorithm" (Dr. M. Javed Idrisi, Aayesha Ashraf, - // https://arxiv.org/abs/2103.04033), which includes a synopsis of - // the history of ellipse drawing algorithms, and further references. - - // Declare some casted constants for use in various calculations below - const rx: i32 = @intCast(radius_x); - const ry: i32 = @intCast(radius_y); - const rxf: f64 = @floatFromInt(radius_x); - const ryf: f64 = @floatFromInt(radius_y); - const cx: i32 = @intCast(center_x); - const cy: i32 = @intCast(center_y); - - // Our plotting x and y - var x: i32 = 0; - var y: i32 = @intCast(radius_y); - - // Decision parameter, initialized for region 1 - var dparam: f64 = sq(ryf) - sq(rxf) * ryf + sq(rxf) * 0.25; - - // Region 1 - while (2 * sq(ry) * x < 2 * sq(rx) * y) { - // Right side - const x1 = @max(0, cx + x); - const y1 = @max(0, cy + y); - const x2 = @max(0, cx + x); - const y2 = @max(0, cy - y); - - // Left side - const x3 = @max(0, cx - x); - const y3 = @max(0, cy + y); - const x4 = @max(0, cx - x); - const y4 = @max(0, cy - y); - - // Points - const p1 = y1 * width + x1; - const p2 = y2 * width + x2; - const p3 = y3 * width + x3; - const p4 = y4 * width + x4; - - // Set the points in the matrix, ignore any out of bounds - if (p1 < cap) points[p1] = 0xFF; - if (p2 < cap) points[p2] = 0xFF; - if (p3 < cap) points[p3] = 0xFF; - if (p4 < cap) points[p4] = 0xFF; - - // Calculate next pixels based on midpoint bounds - x += 1; - if (dparam < 0) { - const xf: f64 = @floatFromInt(x); - dparam += 2 * sq(ryf) * xf + sq(ryf); - } else { - y -= 1; - const xf: f64 = @floatFromInt(x); - const yf: f64 = @floatFromInt(y); - dparam += 2 * sq(ryf) * xf - 2 * sq(rxf) * yf + sq(ryf); - } - } - - // Region 2 - { - // Reset our decision parameter for region 2 - const xf: f64 = @floatFromInt(x); - const yf: f64 = @floatFromInt(y); - dparam = sq(ryf) * sq(xf + 0.5) + sq(rxf) * sq(yf - 1) - sq(rxf) * sq(ryf); - } - while (y >= 0) { - // Right side - const x1 = @max(0, cx + x); - const y1 = @max(0, cy + y); - const x2 = @max(0, cx + x); - const y2 = @max(0, cy - y); - - // Left side - const x3 = @max(0, cx - x); - const y3 = @max(0, cy + y); - const x4 = @max(0, cx - x); - const y4 = @max(0, cy - y); - - // Points - const p1 = y1 * width + x1; - const p2 = y2 * width + x2; - const p3 = y3 * width + x3; - const p4 = y4 * width + x4; - - // Set the points in the matrix, ignore any out of bounds - if (p1 < cap) points[p1] = 0xFF; - if (p2 < cap) points[p2] = 0xFF; - if (p3 < cap) points[p3] = 0xFF; - if (p4 < cap) points[p4] = 0xFF; - - // Calculate next pixels based on midpoint bounds - y -= 1; - if (dparam > 0) { - const yf: f64 = @floatFromInt(y); - dparam -= 2 * sq(rxf) * yf + sq(rxf); - } else { - x += 1; - const xf: f64 = @floatFromInt(x); - const yf: f64 = @floatFromInt(y); - dparam += 2 * sq(ryf) * xf - 2 * sq(rxf) * yf + sq(rxf); - } - } - } - - // Fill - { - const u_height: u32 = @intCast(height); - const u_width: u32 = @intCast(width); - - for (0..u_height) |yf| { - for (0..u_width) |left| { - // Count forward from the left to the first filled pixel - if (points[yf * u_width + left] != 0) { - // Count back to our left point from the right to the first - // filled pixel on the other side. - var right: usize = u_width - 1; - while (right > left) : (right -= 1) { - if (points[yf * u_width + right] != 0) { - break; - } - } - - // Start filling 1 index after the left and go until we hit - // the right; this will be a no-op if the line length is < - // 3 as both left and right will have already been filled. - const start = yf * u_width + left; - const end = yf * u_width + right; - if (end - start >= 3) { - for (start + 1..end) |idx| { - points[idx] = 0xFF; - } - } - } - } - } - } - - // Now that we have our points, we need to "split" our matrix on the x - // axis for the downsample. - { - // The side of the circle we're drawing - const offset_j: u32 = if (cp == 0xE0B4) center_x + 1 else 0; - - for (0..self.height) |r| { - for (0..self.width) |c| { - var total: u32 = 0; - for (0..supersample) |i| { - for (0..supersample) |j| { - const idx = (r * supersample + i) * width + (c * supersample + j + offset_j); - total += points[idx]; - } - } - - const average = @as(u8, @intCast(@min(total / (supersample * supersample), 0xFF))); - canvas.rect( - .{ - .x = @intCast(c), - .y = @intCast(r), - .width = 1, - .height = 1, - }, - @as(font.sprite.Color, @enumFromInt(average)), - ); - } - } - } -} - -fn draw_trapezoid_top_bottom(self: Powerline, canvas: *font.sprite.Canvas, cp: u32) !void { - const t_top: Quad(f64) = if (cp == 0xE0D4) - .{ - .p0 = .{ - .x = 0, - .y = 0, - }, - .p1 = .{ - .x = @floatFromInt(self.width - self.width / 3), - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p2 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p3 = .{ - .x = @floatFromInt(self.width), - .y = 0, - }, - } - else - .{ - .p0 = .{ - .x = 0, - .y = 0, - }, - .p1 = .{ - .x = 0, - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p2 = .{ - .x = @floatFromInt(self.width / 3), - .y = @floatFromInt(self.height / 2 - self.height / 20), - }, - .p3 = .{ - .x = @floatFromInt(self.width), - .y = 0, - }, - }; - - const t_bottom: Quad(f64) = if (cp == 0xE0D4) - .{ - .p0 = .{ - .x = @floatFromInt(self.width - self.width / 3), - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - .p1 = .{ - .x = 0, - .y = @floatFromInt(self.height), - }, - .p2 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height), - }, - .p3 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - } - else - .{ - .p0 = .{ - .x = 0, - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - .p1 = .{ - .x = 0, - .y = @floatFromInt(self.height), - }, - .p2 = .{ - .x = @floatFromInt(self.width), - .y = @floatFromInt(self.height), - }, - .p3 = .{ - .x = @floatFromInt(self.width / 3), - .y = @floatFromInt(self.height / 2 + self.height / 20), - }, - }; - - try canvas.quad(t_top, .on); - try canvas.quad(t_bottom, .on); -} - -test "all" { - const testing = std.testing; - const alloc = testing.allocator; - - const cps = [_]u32{ - 0xE0B0, - 0xE0B2, - 0xE0B8, - 0xE0BA, - 0xE0BC, - 0xE0BE, - 0xE0B4, - 0xE0B6, - 0xE0D2, - 0xE0D4, - 0xE0B1, - 0xE0B3, - }; - for (cps) |cp| { - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - const face: Powerline = .{ .width = 18, .height = 36, .thickness = 2 }; - const glyph = try face.renderGlyph( - alloc, - &atlas_grayscale, - cp, - ); - try testing.expectEqual(@as(u32, face.width), glyph.width); - try testing.expectEqual(@as(u32, face.height), glyph.height); - } -} diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index a5ca7b290..b981449bc 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -81,19 +81,39 @@ pub const Canvas = struct { /// The underlying z2d surface. sfc: z2d.Surface, + padding_x: u32, + padding_y: u32, + + clip_top: u32 = 0, + clip_left: u32 = 0, + clip_right: u32 = 0, + clip_bottom: u32 = 0, + alloc: Allocator, - pub fn init(alloc: Allocator, width: u32, height: u32) !Canvas { + pub fn init( + alloc: Allocator, + width: u32, + height: u32, + padding_x: u32, + padding_y: u32, + ) !Canvas { // Create the surface we'll be using. + // We add padding to both sides (hence `2 *`) const sfc = try z2d.Surface.initPixel( .{ .alpha8 = .{ .a = 0 } }, alloc, - @intCast(width), - @intCast(height), + @intCast(width + 2 * padding_x), + @intCast(height + 2 * padding_y), ); errdefer sfc.deinit(alloc); - return .{ .sfc = sfc, .alloc = alloc }; + return .{ + .sfc = sfc, + .padding_x = padding_x, + .padding_y = padding_y, + .alloc = alloc, + }; } pub fn deinit(self: *Canvas) void { @@ -109,30 +129,33 @@ pub const Canvas = struct { ) (Allocator.Error || font.Atlas.Error)!font.Atlas.Region { assert(atlas.format == .grayscale); - const width = @as(u32, @intCast(self.sfc.getWidth())); - const height = @as(u32, @intCast(self.sfc.getHeight())); + self.trim(); + + const sfc_width: u32 = @intCast(self.sfc.getWidth()); + const sfc_height: u32 = @intCast(self.sfc.getHeight()); + + // Subtract our clip margins from the + // width and height to get region size. + const region_width = sfc_width -| self.clip_left -| self.clip_right; + const region_height = sfc_height -| self.clip_top -| self.clip_bottom; // Allocate our texture atlas region const region = region: { - // We need to add a 1px padding to the font so that we don't - // get fuzzy issues when blending textures. - const padding = 1; - - // Get the full padded region + // Reserve a region with a 1px margin on the bottom and right edges + // so that we can avoid interpolation between adjacent glyphs during + // texture sampling. var region = try atlas.reserve( alloc, - width + (padding * 2), // * 2 because left+right - height + (padding * 2), // * 2 because top+bottom + region_width + 1, + region_height + 1, ); - // Modify the region so that we remove the padding so that - // we write to the non-zero location. The data in an Altlas - // is always initialized to zero (Atlas.clear) so we don't - // need to worry about zero-ing that. - region.x += padding; - region.y += padding; - region.width -= padding * 2; - region.height -= padding * 2; + // Modify the region to remove the margin so that we write to the + // non-zero location. The data in an Altlas is always initialized + // to zero (Atlas.clear) so we don't need to worry about zero-ing + // that. + region.width -= 1; + region.height -= 1; break :region region; }; @@ -140,38 +163,138 @@ pub const Canvas = struct { const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf); // Write the glyph information into the atlas - assert(region.width == width); - assert(region.height == height); - atlas.set(region, buffer); + assert(region.width == region_width); + assert(region.height == region_height); + atlas.setFromLarger( + region, + buffer, + sfc_width, + self.clip_left, + self.clip_top, + ); } return region; } + // Adjust clip boundaries to trim off any fully transparent rows or columns. + // This circumvents abstractions from z2d so that it can be performant. + fn trim(self: *Canvas) void { + const width: u32 = @intCast(self.sfc.getWidth()); + const height: u32 = @intCast(self.sfc.getHeight()); + + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + + top: while (self.clip_top < height - self.clip_bottom) { + const y = self.clip_top; + const x0 = self.clip_left; + const x1 = width - self.clip_right; + for (buf[y * width ..][x0..x1]) |v| { + if (v != 0) break :top; + } + self.clip_top += 1; + } + + bottom: while (self.clip_bottom < height - self.clip_top) { + const y = height - self.clip_bottom -| 1; + const x0 = self.clip_left; + const x1 = width - self.clip_right; + for (buf[y * width ..][x0..x1]) |v| { + if (v != 0) break :bottom; + } + self.clip_bottom += 1; + } + + left: while (self.clip_left < width - self.clip_right) { + const x = self.clip_left; + const y0 = self.clip_top; + const y1 = height - self.clip_bottom; + for (y0..y1) |y| { + if (buf[y * width + x] != 0) break :left; + } + self.clip_left += 1; + } + + right: while (self.clip_right < width - self.clip_left) { + const x = width - self.clip_right -| 1; + const y0 = self.clip_top; + const y1 = height - self.clip_bottom; + for (y0..y1) |y| { + if (buf[y * width + x] != 0) break :right; + } + self.clip_right += 1; + } + } + + /// Only really useful for test purposes, since the clipping region is + /// automatically excluded when writing to an atlas with `writeAtlas`. + pub fn clearClippingRegions(self: *Canvas) void { + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + const width: usize = @intCast(self.sfc.getWidth()); + const height: usize = @intCast(self.sfc.getHeight()); + + for (0..height) |y| { + for (0..self.clip_left) |x| { + buf[y * width + x] = 0; + } + } + + for (0..height) |y| { + for (width - self.clip_right..width) |x| { + buf[y * width + x] = 0; + } + } + + for (0..self.clip_top) |y| { + for (0..width) |x| { + buf[y * width + x] = 0; + } + } + + for (height - self.clip_bottom..height) |y| { + for (0..width) |x| { + buf[y * width + x] = 0; + } + } + } + + /// Return a transformation representing the translation for our padding. + pub fn transformation(self: Canvas) z2d.Transformation { + return .{ + .ax = 1, + .by = 0, + .cx = 0, + .dy = 1, + .tx = @as(f64, @floatFromInt(self.padding_x)), + .ty = @as(f64, @floatFromInt(self.padding_y)), + }; + } + /// Acquires a z2d drawing context, caller MUST deinit context. pub fn getContext(self: *Canvas) z2d.Context { - return .init(self.alloc, &self.sfc); + var ctx = z2d.Context.init(self.alloc, &self.sfc); + // Offset by our padding to keep + // coordinates relative to the cell. + ctx.setTransformation(self.transformation()); + return ctx; } /// Draw and fill a single pixel - pub fn pixel(self: *Canvas, x: u32, y: u32, color: Color) void { + pub fn pixel(self: *Canvas, x: i32, y: i32, color: Color) void { self.sfc.putPixel( - @intCast(x), - @intCast(y), + x + @as(i32, @intCast(self.padding_x)), + y + @as(i32, @intCast(self.padding_y)), .{ .alpha8 = .{ .a = @intFromEnum(color) } }, ); } /// Draw and fill a rectangle. This is the main primitive for drawing /// lines as well (which are just generally skinny rectangles...) - pub fn rect(self: *Canvas, v: Rect(u32), color: Color) void { - const x0 = v.x; - const x1 = v.x + v.width; - const y0 = v.y; - const y1 = v.y + v.height; - - for (y0..y1) |y| { - for (x0..x1) |x| { + pub fn rect(self: *Canvas, v: Rect(i32), color: Color) void { + var y = v.y; + while (y < v.y + v.height) : (y += 1) { + var x = v.x; + while (x < v.x + v.width) : (x += 1) { self.pixel( @intCast(x), @intCast(y), @@ -181,96 +304,226 @@ pub const Canvas = struct { } } + /// Convenience wrapper for `Canvas.rect` + pub fn box( + self: *Canvas, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + color: Color, + ) void { + self.rect((Box(i32){ + .p0 = .{ .x = x0, .y = y0 }, + .p1 = .{ .x = x1, .y = y1 }, + }).rect(), color); + } + /// Draw and fill a quad. pub fn quad(self: *Canvas, q: Quad(f64), color: Color) !void { - var path: z2d.StaticPath(6) = .{}; - path.init(); // nodes.len = 0 - + var path = self.staticPath(6); // nodes.len = 0 path.moveTo(q.p0.x, q.p0.y); // +1, nodes.len = 1 path.lineTo(q.p1.x, q.p1.y); // +1, nodes.len = 2 path.lineTo(q.p2.x, q.p2.y); // +1, nodes.len = 3 path.lineTo(q.p3.x, q.p3.y); // +1, nodes.len = 4 path.close(); // +2, nodes.len = 6 - - try z2d.painter.fill( - self.alloc, - &self.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, - } }, - path.wrapped_path.nodes.items, - .{}, - ); + try self.fillPath(path.wrapped_path, .{}, color); } /// Draw and fill a triangle. pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void { - var path: z2d.StaticPath(5) = .{}; - path.init(); // nodes.len = 0 - + var path = self.staticPath(5); // nodes.len = 0 path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1 path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2 path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3 path.close(); // +2, nodes.len = 5 + try self.fillPath(path.wrapped_path, .{}, color); + } + /// Stroke a line. + pub fn line( + self: *Canvas, + l: Line(f64), + thickness: f64, + color: Color, + ) !void { + var path = self.staticPath(2); // nodes.len = 0 + path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1 + path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2 + try self.strokePath( + path.wrapped_path, + .{ + .line_cap_mode = .butt, + .line_width = thickness, + }, + color, + ); + } + + /// Create a static path of the provided len and initialize it. + /// Use this function instead of making the path manually since + /// it ensures that the transform is applied. + pub inline fn staticPath( + self: *Canvas, + comptime len: usize, + ) z2d.StaticPath(len) { + var path: z2d.StaticPath(len) = .{}; + path.init(); + path.wrapped_path.transformation = self.transformation(); + return path; + } + + /// Stroke a z2d path. + pub fn strokePath( + self: *Canvas, + path: z2d.Path, + opts: z2d.painter.StrokeOpts, + color: Color, + ) z2d.painter.StrokeError!void { + try z2d.painter.stroke( + self.alloc, + &self.sfc, + &.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, + } }, + path.nodes.items, + opts, + ); + } + + /// Do an inner stroke on a z2d path, right now this involves a pretty + /// heavy workaround that uses two extra surfaces; in the future, z2d + /// should add inner and outer strokes natively. + pub fn innerStrokePath( + self: *Canvas, + path: z2d.Path, + opts: z2d.painter.StrokeOpts, + color: Color, + ) (z2d.painter.StrokeError || z2d.painter.FillError)!void { + // On one surface we fill the shape, this will be a mask we + // multiply with the double-width stroke so that only the + // part inside is used. + var fill_sfc: z2d.Surface = try .init( + .image_surface_alpha8, + self.alloc, + self.sfc.getWidth(), + self.sfc.getHeight(), + ); + defer fill_sfc.deinit(self.alloc); + + // On the other we'll do the double width stroke. + var stroke_sfc: z2d.Surface = try .init( + .image_surface_alpha8, + self.alloc, + self.sfc.getWidth(), + self.sfc.getHeight(), + ); + defer stroke_sfc.deinit(self.alloc); + + // Make a closed version of the path for our fill, so + // that we can support open paths for inner stroke. + var closed_path = path; + closed_path.nodes = try path.nodes.clone(self.alloc); + defer closed_path.deinit(self.alloc); + try closed_path.close(self.alloc); + + // Fill the shape in white to the fill surface, we use + // white because this is a mask that we'll multiply with + // the stroke, we want everything inside to be the stroke + // color. + try z2d.painter.fill( + self.alloc, + &fill_sfc, + &.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = 255 } }, + } }, + closed_path.nodes.items, + .{}, + ); + + // Stroke the shape with double the desired width. + var mut_opts = opts; + mut_opts.line_width *= 2; + try z2d.painter.stroke( + self.alloc, + &stroke_sfc, + &.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, + } }, + path.nodes.items, + mut_opts, + ); + + // We multiply the stroke sfc on to the fill surface. + // The z2d composite operation doesn't seem to work for + // this with alpha8 surfaces, so we have to do it manually. + for ( + std.mem.sliceAsBytes(fill_sfc.image_surface_alpha8.buf), + std.mem.sliceAsBytes(stroke_sfc.image_surface_alpha8.buf), + ) |*d, s| { + d.* = @intFromFloat(@round( + 255.0 * + (@as(f64, @floatFromInt(s)) / 255.0) * + (@as(f64, @floatFromInt(d.*)) / 255.0), + )); + } + + // Then we composite the result on to the main surface. + self.sfc.composite(&fill_sfc, .src_over, 0, 0, .{}); + } + + /// Fill a z2d path. + pub fn fillPath( + self: *Canvas, + path: z2d.Path, + opts: z2d.painter.FillOpts, + color: Color, + ) z2d.painter.FillError!void { try z2d.painter.fill( self.alloc, &self.sfc, &.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, } }, - path.wrapped_path.nodes.items, - .{}, - ); - } - - pub fn triangle_outline(self: *Canvas, t: Triangle(f64), thickness: f64, color: Color) !void { - var path: z2d.StaticPath(3) = .{}; - path.init(); // nodes.len = 0 - - path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1 - path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2 - path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3 - - try z2d.painter.stroke( - self.alloc, - &self.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, - } }, - path.wrapped_path.nodes.items, - .{ - .line_cap_mode = .round, - .line_width = thickness, - }, - ); - } - - /// Stroke a line. - pub fn line(self: *Canvas, l: Line(f64), thickness: f64, color: Color) !void { - var path: z2d.StaticPath(2) = .{}; - path.init(); // nodes.len = 0 - - path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1 - path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2 - - try z2d.painter.stroke( - self.alloc, - &self.sfc, - &.{ .opaque_pattern = .{ - .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, - } }, - path.wrapped_path.nodes.items, - .{ - .line_cap_mode = .round, - .line_width = thickness, - }, + path.nodes.items, + opts, ); } + /// Invert all pixels on the canvas. pub fn invert(self: *Canvas) void { for (std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf)) |*v| { v.* = 255 - v.*; } } + + /// Mirror the canvas horizontally. + pub fn flipHorizontal(self: *Canvas) Allocator.Error!void { + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + const clone = try self.alloc.dupe(u8, buf); + defer self.alloc.free(clone); + const width: usize = @intCast(self.sfc.getWidth()); + const height: usize = @intCast(self.sfc.getHeight()); + for (0..height) |y| { + for (0..width) |x| { + buf[y * width + x] = clone[y * width + width - x - 1]; + } + } + std.mem.swap(u32, &self.clip_left, &self.clip_right); + } + + /// Mirror the canvas vertically. + pub fn flipVertical(self: *Canvas) Allocator.Error!void { + const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf); + const clone = try self.alloc.dupe(u8, buf); + defer self.alloc.free(clone); + const width: usize = @intCast(self.sfc.getWidth()); + const height: usize = @intCast(self.sfc.getHeight()); + for (0..height) |y| { + for (0..width) |x| { + buf[y * width + x] = clone[(height - y - 1) * width + x]; + } + } + std.mem.swap(u32, &self.clip_top, &self.clip_bottom); + } }; diff --git a/src/font/sprite/cursor.zig b/src/font/sprite/cursor.zig deleted file mode 100644 index d63db624a..000000000 --- a/src/font/sprite/cursor.zig +++ /dev/null @@ -1,65 +0,0 @@ -//! This file renders cursor sprites. -const std = @import("std"); -const builtin = @import("builtin"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const font = @import("../main.zig"); -const Sprite = font.sprite.Sprite; - -/// Draw a cursor. -pub fn renderGlyph( - alloc: Allocator, - atlas: *font.Atlas, - sprite: Sprite, - width: u32, - height: u32, - thickness: u32, -) !font.Glyph { - // Make a canvas of the desired size - var canvas = try font.sprite.Canvas.init(alloc, width, height); - defer canvas.deinit(); - - // Draw the appropriate sprite - switch (sprite) { - Sprite.cursor_rect => canvas.rect(.{ - .x = 0, - .y = 0, - .width = width, - .height = height, - }, .on), - Sprite.cursor_hollow_rect => { - // left - canvas.rect(.{ .x = 0, .y = 0, .width = thickness, .height = height }, .on); - // right - canvas.rect(.{ .x = width -| thickness, .y = 0, .width = thickness, .height = height }, .on); - // top - canvas.rect(.{ .x = 0, .y = 0, .width = width, .height = thickness }, .on); - // bottom - canvas.rect(.{ .x = 0, .y = height -| thickness, .width = width, .height = thickness }, .on); - }, - Sprite.cursor_bar => canvas.rect(.{ - .x = 0, - .y = 0, - .width = thickness, - .height = height, - }, .on), - else => unreachable, - } - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - return font.Glyph{ - // HACK: Set the width for the bar cursor to just the thickness, - // this is just for the benefit of the custom shader cursor - // uniform code. -- In the future code will be introduced to - // auto-crop the canvas so that this isn't needed. - .width = if (sprite == .cursor_bar) thickness else width, - .height = height, - .offset_x = 0, - .offset_y = @intCast(height), - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(width), - }; -} diff --git a/src/font/sprite/draw/README.md b/src/font/sprite/draw/README.md new file mode 100644 index 000000000..c6219b83f --- /dev/null +++ b/src/font/sprite/draw/README.md @@ -0,0 +1,50 @@ +# This is a *special* directory. +The files in this directory are imported by `../Face.zig` and scanned for pub +functions with names matching a specific format, which are then used to handle +drawing specified codepoints. + +## IMPORTANT +When you add a new file here, you need to add the corresponding import in +`../Face.zig` for its draw functions to be picked up. I tried dynamically +listing these files to do this automatically but it was more pain than it +was worth. + +## `draw*` functions +Any function named `draw` or `draw_` will be used to +draw the codepoint or range of codepoints specified in the name. These are +hex-encoded values with upper case letters. + +`draw*` functions are provided with these arguments: +```zig +/// The codepoint being drawn. For single-codepoint draw functions this can +/// just be discarded, but it's needed for range draw functions to determine +/// which value in the range needs to be drawn. +cp: u32, +/// The canvas on which to draw the codepoint. +//// +/// This canvas has been prepared with an extra quarter of the width/height on +/// each edge, and its transform has been set so that [0, 0] is still the upper +/// left of the cell and [width, height] is still the bottom right; in order to +/// draw above or to the left, use negative values, and to draw below or to the +/// right use values greater than the width or the height. +/// +/// Because the canvas has been prepared this way, it's possible to draw glyphs +/// that exit the cell bounds by some amount- an example of when this is useful +/// is in drawing box-drawing diagonals, with enough overlap so that they can +/// seamlessly connect across corners of cells. +canvas: *font.sprite.Canvas, +/// The width of the cell to draw for. +width: u32, +/// The height of the cell to draw for. +height: u32, +/// The font grid metrics. +metrics: font.Metrics, +``` + +`draw*` functions may only return `DrawFnError!void` (defined in `../Face.zig`). + +## `special.zig` +The functions in `special.zig` are not for drawing unicode codepoints, +rather their names match the enum tag names in the `Sprite` enum from +`src/font/sprite.zig`. They are called with the same arguments as the +other `draw*` functions. diff --git a/src/font/sprite/draw/block.zig b/src/font/sprite/draw/block.zig new file mode 100644 index 000000000..27c6ae516 --- /dev/null +++ b/src/font/sprite/draw/block.zig @@ -0,0 +1,184 @@ +//! Block Elements | U+2580...U+259F +//! https://en.wikipedia.org/wiki/Block_Elements +//! +//! ▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏ +//! ▐░▒▓▔▕▖▗▘▙▚▛▜▝▞▟ +//! + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Shade = common.Shade; +const Quads = common.Quads; +const Alignment = common.Alignment; +const rect = common.rect; + +const font = @import("../../main.zig"); +const Sprite = @import("../../sprite.zig").Sprite; + +// Utility names for common fractions +const one_eighth: f64 = 0.125; +const one_quarter: f64 = 0.25; +const one_third: f64 = (1.0 / 3.0); +const three_eighths: f64 = 0.375; +const half: f64 = 0.5; +const five_eighths: f64 = 0.625; +const two_thirds: f64 = (2.0 / 3.0); +const three_quarters: f64 = 0.75; +const seven_eighths: f64 = 0.875; + +pub fn draw2580_259F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '▀' UPPER HALF BLOCK + 0x2580 => block(metrics, canvas, .upper, 1, half), + // '▁' LOWER ONE EIGHTH BLOCK + 0x2581 => block(metrics, canvas, .lower, 1, one_eighth), + // '▂' LOWER ONE QUARTER BLOCK + 0x2582 => block(metrics, canvas, .lower, 1, one_quarter), + // '▃' LOWER THREE EIGHTHS BLOCK + 0x2583 => block(metrics, canvas, .lower, 1, three_eighths), + // '▄' LOWER HALF BLOCK + 0x2584 => block(metrics, canvas, .lower, 1, half), + // '▅' LOWER FIVE EIGHTHS BLOCK + 0x2585 => block(metrics, canvas, .lower, 1, five_eighths), + // '▆' LOWER THREE QUARTERS BLOCK + 0x2586 => block(metrics, canvas, .lower, 1, three_quarters), + // '▇' LOWER SEVEN EIGHTHS BLOCK + 0x2587 => block(metrics, canvas, .lower, 1, seven_eighths), + // '█' FULL BLOCK + 0x2588 => fullBlockShade(metrics, canvas, .on), + // '▉' LEFT SEVEN EIGHTHS BLOCK + 0x2589 => block(metrics, canvas, .left, seven_eighths, 1), + // '▊' LEFT THREE QUARTERS BLOCK + 0x258a => block(metrics, canvas, .left, three_quarters, 1), + // '▋' LEFT FIVE EIGHTHS BLOCK + 0x258b => block(metrics, canvas, .left, five_eighths, 1), + // '▌' LEFT HALF BLOCK + 0x258c => block(metrics, canvas, .left, half, 1), + // '▍' LEFT THREE EIGHTHS BLOCK + 0x258d => block(metrics, canvas, .left, three_eighths, 1), + // '▎' LEFT ONE QUARTER BLOCK + 0x258e => block(metrics, canvas, .left, one_quarter, 1), + // '▏' LEFT ONE EIGHTH BLOCK + 0x258f => block(metrics, canvas, .left, one_eighth, 1), + + // '▐' RIGHT HALF BLOCK + 0x2590 => block(metrics, canvas, .right, half, 1), + // '░' + 0x2591 => fullBlockShade(metrics, canvas, .light), + // '▒' + 0x2592 => fullBlockShade(metrics, canvas, .medium), + // '▓' + 0x2593 => fullBlockShade(metrics, canvas, .dark), + // '▔' UPPER ONE EIGHTH BLOCK + 0x2594 => block(metrics, canvas, .upper, 1, one_eighth), + // '▕' RIGHT ONE EIGHTH BLOCK + 0x2595 => block(metrics, canvas, .right, one_eighth, 1), + // '▖' + 0x2596 => quadrant(metrics, canvas, .{ .bl = true }), + // '▗' + 0x2597 => quadrant(metrics, canvas, .{ .br = true }), + // '▘' + 0x2598 => quadrant(metrics, canvas, .{ .tl = true }), + // '▙' + 0x2599 => quadrant(metrics, canvas, .{ .tl = true, .bl = true, .br = true }), + // '▚' + 0x259a => quadrant(metrics, canvas, .{ .tl = true, .br = true }), + // '▛' + 0x259b => quadrant(metrics, canvas, .{ .tl = true, .tr = true, .bl = true }), + // '▜' + 0x259c => quadrant(metrics, canvas, .{ .tl = true, .tr = true, .br = true }), + // '▝' + 0x259d => quadrant(metrics, canvas, .{ .tr = true }), + // '▞' + 0x259e => quadrant(metrics, canvas, .{ .tr = true, .bl = true }), + // '▟' + 0x259f => quadrant(metrics, canvas, .{ .tr = true, .bl = true, .br = true }), + + else => unreachable, + } +} + +pub fn block( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime alignment: Alignment, + comptime width: f64, + comptime height: f64, +) void { + blockShade(metrics, canvas, alignment, width, height, .on); +} + +pub fn blockShade( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime alignment: Alignment, + comptime width: f64, + comptime height: f64, + comptime shade: Shade, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const w: u32 = @intFromFloat(@round(float_width * width)); + const h: u32 = @intFromFloat(@round(float_height * height)); + + const x = switch (alignment.horizontal) { + .left => 0, + .right => metrics.cell_width - w, + .center => (metrics.cell_width - w) / 2, + }; + const y = switch (alignment.vertical) { + .top => 0, + .bottom => metrics.cell_height - h, + .middle => (metrics.cell_height - h) / 2, + }; + + canvas.rect(.{ + .x = @intCast(x), + .y = @intCast(y), + .width = @intCast(w), + .height = @intCast(h), + }, @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade)))); +} + +pub fn fullBlockShade( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + shade: Shade, +) void { + canvas.box( + 0, + 0, + @intCast(metrics.cell_width), + @intCast(metrics.cell_height), + @as(font.sprite.Color, @enumFromInt(@intFromEnum(shade))), + ); +} + +fn quadrant( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime quads: Quads, +) void { + const center_x = metrics.cell_width / 2 + metrics.cell_width % 2; + const center_y = metrics.cell_height / 2 + metrics.cell_height % 2; + + if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y); + if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y); + if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height); + if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height); +} diff --git a/src/font/sprite/draw/box.zig b/src/font/sprite/draw/box.zig new file mode 100644 index 000000000..91d78d2b2 --- /dev/null +++ b/src/font/sprite/draw/box.zig @@ -0,0 +1,947 @@ +//! Box Drawing | U+2500...U+257F +//! https://en.wikipedia.org/wiki/Box_Drawing +//! +//! ─━│┃┄┅┆┇┈┉┊┋┌┍┎┏ +//! ┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟ +//! ┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯ +//! ┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿ +//! ╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏ +//! ═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟ +//! ╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯ +//! ╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿ +//! + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Shade = common.Shade; +const Quads = common.Quads; +const Corner = common.Corner; +const Edge = common.Edge; +const Alignment = common.Alignment; +const rect = common.rect; +const hline = common.hline; +const vline = common.vline; +const hlineMiddle = common.hlineMiddle; +const vlineMiddle = common.vlineMiddle; + +const font = @import("../../main.zig"); +const Sprite = @import("../../sprite.zig").Sprite; + +/// Specification of a traditional intersection-style line/box-drawing char, +/// which can have a different style of line from each edge to the center. +pub const Lines = packed struct(u8) { + up: Style = .none, + right: Style = .none, + down: Style = .none, + left: Style = .none, + + const Style = enum(u2) { + none, + light, + heavy, + double, + }; +}; + +pub fn draw2500_257F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '─' + 0x2500 => linesChar(metrics, canvas, .{ .left = .light, .right = .light }), + // '━' + 0x2501 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .heavy }), + // '│' + 0x2502 => linesChar(metrics, canvas, .{ .up = .light, .down = .light }), + // '┃' + 0x2503 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy }), + // '┄' + 0x2504 => dashHorizontal( + metrics, + canvas, + 3, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┅' + 0x2505 => dashHorizontal( + metrics, + canvas, + 3, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┆' + 0x2506 => dashVertical( + metrics, + canvas, + 3, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┇' + 0x2507 => dashVertical( + metrics, + canvas, + 3, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┈' + 0x2508 => dashHorizontal( + metrics, + canvas, + 4, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┉' + 0x2509 => dashHorizontal( + metrics, + canvas, + 4, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┊' + 0x250a => dashVertical( + metrics, + canvas, + 4, + Thickness.light.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┋' + 0x250b => dashVertical( + metrics, + canvas, + 4, + Thickness.heavy.height(metrics.box_thickness), + @max(4, Thickness.light.height(metrics.box_thickness)), + ), + // '┌' + 0x250c => linesChar(metrics, canvas, .{ .down = .light, .right = .light }), + // '┍' + 0x250d => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy }), + // '┎' + 0x250e => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light }), + // '┏' + 0x250f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .heavy }), + + // '┐' + 0x2510 => linesChar(metrics, canvas, .{ .down = .light, .left = .light }), + // '┑' + 0x2511 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy }), + // '┒' + 0x2512 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light }), + // '┓' + 0x2513 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy }), + // '└' + 0x2514 => linesChar(metrics, canvas, .{ .up = .light, .right = .light }), + // '┕' + 0x2515 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy }), + // '┖' + 0x2516 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light }), + // '┗' + 0x2517 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .heavy }), + // '┘' + 0x2518 => linesChar(metrics, canvas, .{ .up = .light, .left = .light }), + // '┙' + 0x2519 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy }), + // '┚' + 0x251a => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light }), + // '┛' + 0x251b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy }), + // '├' + 0x251c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .light }), + // '┝' + 0x251d => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .heavy }), + // '┞' + 0x251e => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light, .down = .light }), + // '┟' + 0x251f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light, .up = .light }), + + // '┠' + 0x2520 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .light }), + // '┡' + 0x2521 => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy, .up = .heavy }), + // '┢' + 0x2522 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy, .down = .heavy }), + // '┣' + 0x2523 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .heavy }), + // '┤' + 0x2524 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light }), + // '┥' + 0x2525 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy }), + // '┦' + 0x2526 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .down = .light }), + // '┧' + 0x2527 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .up = .light }), + // '┨' + 0x2528 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light }), + // '┩' + 0x2529 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .up = .heavy }), + // '┪' + 0x252a => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .down = .heavy }), + // '┫' + 0x252b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy }), + // '┬' + 0x252c => linesChar(metrics, canvas, .{ .down = .light, .left = .light, .right = .light }), + // '┭' + 0x252d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .down = .light }), + // '┮' + 0x252e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .down = .light }), + // '┯' + 0x252f => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .right = .heavy }), + + // '┰' + 0x2530 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .right = .light }), + // '┱' + 0x2531 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .down = .heavy }), + // '┲' + 0x2532 => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .down = .heavy }), + // '┳' + 0x2533 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy, .right = .heavy }), + // '┴' + 0x2534 => linesChar(metrics, canvas, .{ .up = .light, .left = .light, .right = .light }), + // '┵' + 0x2535 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light }), + // '┶' + 0x2536 => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light }), + // '┷' + 0x2537 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .right = .heavy }), + // '┸' + 0x2538 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .right = .light }), + // '┹' + 0x2539 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy }), + // '┺' + 0x253a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy }), + // '┻' + 0x253b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy, .right = .heavy }), + // '┼' + 0x253c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light, .right = .light }), + // '┽' + 0x253d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light, .down = .light }), + // '┾' + 0x253e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light, .down = .light }), + // '┿' + 0x253f => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy, .right = .heavy }), + + // '╀' + 0x2540 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light, .left = .light, .right = .light }), + // '╁' + 0x2541 => linesChar(metrics, canvas, .{ .down = .heavy, .up = .light, .left = .light, .right = .light }), + // '╂' + 0x2542 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }), + // '╃' + 0x2543 => linesChar(metrics, canvas, .{ .left = .heavy, .up = .heavy, .right = .light, .down = .light }), + // '╄' + 0x2544 => linesChar(metrics, canvas, .{ .right = .heavy, .up = .heavy, .left = .light, .down = .light }), + // '╅' + 0x2545 => linesChar(metrics, canvas, .{ .left = .heavy, .down = .heavy, .right = .light, .up = .light }), + // '╆' + 0x2546 => linesChar(metrics, canvas, .{ .right = .heavy, .down = .heavy, .left = .light, .up = .light }), + // '╇' + 0x2547 => linesChar(metrics, canvas, .{ .down = .light, .up = .heavy, .left = .heavy, .right = .heavy }), + // '╈' + 0x2548 => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy, .left = .heavy, .right = .heavy }), + // '╉' + 0x2549 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy, .down = .heavy }), + // '╊' + 0x254a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy, .down = .heavy }), + // '╋' + 0x254b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy, .right = .heavy }), + // '╌' + 0x254c => dashHorizontal( + metrics, + canvas, + 2, + Thickness.light.height(metrics.box_thickness), + Thickness.light.height(metrics.box_thickness), + ), + // '╍' + 0x254d => dashHorizontal( + metrics, + canvas, + 2, + Thickness.heavy.height(metrics.box_thickness), + Thickness.heavy.height(metrics.box_thickness), + ), + // '╎' + 0x254e => dashVertical( + metrics, + canvas, + 2, + Thickness.light.height(metrics.box_thickness), + Thickness.heavy.height(metrics.box_thickness), + ), + // '╏' + 0x254f => dashVertical( + metrics, + canvas, + 2, + Thickness.heavy.height(metrics.box_thickness), + Thickness.heavy.height(metrics.box_thickness), + ), + + // '═' + 0x2550 => linesChar(metrics, canvas, .{ .left = .double, .right = .double }), + // '║' + 0x2551 => linesChar(metrics, canvas, .{ .up = .double, .down = .double }), + // '╒' + 0x2552 => linesChar(metrics, canvas, .{ .down = .light, .right = .double }), + // '╓' + 0x2553 => linesChar(metrics, canvas, .{ .down = .double, .right = .light }), + // '╔' + 0x2554 => linesChar(metrics, canvas, .{ .down = .double, .right = .double }), + // '╕' + 0x2555 => linesChar(metrics, canvas, .{ .down = .light, .left = .double }), + // '╖' + 0x2556 => linesChar(metrics, canvas, .{ .down = .double, .left = .light }), + // '╗' + 0x2557 => linesChar(metrics, canvas, .{ .down = .double, .left = .double }), + // '╘' + 0x2558 => linesChar(metrics, canvas, .{ .up = .light, .right = .double }), + // '╙' + 0x2559 => linesChar(metrics, canvas, .{ .up = .double, .right = .light }), + // '╚' + 0x255a => linesChar(metrics, canvas, .{ .up = .double, .right = .double }), + // '╛' + 0x255b => linesChar(metrics, canvas, .{ .up = .light, .left = .double }), + // '╜' + 0x255c => linesChar(metrics, canvas, .{ .up = .double, .left = .light }), + // '╝' + 0x255d => linesChar(metrics, canvas, .{ .up = .double, .left = .double }), + // '╞' + 0x255e => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .double }), + // '╟' + 0x255f => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .light }), + + // '╠' + 0x2560 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .double }), + // '╡' + 0x2561 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double }), + // '╢' + 0x2562 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light }), + // '╣' + 0x2563 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double }), + // '╤' + 0x2564 => linesChar(metrics, canvas, .{ .down = .light, .left = .double, .right = .double }), + // '╥' + 0x2565 => linesChar(metrics, canvas, .{ .down = .double, .left = .light, .right = .light }), + // '╦' + 0x2566 => linesChar(metrics, canvas, .{ .down = .double, .left = .double, .right = .double }), + // '╧' + 0x2567 => linesChar(metrics, canvas, .{ .up = .light, .left = .double, .right = .double }), + // '╨' + 0x2568 => linesChar(metrics, canvas, .{ .up = .double, .left = .light, .right = .light }), + // '╩' + 0x2569 => linesChar(metrics, canvas, .{ .up = .double, .left = .double, .right = .double }), + // '╪' + 0x256a => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double, .right = .double }), + // '╫' + 0x256b => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light, .right = .light }), + // '╬' + 0x256c => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }), + // '╭' + 0x256d => try arc(metrics, canvas, .br, .light), + // '╮' + 0x256e => try arc(metrics, canvas, .bl, .light), + // '╯' + 0x256f => try arc(metrics, canvas, .tl, .light), + + // '╰' + 0x2570 => try arc(metrics, canvas, .tr, .light), + // '╱' + 0x2571 => lightDiagonalUpperRightToLowerLeft(metrics, canvas), + // '╲' + 0x2572 => lightDiagonalUpperLeftToLowerRight(metrics, canvas), + // '╳' + 0x2573 => lightDiagonalCross(metrics, canvas), + // '╴' + 0x2574 => linesChar(metrics, canvas, .{ .left = .light }), + // '╵' + 0x2575 => linesChar(metrics, canvas, .{ .up = .light }), + // '╶' + 0x2576 => linesChar(metrics, canvas, .{ .right = .light }), + // '╷' + 0x2577 => linesChar(metrics, canvas, .{ .down = .light }), + // '╸' + 0x2578 => linesChar(metrics, canvas, .{ .left = .heavy }), + // '╹' + 0x2579 => linesChar(metrics, canvas, .{ .up = .heavy }), + // '╺' + 0x257a => linesChar(metrics, canvas, .{ .right = .heavy }), + // '╻' + 0x257b => linesChar(metrics, canvas, .{ .down = .heavy }), + // '╼' + 0x257c => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy }), + // '╽' + 0x257d => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy }), + // '╾' + 0x257e => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light }), + // '╿' + 0x257f => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light }), + + else => unreachable, + } +} + +pub fn linesChar( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + lines: Lines, +) void { + const light_px = Thickness.light.height(metrics.box_thickness); + const heavy_px = Thickness.heavy.height(metrics.box_thickness); + + // Top of light horizontal strokes + const h_light_top = (metrics.cell_height -| light_px) / 2; + // Bottom of light horizontal strokes + const h_light_bottom = h_light_top +| light_px; + + // Top of heavy horizontal strokes + const h_heavy_top = (metrics.cell_height -| heavy_px) / 2; + // Bottom of heavy horizontal strokes + const h_heavy_bottom = h_heavy_top +| heavy_px; + + // Top of the top doubled horizontal stroke (bottom is `h_light_top`) + const h_double_top = h_light_top -| light_px; + // Bottom of the bottom doubled horizontal stroke (top is `h_light_bottom`) + const h_double_bottom = h_light_bottom +| light_px; + + // Left of light vertical strokes + const v_light_left = (metrics.cell_width -| light_px) / 2; + // Right of light vertical strokes + const v_light_right = v_light_left +| light_px; + + // Left of heavy vertical strokes + const v_heavy_left = (metrics.cell_width -| heavy_px) / 2; + // Right of heavy vertical strokes + const v_heavy_right = v_heavy_left +| heavy_px; + + // Left of the left doubled vertical stroke (right is `v_light_left`) + const v_double_left = v_light_left -| light_px; + // Right of the right doubled vertical stroke (left is `v_light_right`) + const v_double_right = v_light_right +| light_px; + + // The bottom of the up line + const up_bottom = if (lines.left == .heavy or lines.right == .heavy) + h_heavy_bottom + else if (lines.left != lines.right or lines.down == lines.up) + if (lines.left == .double or lines.right == .double) + h_double_bottom + else + h_light_bottom + else if (lines.left == .none and lines.right == .none) + h_light_bottom + else + h_light_top; + + // The top of the down line + const down_top = if (lines.left == .heavy or lines.right == .heavy) + h_heavy_top + else if (lines.left != lines.right or lines.up == lines.down) + if (lines.left == .double or lines.right == .double) + h_double_top + else + h_light_top + else if (lines.left == .none and lines.right == .none) + h_light_top + else + h_light_bottom; + + // The right of the left line + const left_right = if (lines.up == .heavy or lines.down == .heavy) + v_heavy_right + else if (lines.up != lines.down or lines.left == lines.right) + if (lines.up == .double or lines.down == .double) + v_double_right + else + v_light_right + else if (lines.up == .none and lines.down == .none) + v_light_right + else + v_light_left; + + // The left of the right line + const right_left = if (lines.up == .heavy or lines.down == .heavy) + v_heavy_left + else if (lines.up != lines.down or lines.right == lines.left) + if (lines.up == .double or lines.down == .double) + v_double_left + else + v_light_left + else if (lines.up == .none and lines.down == .none) + v_light_left + else + v_light_right; + + switch (lines.up) { + .none => {}, + .light => canvas.box( + @intCast(v_light_left), + 0, + @intCast(v_light_right), + @intCast(up_bottom), + .on, + ), + .heavy => canvas.box( + @intCast(v_heavy_left), + 0, + @intCast(v_heavy_right), + @intCast(up_bottom), + .on, + ), + .double => { + const left_bottom = if (lines.left == .double) h_light_top else up_bottom; + const right_bottom = if (lines.right == .double) h_light_top else up_bottom; + + canvas.box( + @intCast(v_double_left), + 0, + @intCast(v_light_left), + @intCast(left_bottom), + .on, + ); + canvas.box( + @intCast(v_light_right), + 0, + @intCast(v_double_right), + @intCast(right_bottom), + .on, + ); + }, + } + + switch (lines.right) { + .none => {}, + .light => canvas.box( + @intCast(right_left), + @intCast(h_light_top), + @intCast(metrics.cell_width), + @intCast(h_light_bottom), + .on, + ), + .heavy => canvas.box( + @intCast(right_left), + @intCast(h_heavy_top), + @intCast(metrics.cell_width), + @intCast(h_heavy_bottom), + .on, + ), + .double => { + const top_left = if (lines.up == .double) v_light_right else right_left; + const bottom_left = if (lines.down == .double) v_light_right else right_left; + + canvas.box( + @intCast(top_left), + @intCast(h_double_top), + @intCast(metrics.cell_width), + @intCast(h_light_top), + .on, + ); + canvas.box( + @intCast(bottom_left), + @intCast(h_light_bottom), + @intCast(metrics.cell_width), + @intCast(h_double_bottom), + .on, + ); + }, + } + + switch (lines.down) { + .none => {}, + .light => canvas.box( + @intCast(v_light_left), + @intCast(down_top), + @intCast(v_light_right), + @intCast(metrics.cell_height), + .on, + ), + .heavy => canvas.box( + @intCast(v_heavy_left), + @intCast(down_top), + @intCast(v_heavy_right), + @intCast(metrics.cell_height), + .on, + ), + .double => { + const left_top = if (lines.left == .double) h_light_bottom else down_top; + const right_top = if (lines.right == .double) h_light_bottom else down_top; + + canvas.box( + @intCast(v_double_left), + @intCast(left_top), + @intCast(v_light_left), + @intCast(metrics.cell_height), + .on, + ); + canvas.box( + @intCast(v_light_right), + @intCast(right_top), + @intCast(v_double_right), + @intCast(metrics.cell_height), + .on, + ); + }, + } + + switch (lines.left) { + .none => {}, + .light => canvas.box( + 0, + @intCast(h_light_top), + @intCast(left_right), + @intCast(h_light_bottom), + .on, + ), + .heavy => canvas.box( + 0, + @intCast(h_heavy_top), + @intCast(left_right), + @intCast(h_heavy_bottom), + .on, + ), + .double => { + const top_right = if (lines.up == .double) v_light_left else left_right; + const bottom_right = if (lines.down == .double) v_light_left else left_right; + + canvas.box( + 0, + @intCast(h_double_top), + @intCast(top_right), + @intCast(h_light_top), + .on, + ); + canvas.box( + 0, + @intCast(h_light_bottom), + @intCast(bottom_right), + @intCast(h_double_bottom), + .on, + ); + }, + } +} + +pub fn lightDiagonalUpperRightToLowerLeft( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + // We overshoot the corners by a tiny bit, but we need to + // maintain the correct slope, so we calculate that here. + const slope_x: f64 = @min(1.0, float_width / float_height); + const slope_y: f64 = @min(1.0, float_height / float_width); + + canvas.line(.{ + .p0 = .{ + .x = float_width + 0.5 * slope_x, + .y = -0.5 * slope_y, + }, + .p1 = .{ + .x = -0.5 * slope_x, + .y = float_height + 0.5 * slope_y, + }, + }, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {}; +} + +pub fn lightDiagonalUpperLeftToLowerRight( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + // We overshoot the corners by a tiny bit, but we need to + // maintain the correct slope, so we calculate that here. + const slope_x: f64 = @min(1.0, float_width / float_height); + const slope_y: f64 = @min(1.0, float_height / float_width); + + canvas.line(.{ + .p0 = .{ + .x = -0.5 * slope_x, + .y = -0.5 * slope_y, + }, + .p1 = .{ + .x = float_width + 0.5 * slope_x, + .y = float_height + 0.5 * slope_y, + }, + }, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {}; +} + +pub fn lightDiagonalCross( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, +) void { + lightDiagonalUpperRightToLowerLeft(metrics, canvas); + lightDiagonalUpperLeftToLowerRight(metrics, canvas); +} + +fn quadrant( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime quads: Quads, +) void { + const center_x = metrics.cell_width / 2 + metrics.cell_width % 2; + const center_y = metrics.cell_height / 2 + metrics.cell_height % 2; + + if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y); + if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y); + if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height); + if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height); +} + +pub fn arc( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime corner: Corner, + comptime thickness: Thickness, +) !void { + const thick_px = thickness.height(metrics.box_thickness); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + const center_x: f64 = @as(f64, @floatFromInt((metrics.cell_width -| thick_px) / 2)) + float_thick / 2; + const center_y: f64 = @as(f64, @floatFromInt((metrics.cell_height -| thick_px) / 2)) + float_thick / 2; + + const r = @min(float_width, float_height) / 2; + + // Fraction away from the center to place the middle control points, + const s: f64 = 0.25; + + var path = canvas.staticPath(4); + + switch (corner) { + .tl => { + path.moveTo(center_x, 0); + path.lineTo(center_x, center_y - r); + path.curveTo( + center_x, + center_y - s * r, + center_x - s * r, + center_y, + center_x - r, + center_y, + ); + path.lineTo(0, center_y); + }, + .tr => { + path.moveTo(center_x, 0); + path.lineTo(center_x, center_y - r); + path.curveTo( + center_x, + center_y - s * r, + center_x + s * r, + center_y, + center_x + r, + center_y, + ); + path.lineTo(float_width, center_y); + }, + .bl => { + path.moveTo(center_x, float_height); + path.lineTo(center_x, center_y + r); + path.curveTo( + center_x, + center_y + s * r, + center_x - s * r, + center_y, + center_x - r, + center_y, + ); + path.lineTo(0, center_y); + }, + .br => { + path.moveTo(center_x, float_height); + path.lineTo(center_x, center_y + r); + path.curveTo( + center_x, + center_y + s * r, + center_x + s * r, + center_y, + center_x + r, + center_y, + ); + path.lineTo(float_width, center_y); + }, + } + + try canvas.strokePath( + path.wrapped_path, + .{ + .line_cap_mode = .butt, + .line_width = float_thick, + }, + .on, + ); +} + +fn dashHorizontal( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + count: u8, + thick_px: u32, + desired_gap: u32, +) void { + assert(count >= 2 and count <= 4); + + // +------------+ + // | | + // | | + // | | + // | | + // | -- -- -- | + // | | + // | | + // | | + // | | + // +------------+ + // Our dashed line should be made such that when tiled horizontally + // it creates one consistent line with no uneven gap or segment sizes. + // In order to make sure this is the case, we should have half-sized + // gaps on the left and right so that it is centered properly. + + // For N dashes, there are N - 1 gaps between them, but we also have + // half-sized gaps on either side, adding up to N total gaps. + const gap_count = count; + + // We need at least 1 pixel for each gap and each dash, if we don't + // have that then we can't draw our dashed line correctly so we just + // draw a solid line and return. + if (metrics.cell_width < count + gap_count) { + hlineMiddle(metrics, canvas, .light); + return; + } + + // We never want the gaps to take up more than 50% of the space, + // because if they do the dashes are too small and look wrong. + const gap_width: i32 = @intCast(@min(desired_gap, metrics.cell_width / (2 * count))); + const total_gap_width: i32 = gap_count * gap_width; + const total_dash_width: i32 = @as(i32, @intCast(metrics.cell_width)) - total_gap_width; + const dash_width: i32 = @divFloor(total_dash_width, count); + const remaining: i32 = @mod(total_dash_width, count); + + assert(dash_width * count + gap_width * gap_count + remaining == metrics.cell_width); + + // Our dashes should be centered vertically. + const y: i32 = @intCast((metrics.cell_height -| thick_px) / 2); + + // We start at half a gap from the left edge, in order to center + // our dashes properly. + var x: i32 = @divFloor(gap_width, 2); + + // We'll distribute the extra space in to dash widths, 1px at a + // time. We prefer this to making gaps larger since that is much + // more visually obvious. + var extra: i32 = remaining; + + for (0..count) |_| { + var x1 = x + dash_width; + // We distribute left-over size in to dash widths, + // since it's less obvious there than in the gaps. + if (extra > 0) { + extra -= 1; + x1 += 1; + } + hline(canvas, x, x1, y, thick_px); + // Advance by the width of the dash we drew and the width + // of a gap to get the the start of the next dash. + x = x1 + gap_width; + } +} + +fn dashVertical( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime count: u8, + thick_px: u32, + desired_gap: u32, +) void { + assert(count >= 2 and count <= 4); + + // +-----------+ + // | | | + // | | | + // | | + // | | | + // | | | + // | | + // | | | + // | | | + // | | + // +-----------+ + // Our dashed line should be made such that when tiled vertically it + // it creates one consistent line with no uneven gap or segment sizes. + // In order to make sure this is the case, we should have an extra gap + // gap at the bottom. + // + // A single full-sized extra gap is preferred to two half-sized ones for + // vertical to allow better joining to solid characters without creating + // visible half-sized gaps. Unlike horizontal, centering is a lot less + // important, visually. + + // Because of the extra gap at the bottom, there are as many gaps as + // there are dashes. + const gap_count = count; + + // We need at least 1 pixel for each gap and each dash, if we don't + // have that then we can't draw our dashed line correctly so we just + // draw a solid line and return. + if (metrics.cell_height < count + gap_count) { + vlineMiddle(metrics, canvas, .light); + return; + } + + // We never want the gaps to take up more than 50% of the space, + // because if they do the dashes are too small and look wrong. + const gap_height: i32 = @intCast(@min(desired_gap, metrics.cell_height / (2 * count))); + const total_gap_height: i32 = gap_count * gap_height; + const total_dash_height: i32 = @as(i32, @intCast(metrics.cell_height)) - total_gap_height; + const dash_height: i32 = @divFloor(total_dash_height, count); + const remaining: i32 = @mod(total_dash_height, count); + + assert(dash_height * count + gap_height * gap_count + remaining == metrics.cell_height); + + // Our dashes should be centered horizontally. + const x: i32 = @intCast((metrics.cell_width -| thick_px) / 2); + + // We start at the top of the cell. + var y: i32 = 0; + + // We'll distribute the extra space in to dash heights, 1px at a + // time. We prefer this to making gaps larger since that is much + // more visually obvious. + var extra: i32 = remaining; + + inline for (0..count) |_| { + var y1 = y + dash_height; + // We distribute left-over size in to dash widths, + // since it's less obvious there than in the gaps. + if (extra > 0) { + extra -= 1; + y1 += 1; + } + vline(canvas, y, y1, x, thick_px); + // Advance by the height of the dash we drew and the height + // of a gap to get the the start of the next dash. + y = y1 + gap_height; + } +} diff --git a/src/font/sprite/draw/braille.zig b/src/font/sprite/draw/braille.zig new file mode 100644 index 000000000..c756ff369 --- /dev/null +++ b/src/font/sprite/draw/braille.zig @@ -0,0 +1,148 @@ +//! Braille Patterns | U+2800...U+28FF +//! https://en.wikipedia.org/wiki/Braille_Patterns +//! +//! (6 dot patterns) +//! ⠀ ⠁ ⠂ ⠃ ⠄ ⠅ ⠆ ⠇ ⠈ ⠉ ⠊ ⠋ ⠌ ⠍ ⠎ ⠏ +//! ⠐ ⠑ ⠒ ⠓ ⠔ ⠕ ⠖ ⠗ ⠘ ⠙ ⠚ ⠛ ⠜ ⠝ ⠞ ⠟ +//! ⠠ ⠡ ⠢ ⠣ ⠤ ⠥ ⠦ ⠧ ⠨ ⠩ ⠪ ⠫ ⠬ ⠭ ⠮ ⠯ +//! ⠰ ⠱ ⠲ ⠳ ⠴ ⠵ ⠶ ⠷ ⠸ ⠹ ⠺ ⠻ ⠼ ⠽ ⠾ ⠿ +//! +//! (8 dot patterns) +//! ⡀ ⡁ ⡂ ⡃ ⡄ ⡅ ⡆ ⡇ ⡈ ⡉ ⡊ ⡋ ⡌ ⡍ ⡎ ⡏ +//! ⡐ ⡑ ⡒ ⡓ ⡔ ⡕ ⡖ ⡗ ⡘ ⡙ ⡚ ⡛ ⡜ ⡝ ⡞ ⡟ +//! ⡠ ⡡ ⡢ ⡣ ⡤ ⡥ ⡦ ⡧ ⡨ ⡩ ⡪ ⡫ ⡬ ⡭ ⡮ ⡯ +//! ⡰ ⡱ ⡲ ⡳ ⡴ ⡵ ⡶ ⡷ ⡸ ⡹ ⡺ ⡻ ⡼ ⡽ ⡾ ⡿ +//! ⢀ ⢁ ⢂ ⢃ ⢄ ⢅ ⢆ ⢇ ⢈ ⢉ ⢊ ⢋ ⢌ ⢍ ⢎ ⢏ +//! ⢐ ⢑ ⢒ ⢓ ⢔ ⢕ ⢖ ⢗ ⢘ ⢙ ⢚ ⢛ ⢜ ⢝ ⢞ ⢟ +//! ⢠ ⢡ ⢢ ⢣ ⢤ ⢥ ⢦ ⢧ ⢨ ⢩ ⢪ ⢫ ⢬ ⢭ ⢮ ⢯ +//! ⢰ ⢱ ⢲ ⢳ ⢴ ⢵ ⢶ ⢷ ⢸ ⢹ ⢺ ⢻ ⢼ ⢽ ⢾ ⢿ +//! ⣀ ⣁ ⣂ ⣃ ⣄ ⣅ ⣆ ⣇ ⣈ ⣉ ⣊ ⣋ ⣌ ⣍ ⣎ ⣏ +//! ⣐ ⣑ ⣒ ⣓ ⣔ ⣕ ⣖ ⣗ ⣘ ⣙ ⣚ ⣛ ⣜ ⣝ ⣞ ⣟ +//! ⣠ ⣡ ⣢ ⣣ ⣤ ⣥ ⣦ ⣧ ⣨ ⣩ ⣪ ⣫ ⣬ ⣭ ⣮ ⣯ +//! ⣰ ⣱ ⣲ ⣳ ⣴ ⣵ ⣶ ⣷ ⣸ ⣹ ⣺ ⣻ ⣼ ⣽ ⣾ ⣿ +//! + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const font = @import("../../main.zig"); + +/// A braille pattern. +/// +/// Mnemonic: +/// [t]op - . . +/// [u]pper - . . +/// [l]ower - . . +/// [b]ottom - . . +/// | | +/// [l]eft, [r]ight +/// +/// Struct layout matches bit patterns of unicode codepoints. +const Pattern = packed struct(u8) { + tl: bool, + ul: bool, + ll: bool, + tr: bool, + ur: bool, + lr: bool, + bl: bool, + br: bool, + + fn from(cp: u32) Pattern { + return @bitCast(@as(u8, @truncate(cp))); + } +}; + +pub fn draw2800_28FF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = metrics; + + var w: i32 = @intCast(@min(width / 4, height / 8)); + var x_spacing: i32 = @intCast(width / 4); + var y_spacing: i32 = @intCast(height / 8); + var x_margin: i32 = @divFloor(x_spacing, 2); + var y_margin: i32 = @divFloor(y_spacing, 2); + + var x_px_left: i32 = + @as(i32, @intCast(width)) - 2 * x_margin - x_spacing - 2 * w; + + var y_px_left: i32 = + @as(i32, @intCast(height)) - 2 * y_margin - 3 * y_spacing - 4 * w; + + // First, try hard to ensure the DOT width is non-zero + if (x_px_left >= 2 and y_px_left >= 4 and w == 0) { + w += 1; + x_px_left -= 2; + y_px_left -= 4; + } + + // Second, prefer a non-zero margin + if (x_px_left >= 2 and x_margin == 0) { + x_margin = 1; + x_px_left -= 2; + } + if (y_px_left >= 2 and y_margin == 0) { + y_margin = 1; + y_px_left -= 2; + } + + // Third, increase spacing + if (x_px_left >= 1) { + x_spacing += 1; + x_px_left -= 1; + } + if (y_px_left >= 3) { + y_spacing += 1; + y_px_left -= 3; + } + + // Fourth, margins (“spacing”, but on the sides) + if (x_px_left >= 2) { + x_margin += 1; + x_px_left -= 2; + } + if (y_px_left >= 2) { + y_margin += 1; + y_px_left -= 2; + } + + // Last - increase dot width + if (x_px_left >= 2 and y_px_left >= 4) { + w += 1; + x_px_left -= 2; + y_px_left -= 4; + } + + assert(x_px_left <= 1 or y_px_left <= 1); + assert(2 * x_margin + 2 * w + x_spacing <= width); + assert(2 * y_margin + 4 * w + 3 * y_spacing <= height); + + const x = [2]i32{ x_margin, x_margin + w + x_spacing }; + const y = y: { + var y: [4]i32 = undefined; + y[0] = y_margin; + y[1] = y[0] + w + y_spacing; + y[2] = y[1] + w + y_spacing; + y[3] = y[2] + w + y_spacing; + break :y y; + }; + + assert(cp >= 0x2800); + assert(cp <= 0x28ff); + const p: Pattern = .from(cp); + + if (p.tl) canvas.box(x[0], y[0], x[0] + w, y[0] + w, .on); + if (p.ul) canvas.box(x[0], y[1], x[0] + w, y[1] + w, .on); + if (p.ll) canvas.box(x[0], y[2], x[0] + w, y[2] + w, .on); + if (p.bl) canvas.box(x[0], y[3], x[0] + w, y[3] + w, .on); + if (p.tr) canvas.box(x[1], y[0], x[1] + w, y[0] + w, .on); + if (p.ur) canvas.box(x[1], y[1], x[1] + w, y[1] + w, .on); + if (p.lr) canvas.box(x[1], y[2], x[1] + w, y[2] + w, .on); + if (p.br) canvas.box(x[1], y[3], x[1] + w, y[3] + w, .on); +} diff --git a/src/font/sprite/draw/branch.zig b/src/font/sprite/draw/branch.zig new file mode 100644 index 000000000..ac7220390 --- /dev/null +++ b/src/font/sprite/draw/branch.zig @@ -0,0 +1,505 @@ +//! Branch Drawing Characters | U+F5D0...U+F60D +//! +//! Branch drawing character set, used for drawing git-like +//! graphs in the terminal. Originally implemented in Kitty. +//! Ref: +//! - https://github.com/kovidgoyal/kitty/pull/7681 +//! - https://github.com/kovidgoyal/kitty/pull/7805 +//! NOTE: Kitty is GPL licensed, and its code was not referenced +//! for these characters, only the loose specification of +//! the character set in the pull request descriptions. +//! +//!                 +//!                 +//!                 +//!               +//! + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Shade = common.Shade; +const Edge = common.Edge; +const hlineMiddle = common.hlineMiddle; +const vlineMiddle = common.vlineMiddle; + +const arc = @import("box.zig").arc; + +const font = @import("../../main.zig"); + +/// Specification of a branch drawing node, which consists of a +/// circle which is either empty or filled, and lines connecting +/// optionally between the circle and each of the 4 edges. +const BranchNode = packed struct(u5) { + up: bool = false, + right: bool = false, + down: bool = false, + left: bool = false, + filled: bool = false, +}; + +pub fn drawF5D0_F60D( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '' + 0x0f5d0 => hlineMiddle(metrics, canvas, .light), + // '' + 0x0f5d1 => vlineMiddle(metrics, canvas, .light), + // '' + 0x0f5d2 => fadingLine(metrics, canvas, .right, .light), + // '' + 0x0f5d3 => fadingLine(metrics, canvas, .left, .light), + // '' + 0x0f5d4 => fadingLine(metrics, canvas, .bottom, .light), + // '' + 0x0f5d5 => fadingLine(metrics, canvas, .top, .light), + // '' + 0x0f5d6 => try arc(metrics, canvas, .br, .light), + // '' + 0x0f5d7 => try arc(metrics, canvas, .bl, .light), + // '' + 0x0f5d8 => try arc(metrics, canvas, .tr, .light), + // '' + 0x0f5d9 => try arc(metrics, canvas, .tl, .light), + // '' + 0x0f5da => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + }, + // '' + 0x0f5db => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5dc => { + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5dd => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + }, + // '' + 0x0f5de => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5df => { + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .bl, .light); + }, + + // '' + 0x0f5e0 => { + try arc(metrics, canvas, .bl, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e1 => { + try arc(metrics, canvas, .br, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e2 => { + try arc(metrics, canvas, .br, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5e3 => { + try arc(metrics, canvas, .tl, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e4 => { + try arc(metrics, canvas, .tr, .light); + hlineMiddle(metrics, canvas, .light); + }, + // '' + 0x0f5e5 => { + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .tl, .light); + }, + // '' + 0x0f5e6 => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .tr, .light); + }, + // '' + 0x0f5e7 => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .bl, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5e8 => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .bl, .light); + try arc(metrics, canvas, .tl, .light); + }, + // '' + 0x0f5e9 => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5ea => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5eb => { + vlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5ec => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tl, .light); + try arc(metrics, canvas, .br, .light); + }, + // '' + 0x0f5ed => { + hlineMiddle(metrics, canvas, .light); + try arc(metrics, canvas, .tr, .light); + try arc(metrics, canvas, .bl, .light); + }, + // '' + 0x0f5ee => branchNode(metrics, canvas, .{ .filled = true }, .light), + // '' + 0x0f5ef => branchNode(metrics, canvas, .{}, .light), + + // '' + 0x0f5f0 => branchNode(metrics, canvas, .{ + .right = true, + .filled = true, + }, .light), + // '' + 0x0f5f1 => branchNode(metrics, canvas, .{ + .right = true, + }, .light), + // '' + 0x0f5f2 => branchNode(metrics, canvas, .{ + .left = true, + .filled = true, + }, .light), + // '' + 0x0f5f3 => branchNode(metrics, canvas, .{ + .left = true, + }, .light), + // '' + 0x0f5f4 => branchNode(metrics, canvas, .{ + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f5f5 => branchNode(metrics, canvas, .{ + .left = true, + .right = true, + }, .light), + // '' + 0x0f5f6 => branchNode(metrics, canvas, .{ + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5f7 => branchNode(metrics, canvas, .{ + .down = true, + }, .light), + // '' + 0x0f5f8 => branchNode(metrics, canvas, .{ + .up = true, + .filled = true, + }, .light), + // '' + 0x0f5f9 => branchNode(metrics, canvas, .{ + .up = true, + }, .light), + // '' + 0x0f5fa => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5fb => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + }, .light), + // '' + 0x0f5fc => branchNode(metrics, canvas, .{ + .right = true, + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5fd => branchNode(metrics, canvas, .{ + .right = true, + .down = true, + }, .light), + // '' + 0x0f5fe => branchNode(metrics, canvas, .{ + .left = true, + .down = true, + .filled = true, + }, .light), + // '' + 0x0f5ff => branchNode(metrics, canvas, .{ + .left = true, + .down = true, + }, .light), + + // '' + 0x0f600 => branchNode(metrics, canvas, .{ + .up = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f601 => branchNode(metrics, canvas, .{ + .up = true, + .right = true, + }, .light), + // '' + 0x0f602 => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + .filled = true, + }, .light), + // '' + 0x0f603 => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + }, .light), + // '' + 0x0f604 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f605 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .right = true, + }, .light), + // '' + 0x0f606 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + .filled = true, + }, .light), + // '' + 0x0f607 => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + }, .light), + // '' + 0x0f608 => branchNode(metrics, canvas, .{ + .down = true, + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f609 => branchNode(metrics, canvas, .{ + .down = true, + .left = true, + .right = true, + }, .light), + // '' + 0x0f60a => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f60b => branchNode(metrics, canvas, .{ + .up = true, + .left = true, + .right = true, + }, .light), + // '' + 0x0f60c => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + .right = true, + .filled = true, + }, .light), + // '' + 0x0f60d => branchNode(metrics, canvas, .{ + .up = true, + .down = true, + .left = true, + .right = true, + }, .light), + + else => unreachable, + } +} + +fn branchNode( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + node: BranchNode, + comptime thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + + // Top of horizontal strokes + const h_top = (metrics.cell_height -| thick_px) / 2; + // Bottom of horizontal strokes + const h_bottom = h_top +| thick_px; + // Left of vertical strokes + const v_left = (metrics.cell_width -| thick_px) / 2; + // Right of vertical strokes + const v_right = v_left +| thick_px; + + // We calculate the center of the circle this way + // to ensure it aligns with box drawing characters + // since the lines are sometimes off center to + // make sure they aren't split between pixels. + const cx: f64 = @as(f64, @floatFromInt(v_left)) + float_thick / 2; + const cy: f64 = @as(f64, @floatFromInt(h_top)) + float_thick / 2; + // The radius needs to be the smallest distance from the center to an edge. + const r: f64 = @min( + @min(cx, cy), + @min(float_width - cx, float_height - cy), + ); + + var ctx = canvas.getContext(); + defer ctx.deinit(); + ctx.setSource(.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, + } }); + ctx.setLineWidth(float_thick); + + // These @intFromFloat casts shouldn't ever fail since r can never + // be greater than cx or cy, so when subtracting it from them the + // result can never be negative. + if (node.up) canvas.box( + @intCast(v_left), + 0, + @intCast(v_right), + @intFromFloat(@ceil(cy - r + float_thick / 2)), + .on, + ); + if (node.right) canvas.box( + @intFromFloat(@floor(cx + r - float_thick / 2)), + @intCast(h_top), + @intCast(metrics.cell_width), + @intCast(h_bottom), + .on, + ); + if (node.down) canvas.box( + @intCast(v_left), + @intFromFloat(@floor(cy + r - float_thick / 2)), + @intCast(v_right), + @intCast(metrics.cell_height), + .on, + ); + if (node.left) canvas.box( + 0, + @intCast(h_top), + @intFromFloat(@ceil(cx - r + float_thick / 2)), + @intCast(h_bottom), + .on, + ); + + if (node.filled) { + ctx.arc(cx, cy, r, 0, std.math.pi * 2) catch return; + ctx.closePath() catch return; + ctx.fill() catch return; + } else { + ctx.arc(cx, cy, r - float_thick / 2, 0, std.math.pi * 2) catch return; + ctx.closePath() catch return; + ctx.stroke() catch return; + } +} + +fn fadingLine( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime to: Edge, + comptime thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + // Top of horizontal strokes + const h_top = (metrics.cell_height -| thick_px) / 2; + // Bottom of horizontal strokes + const h_bottom = h_top +| thick_px; + // Left of vertical strokes + const v_left = (metrics.cell_width -| thick_px) / 2; + // Right of vertical strokes + const v_right = v_left +| thick_px; + + // If we're fading to the top or left, we start with 0.0 + // and increment up as we progress, otherwise we start + // at 255.0 and increment down (negative). + var color: f64 = switch (to) { + .top, .left => 0.0, + .bottom, .right => 255.0, + }; + const inc: f64 = 255.0 / switch (to) { + .top => float_height, + .bottom => -float_height, + .left => float_width, + .right => -float_width, + }; + + switch (to) { + .top, .bottom => { + for (0..metrics.cell_height) |y| { + for (v_left..v_right) |x| { + canvas.pixel( + @intCast(x), + @intCast(y), + @enumFromInt(@as(u8, @intFromFloat(@round(color)))), + ); + } + color += inc; + } + }, + .left, .right => { + for (0..metrics.cell_width) |x| { + for (h_top..h_bottom) |y| { + canvas.pixel( + @intCast(x), + @intCast(y), + @enumFromInt(@as(u8, @intFromFloat(@round(color)))), + ); + } + color += inc; + } + }, + } +} diff --git a/src/font/sprite/draw/common.zig b/src/font/sprite/draw/common.zig new file mode 100644 index 000000000..2f608180e --- /dev/null +++ b/src/font/sprite/draw/common.zig @@ -0,0 +1,244 @@ +//! This file contains a set of useful helper functions +//! and types for drawing our sprite font glyphs. These +//! are generally applicable to multiple sets of glyphs +//! rather than being single-use. + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const font = @import("../../main.zig"); +const Sprite = @import("../../sprite.zig").Sprite; + +const log = std.log.scoped(.sprite_font); + +// Utility names for common fractions +pub const one_eighth: f64 = 0.125; +pub const one_quarter: f64 = 0.25; +pub const one_third: f64 = (1.0 / 3.0); +pub const three_eighths: f64 = 0.375; +pub const half: f64 = 0.5; +pub const five_eighths: f64 = 0.625; +pub const two_thirds: f64 = (2.0 / 3.0); +pub const three_quarters: f64 = 0.75; +pub const seven_eighths: f64 = 0.875; + +/// The thickness of a line. +pub const Thickness = enum { + super_light, + light, + heavy, + + /// Calculate the real height of a line based on its + /// thickness and a base thickness value. The base + /// thickness value is expected to be in pixels. + pub fn height(self: Thickness, base: u32) u32 { + return switch (self) { + .super_light => @max(base / 2, 1), + .light => base, + .heavy => base * 2, + }; + } +}; + +/// Shades. +pub const Shade = enum(u8) { + off = 0x00, + light = 0x40, + medium = 0x80, + dark = 0xc0, + on = 0xff, + + _, +}; + +/// Applicable to any set of glyphs with features +/// that may be present or not in each quadrant. +pub const Quads = packed struct(u4) { + tl: bool = false, + tr: bool = false, + bl: bool = false, + br: bool = false, +}; + +/// A corner of a cell. +pub const Corner = enum(u2) { + tl, + tr, + bl, + br, +}; + +/// An edge of a cell. +pub const Edge = enum(u2) { + top, + left, + bottom, + right, +}; + +/// Alignment of a figure within a cell. +pub const Alignment = struct { + horizontal: enum { + left, + right, + center, + } = .center, + + vertical: enum { + top, + bottom, + middle, + } = .middle, + + pub const upper: Alignment = .{ .vertical = .top }; + pub const lower: Alignment = .{ .vertical = .bottom }; + pub const left: Alignment = .{ .horizontal = .left }; + pub const right: Alignment = .{ .horizontal = .right }; + + pub const upper_left: Alignment = .{ .vertical = .top, .horizontal = .left }; + pub const upper_right: Alignment = .{ .vertical = .top, .horizontal = .right }; + pub const lower_left: Alignment = .{ .vertical = .bottom, .horizontal = .left }; + pub const lower_right: Alignment = .{ .vertical = .bottom, .horizontal = .right }; + + pub const center: Alignment = .{}; + + pub const upper_center = upper; + pub const lower_center = lower; + pub const middle_left = left; + pub const middle_right = right; + pub const middle_center: Alignment = center; + + pub const top = upper; + pub const bottom = lower; + pub const center_top = top; + pub const center_bottom = bottom; + + pub const top_left = upper_left; + pub const top_right = upper_right; + pub const bottom_left = lower_left; + pub const bottom_right = lower_right; +}; + +/// Fill a rect, clamped to within the cell boundaries. +/// +/// TODO: Eliminate usages of this, prefer `canvas.box`. +pub fn rect( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + x1: u32, + y1: u32, + x2: u32, + y2: u32, +) void { + canvas.box( + @intCast(@min(@max(x1, 0), metrics.cell_width)), + @intCast(@min(@max(y1, 0), metrics.cell_height)), + @intCast(@min(@max(x2, 0), metrics.cell_width)), + @intCast(@min(@max(y2, 0), metrics.cell_height)), + .on, + ); +} + +/// Centered vertical line of the provided thickness. +pub fn vlineMiddle( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + vline( + canvas, + 0, + @intCast(metrics.cell_height), + @intCast((metrics.cell_width -| thick_px) / 2), + thick_px, + ); +} + +/// Centered horizontal line of the provided thickness. +pub fn hlineMiddle( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + thickness: Thickness, +) void { + const thick_px = thickness.height(metrics.box_thickness); + hline( + canvas, + 0, + @intCast(metrics.cell_width), + @intCast((metrics.cell_height -| thick_px) / 2), + thick_px, + ); +} + +/// Vertical line with the left edge at `x`, between `y1` and `y2`. +pub fn vline( + canvas: *font.sprite.Canvas, + y1: i32, + y2: i32, + x: i32, + thickness_px: u32, +) void { + canvas.box(x, y1, x + @as(i32, @intCast(thickness_px)), y2, .on); +} + +/// Horizontal line with the top edge at `y`, between `x1` and `x2`. +pub fn hline( + canvas: *font.sprite.Canvas, + x1: i32, + x2: i32, + y: i32, + thickness_px: u32, +) void { + canvas.box(x1, y, x2, y + @as(i32, @intCast(thickness_px)), .on); +} + +/// xHalfs[0] should be used as the right edge of a left-aligned half. +/// xHalfs[1] should be used as the left edge of a right-aligned half. +pub fn xHalfs(metrics: font.Metrics) [2]u32 { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const half_width: u32 = @intFromFloat(@round(0.5 * float_width)); + return .{ half_width, metrics.cell_width - half_width }; +} + +/// Use these values as such: +/// yThirds[0] bottom edge of the first third. +/// yThirds[1] top edge of the second third. +/// yThirds[2] bottom edge of the second third. +/// yThirds[3] top edge of the final third. +pub fn yThirds(metrics: font.Metrics) [4]u32 { + const float_height: f64 = @floatFromInt(metrics.cell_height); + const one_third_height: u32 = @intFromFloat(@round(one_third * float_height)); + const two_thirds_height: u32 = @intFromFloat(@round(two_thirds * float_height)); + return .{ + one_third_height, + metrics.cell_height - two_thirds_height, + two_thirds_height, + metrics.cell_height - one_third_height, + }; +} + +/// Use these values as such: +/// yQuads[0] bottom edge of first quarter. +/// yQuads[1] top edge of second quarter. +/// yQuads[2] bottom edge of second quarter. +/// yQuads[3] top edge of third quarter. +/// yQuads[4] bottom edge of third quarter +/// yQuads[5] top edge of fourth quarter. +pub fn yQuads(metrics: font.Metrics) [6]u32 { + const float_height: f64 = @floatFromInt(metrics.cell_height); + const quarter_height: u32 = @intFromFloat(@round(0.25 * float_height)); + const half_height: u32 = @intFromFloat(@round(0.50 * float_height)); + const three_quarters_height: u32 = @intFromFloat(@round(0.75 * float_height)); + return .{ + quarter_height, + metrics.cell_height - three_quarters_height, + half_height, + metrics.cell_height - half_height, + three_quarters_height, + metrics.cell_height - quarter_height, + }; +} diff --git a/src/font/sprite/draw/geometric_shapes.zig b/src/font/sprite/draw/geometric_shapes.zig new file mode 100644 index 000000000..d95a4fd2f --- /dev/null +++ b/src/font/sprite/draw/geometric_shapes.zig @@ -0,0 +1,200 @@ +//! Geometric Shapes | U+25A0...U+25FF +//! https://en.wikipedia.org/wiki/Geometric_Shapes_(Unicode_block) +//! +//! ■ □ ▢ ▣ ▤ ▥ ▦ ▧ ▨ ▩ ▪ ▫ ▬ ▭ ▮ ▯ +//! ▰ ▱ ▲ △ ▴ ▵ ▶ ▷ ▸ ▹ ► ▻ ▼ ▽ ▾ ▿ +//! ◀ ◁ ◂ ◃ ◄ ◅ ◆ ◇ ◈ ◉ ◊ ○ ◌ ◍ ◎ ● +//! ◐ ◑ ◒ ◓ ◔ ◕ ◖ ◗ ◘ ◙ ◚ ◛ ◜ ◝ ◞ ◟ +//! ◠ ◡ ◢ ◣ ◤ ◥ ◦ ◧ ◨ ◩ ◪ ◫ ◬ ◭ ◮ ◯ +//! ◰ ◱ ◲ ◳ ◴ ◵ ◶ ◷ ◸ ◹ ◺ ◻ ◼ ◽︎◾︎◿ +//! +//! Only a subset of this block is viable for sprite drawing; filling +//! out this file to have full coverage of this block is not the goal. +//! + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Corner = common.Corner; +const Shade = common.Shade; + +const font = @import("../../main.zig"); + +/// ◢ ◣ ◤ ◥ +pub fn draw25E2_25E5( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + switch (cp) { + // ◢ + 0x25e2 => try cornerTriangleShade(metrics, canvas, .br, .on), + // ◣ + 0x25e3 => try cornerTriangleShade(metrics, canvas, .bl, .on), + // ◤ + 0x25e4 => try cornerTriangleShade(metrics, canvas, .tl, .on), + // ◥ + 0x25e5 => try cornerTriangleShade(metrics, canvas, .tr, .on), + + else => unreachable, + } +} + +/// ◸ ◹ ◺ +pub fn draw25F8_25FA( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + switch (cp) { + // ◸ + 0x25f8 => try cornerTriangleOutline(metrics, canvas, .tl), + // ◹ + 0x25f9 => try cornerTriangleOutline(metrics, canvas, .tr), + // ◺ + 0x25fa => try cornerTriangleOutline(metrics, canvas, .bl), + + else => unreachable, + } +} + +/// ◿ +pub fn draw25FF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + try cornerTriangleOutline(metrics, canvas, .br); +} + +pub fn cornerTriangleShade( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime corner: Corner, + comptime shade: Shade, +) !void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const x0, const y0, const x1, const y1, const x2, const y2 = + switch (corner) { + .tl => .{ + 0, + 0, + 0, + float_height, + float_width, + 0, + }, + .tr => .{ + 0, + 0, + float_width, + float_height, + float_width, + 0, + }, + .bl => .{ + 0, + 0, + 0, + float_height, + float_width, + float_height, + }, + .br => .{ + 0, + float_height, + float_width, + float_height, + float_width, + 0, + }, + }; + + var path = canvas.staticPath(5); // nodes.len = 0 + path.moveTo(x0, y0); // +1, nodes.len = 1 + path.lineTo(x1, y1); // +1, nodes.len = 2 + path.lineTo(x2, y2); // +1, nodes.len = 3 + path.close(); // +2, nodes.len = 5 + + try canvas.fillPath( + path.wrapped_path, + .{}, + @enumFromInt(@intFromEnum(shade)), + ); +} + +pub fn cornerTriangleOutline( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime corner: Corner, +) !void { + const float_thick: f64 = @floatFromInt(Thickness.light.height(metrics.box_thickness)); + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const x0, const y0, const x1, const y1, const x2, const y2 = + switch (corner) { + .tl => .{ + 0, + 0, + 0, + float_height, + float_width, + 0, + }, + .tr => .{ + 0, + 0, + float_width, + float_height, + float_width, + 0, + }, + .bl => .{ + 0, + 0, + 0, + float_height, + float_width, + float_height, + }, + .br => .{ + 0, + float_height, + float_width, + float_height, + float_width, + 0, + }, + }; + + var path = canvas.staticPath(5); // nodes.len = 0 + path.moveTo(x0, y0); // +1, nodes.len = 1 + path.lineTo(x1, y1); // +1, nodes.len = 2 + path.lineTo(x2, y2); // +1, nodes.len = 3 + path.close(); // +2, nodes.len = 5 + + try canvas.innerStrokePath(path.wrapped_path, .{ + .line_cap_mode = .butt, + .line_width = float_thick, + }, .on); +} diff --git a/src/font/sprite/octants.txt b/src/font/sprite/draw/octants.txt similarity index 100% rename from src/font/sprite/octants.txt rename to src/font/sprite/draw/octants.txt diff --git a/src/font/sprite/draw/powerline.zig b/src/font/sprite/draw/powerline.zig new file mode 100644 index 000000000..24fce454b --- /dev/null +++ b/src/font/sprite/draw/powerline.zig @@ -0,0 +1,396 @@ +//! Powerline + Powerline Extra Symbols | U+E0B0...U+E0D4 +//! https://github.com/ryanoasis/powerline-extra-symbols +//! +//!                 +//!               +//!     +//! +//! We implement the more geometric glyphs here, but not the stylized ones. +//! + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Shade = common.Shade; + +const box = @import("box.zig"); + +const font = @import("../../main.zig"); +const Quad = font.sprite.Canvas.Quad; + +///  +pub fn drawE0B0( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = float_height / 2 }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0B2( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = float_width, .y = 0 }, + .p1 = .{ .x = 0, .y = float_height / 2 }, + .p2 = .{ .x = float_width, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0B8( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = float_height }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0B9( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperLeftToLowerRight(metrics, canvas); +} + +///  +pub fn drawE0BA( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = float_width, .y = 0 }, + .p1 = .{ .x = float_width, .y = float_height }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0BB( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperRightToLowerLeft(metrics, canvas); +} + +///  +pub fn drawE0BC( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = 0 }, + .p2 = .{ .x = 0, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0BD( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperRightToLowerLeft(metrics, canvas); +} + +///  +pub fn drawE0BE( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + try canvas.triangle(.{ + .p0 = .{ .x = 0, .y = 0 }, + .p1 = .{ .x = float_width, .y = 0 }, + .p2 = .{ .x = float_width, .y = float_height }, + }, .on); +} + +///  +pub fn drawE0BF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + box.lightDiagonalUpperLeftToLowerRight(metrics, canvas); +} + +///  +pub fn drawE0B1( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + + var path = canvas.staticPath(3); + path.moveTo(0, 0); + path.lineTo(float_width, float_height / 2); + path.lineTo(0, float_height); + + try canvas.strokePath( + path.wrapped_path, + .{ + .line_cap_mode = .butt, + .line_width = @floatFromInt( + Thickness.light.height(metrics.box_thickness), + ), + }, + .on, + ); +} + +///  +pub fn drawE0B3( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0B1(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} + +///  +pub fn drawE0B4( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + + // Coefficient for approximating a circular arc. + const c: f64 = (std.math.sqrt2 - 1.0) * 4.0 / 3.0; + + const radius: f64 = @min(float_width, float_height / 2); + + var path = canvas.staticPath(6); + path.moveTo(0, 0); + path.curveTo( + radius * c, + 0, + radius, + radius - radius * c, + radius, + radius, + ); + path.lineTo(radius, float_height - radius); + path.curveTo( + radius, + float_height - radius + radius * c, + radius * c, + float_height, + 0, + float_height, + ); + path.close(); + + try canvas.fillPath(path.wrapped_path, .{}, .on); +} + +///  +pub fn drawE0B5( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + + // Coefficient for approximating a circular arc. + const c: f64 = (std.math.sqrt2 - 1.0) * 4.0 / 3.0; + + const radius: f64 = @min(float_width, float_height / 2); + + var path = canvas.staticPath(4); + path.moveTo(0, 0); + path.curveTo( + radius * c, + 0, + radius, + radius - radius * c, + radius, + radius, + ); + path.lineTo(radius, float_height - radius); + path.curveTo( + radius, + float_height - radius + radius * c, + radius * c, + float_height, + 0, + float_height, + ); + + try canvas.innerStrokePath(path.wrapped_path, .{ + .line_width = @floatFromInt(metrics.box_thickness), + .line_cap_mode = .butt, + }, .on); +} + +///  +pub fn drawE0B6( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0B4(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} + +///  +pub fn drawE0B7( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0B5(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} + +///  +pub fn drawE0D2( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + + const float_width: f64 = @floatFromInt(width); + const float_height: f64 = @floatFromInt(height); + const float_thick: f64 = @floatFromInt(metrics.box_thickness); + + // Top piece + { + var path = canvas.staticPath(6); + path.moveTo(0, 0); + path.lineTo(float_width, 0); + path.lineTo(float_width / 2, float_height / 2 - float_thick / 2); + path.lineTo(0, float_height / 2 - float_thick / 2); + path.close(); + + try canvas.fillPath(path.wrapped_path, .{}, .on); + } + + // Bottom piece + { + var path = canvas.staticPath(6); + path.moveTo(0, float_height); + path.lineTo(float_width, float_height); + path.lineTo(float_width / 2, float_height / 2 + float_thick / 2); + path.lineTo(0, float_height / 2 + float_thick / 2); + path.close(); + + try canvas.fillPath(path.wrapped_path, .{}, .on); + } +} + +///  +pub fn drawE0D4( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + try drawE0D2(cp, canvas, width, height, metrics); + try canvas.flipHorizontal(); +} diff --git a/src/font/sprite/draw/special.zig b/src/font/sprite/draw/special.zig new file mode 100644 index 000000000..3d75360e3 --- /dev/null +++ b/src/font/sprite/draw/special.zig @@ -0,0 +1,328 @@ +//! This file contains glyph drawing functions for all of the +//! non-Unicode sprite glyphs, such as cursors and underlines. +//! +//! The naming convention in this file differs from the usual +//! because the draw functions for special sprites are found by +//! having names that exactly match the enum fields in Sprite. + +const std = @import("std"); +const builtin = @import("builtin"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const font = @import("../../main.zig"); +const Sprite = font.sprite.Sprite; + +pub fn underline( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.underline_position), + .width = @intCast(width), + .height = @intCast(metrics.underline_thickness), + }, .on); +} + +pub fn underline_double( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + // We place one underline above the underline position, and one below + // by one thickness, creating a "negative" underline where the single + // underline would be placed. + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.underline_position -| metrics.underline_thickness), + .width = @intCast(width), + .height = @intCast(metrics.underline_thickness), + }, .on); + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.underline_position +| metrics.underline_thickness), + .width = @intCast(width), + .height = @intCast(metrics.underline_thickness), + }, .on); +} + +pub fn underline_dotted( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + // TODO: Rework this now that we can go out of bounds, just + // make sure that adjacent versions of this glyph align. + const dot_width = @max(metrics.underline_thickness, 3); + const dot_count = @max((width / dot_width) / 2, 1); + const gap_width = std.math.divCeil( + u32, + width -| (dot_count * dot_width), + dot_count, + ) catch return error.MathError; + var i: u32 = 0; + while (i < dot_count) : (i += 1) { + // Ensure we never go out of bounds for the rect + const x = @min(i * (dot_width + gap_width), width - 1); + const rect_width = @min(width - x, dot_width); + canvas.rect(.{ + .x = @intCast(x), + .y = @intCast(metrics.underline_position), + .width = @intCast(rect_width), + .height = @intCast(metrics.underline_thickness), + }, .on); + } +} + +pub fn underline_dashed( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + const dash_width = width / 3 + 1; + const dash_count = (width / dash_width) + 1; + var i: u32 = 0; + while (i < dash_count) : (i += 2) { + // Ensure we never go out of bounds for the rect + const x = @min(i * dash_width, width - 1); + const rect_width = @min(width - x, dash_width); + canvas.rect(.{ + .x = @intCast(x), + .y = @intCast(metrics.underline_position), + .width = @intCast(rect_width), + .height = @intCast(metrics.underline_thickness), + }, .on); + } +} + +pub fn underline_curly( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + // TODO: Rework this using z2d, this is pretty cool code and all but + // it doesn't need to be highly optimized and z2d path drawing + // code would be clearer and nicer to have. + + const float_width: f64 = @floatFromInt(width); + // Because of we way we draw the undercurl, we end up making it around 1px + // thicker than it should be, to fix this we just reduce the thickness by 1. + // + // We use a minimum thickness of 0.414 because this empirically produces + // the nicest undercurls at 1px underline thickness; thinner tends to look + // too thin compared to straight underlines and has artefacting. + const float_thick: f64 = @max( + 0.414, + @as(f64, @floatFromInt(metrics.underline_thickness -| 1)), + ); + + // Calculate the wave period for a single character + // `2 * pi...` = 1 peak per character + // `4 * pi...` = 2 peaks per character + const wave_period = 2 * std.math.pi / float_width; + + // The full amplitude of the wave can be from the bottom to the + // underline position. We also calculate our mid y point of the wave + const half_amplitude = 1.0 / wave_period; + const y_mid: f64 = half_amplitude + float_thick * 0.5 + 1; + + // Offset to move the undercurl up slightly. + const y_off: u32 = @intFromFloat(half_amplitude * 0.5); + + // This is used in calculating the offset curve estimate below. + const offset_factor = @min(1.0, float_thick * 0.5 * wave_period) * @min( + 1.0, + half_amplitude * wave_period, + ); + + // follow Xiaolin Wu's antialias algorithm to draw the curve + var x: u32 = 0; + while (x < width) : (x += 1) { + // We sample the wave function at the *middle* of each + // pixel column, to ensure that it renders symmetrically. + const t: f64 = (@as(f64, @floatFromInt(x)) + 0.5) * wave_period; + // Use the slope at this location to add thickness to + // the line on this column, counteracting the thinning + // caused by the slope. + // + // This is not the exact offset curve for a sine wave, + // but it's a decent enough approximation. + // + // How did I derive this? I stared at Desmos and fiddled + // with numbers for an hour until it was good enough. + const t_u: f64 = t + std.math.pi; + const slope_factor_u: f64 = + (@sin(t_u) * @sin(t_u) * offset_factor) / + ((1.0 + @cos(t_u / 2) * @cos(t_u / 2) * 2) * wave_period); + const slope_factor_l: f64 = + (@sin(t) * @sin(t) * offset_factor) / + ((1.0 + @cos(t / 2) * @cos(t / 2) * 2) * wave_period); + + const cosx: f64 = @cos(t); + // This will be the center of our stroke. + const y: f64 = y_mid + half_amplitude * cosx; + + // The upper pixel and lower pixel are + // calculated relative to the center. + const y_u: f64 = y - float_thick * 0.5 - slope_factor_u; + const y_l: f64 = y + float_thick * 0.5 + slope_factor_l; + const y_upper: u32 = @intFromFloat(@floor(y_u)); + const y_lower: u32 = @intFromFloat(@ceil(y_l)); + const alpha_u: u8 = @intFromFloat( + @round(255 * (1.0 - @abs(y_u - @floor(y_u)))), + ); + const alpha_l: u8 = @intFromFloat( + @round(255 * (1.0 - @abs(y_l - @ceil(y_l)))), + ); + + // upper and lower bounds + canvas.pixel( + @intCast(x), + @intCast(metrics.underline_position +| y_upper -| y_off), + @enumFromInt(alpha_u), + ); + canvas.pixel( + @intCast(x), + @intCast(metrics.underline_position +| y_lower -| y_off), + @enumFromInt(alpha_l), + ); + + // fill between upper and lower bound + var y_fill: u32 = y_upper + 1; + while (y_fill < y_lower) : (y_fill += 1) { + canvas.pixel( + @intCast(x), + @intCast(metrics.underline_position +| y_fill -| y_off), + .on, + ); + } + } +} + +pub fn strikethrough( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.strikethrough_position), + .width = @intCast(width), + .height = @intCast(metrics.strikethrough_thickness), + }, .on); +} + +pub fn overline( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.overline_position), + .width = @intCast(width), + .height = @intCast(metrics.overline_thickness), + }, .on); +} + +pub fn cursor_rect( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = metrics; + + canvas.rect(.{ + .x = 0, + .y = 0, + .width = @intCast(width), + .height = @intCast(height), + }, .on); +} + +pub fn cursor_hollow_rect( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + + // We fill the entire rect and then hollow out the inside, this isn't very + // efficient but it doesn't need to be and it's the easiest way to write it. + canvas.rect(.{ + .x = 0, + .y = 0, + .width = @intCast(width), + .height = @intCast(height), + }, .on); + canvas.rect(.{ + .x = @intCast(metrics.cursor_thickness), + .y = @intCast(metrics.cursor_thickness), + .width = @intCast(width -| metrics.cursor_thickness * 2), + .height = @intCast(height -| metrics.cursor_thickness * 2), + }, .off); +} + +pub fn cursor_bar( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + + // We place the bar cursor half of its thickness over the left edge of the + // cell, so that it sits centered between characters, not biased to a side. + // + // We round up (add 1 before dividing by 2) because, empirically, having a + // 1px cursor shifted left a pixel looks better than having it not shifted. + canvas.rect(.{ + .x = -@as(i32, @intCast((metrics.cursor_thickness + 1) / 2)), + .y = 0, + .width = @intCast(metrics.cursor_thickness), + .height = @intCast(height), + }, .on); +} diff --git a/src/font/sprite/draw/symbols_for_legacy_computing.zig b/src/font/sprite/draw/symbols_for_legacy_computing.zig new file mode 100644 index 000000000..a17ddb494 --- /dev/null +++ b/src/font/sprite/draw/symbols_for_legacy_computing.zig @@ -0,0 +1,1431 @@ +//! Symbols for Legacy Computing | U+1FB00...U+1FBFF +//! https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing +//! +//! 🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏 +//! 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟 +//! 🬠 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯 +//! 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻 🬼 🬽 🬾 🬿 +//! 🭀 🭁 🭂 🭃 🭄 🭅 🭆 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏 +//! 🭐 🭑 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜 🭝 🭞 🭟 +//! 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯 +//! 🭰 🭱 🭲 🭳 🭴 🭵 🭶 🭷 🭸 🭹 🭺 🭻 🭼 🭽 🭾 🭿 +//! 🮀 🮁 🮂 🮃 🮄 🮅 🮆 🮇 🮈 🮉 🮊 🮋 🮌 🮍 🮎 🮏 +//! 🮐 🮑 🮒 🮔 🮕 🮖 🮗 🮘 🮙 🮚 🮛 🮜 🮝 🮞 🮟 +//! 🮠 🮡 🮢 🮣 🮤 🮥 🮦 🮧 🮨 🮩 🮪 🮫 🮬 🮭 🮮 🮯 +//! 🮰 🮱 🮲 🮳 🮴 🮵 🮶 🮷 🮸 🮹 🮺 🮻 🮼 🮽 🮾 🮿 +//! 🯀 🯁 🯂 🯃 🯄 🯅 🯆 🯇 🯈 🯉 🯊 🯋 🯌 🯍 🯎 🯏 +//! 🯐 🯑 🯒 🯓 🯔 🯕 🯖 🯗 🯘 🯙 🯚 🯛 🯜 🯝 🯞 🯟 +//! 🯠 🯡 🯢 🯣 🯤 🯥 🯦 🯧 🯨 🯩 🯪 🯫 🯬 🯭 🯮 🯯 +//! 🯰 🯱 🯲 🯳 🯴 🯵 🯶 🯷 🯸 🯹 +//! + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Alignment = common.Alignment; +const Corner = common.Corner; +const Quads = common.Quads; +const Edge = common.Edge; +const Shade = common.Shade; +const xHalfs = common.xHalfs; +const yThirds = common.yThirds; +const rect = common.rect; + +const box = @import("box.zig"); +const block = @import("block.zig"); +const geo = @import("geometric_shapes.zig"); + +const font = @import("../../main.zig"); + +// Utility names for common fractions +const one_eighth: f64 = 0.125; +const one_quarter: f64 = 0.25; +const one_third: f64 = (1.0 / 3.0); +const three_eighths: f64 = 0.375; +const half: f64 = 0.5; +const five_eighths: f64 = 0.625; +const two_thirds: f64 = (2.0 / 3.0); +const three_quarters: f64 = 0.75; +const seven_eighths: f64 = 0.875; + +const SmoothMosaic = packed struct(u10) { + tl: bool, + ul: bool, + ll: bool, + bl: bool, + bc: bool, + br: bool, + lr: bool, + ur: bool, + tr: bool, + tc: bool, + + fn from(comptime pattern: *const [15:0]u8) SmoothMosaic { + return .{ + .tl = pattern[0] == '#', + + .ul = pattern[4] == '#' and + (pattern[0] != '#' or pattern[8] != '#'), + + .ll = pattern[8] == '#' and + (pattern[4] != '#' or pattern[12] != '#'), + + .bl = pattern[12] == '#', + + .bc = pattern[13] == '#' and + (pattern[12] != '#' or pattern[14] != '#'), + + .br = pattern[14] == '#', + + .lr = pattern[10] == '#' and + (pattern[14] != '#' or pattern[6] != '#'), + + .ur = pattern[6] == '#' and + (pattern[10] != '#' or pattern[2] != '#'), + + .tr = pattern[2] == '#', + + .tc = pattern[1] == '#' and + (pattern[2] != '#' or pattern[0] != '#'), + }; + } +}; + +/// Sextants +pub fn draw1FB00_1FB3B( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + const Sextants = packed struct(u6) { + tl: bool, + tr: bool, + ml: bool, + mr: bool, + bl: bool, + br: bool, + }; + + assert(cp >= 0x1fb00 and cp <= 0x1fb3b); + const idx = cp - 0x1fb00; + const sex: Sextants = @bitCast(@as(u6, @intCast( + idx + (idx / 0x14) + 1, + ))); + + const x_halfs = xHalfs(metrics); + const y_thirds = yThirds(metrics); + + if (sex.tl) rect(metrics, canvas, 0, 0, x_halfs[0], y_thirds[0]); + if (sex.tr) rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_thirds[0]); + if (sex.ml) rect(metrics, canvas, 0, y_thirds[1], x_halfs[0], y_thirds[2]); + if (sex.mr) rect(metrics, canvas, x_halfs[1], y_thirds[1], metrics.cell_width, y_thirds[2]); + if (sex.bl) rect(metrics, canvas, 0, y_thirds[3], x_halfs[0], metrics.cell_height); + if (sex.br) rect(metrics, canvas, x_halfs[1], y_thirds[3], metrics.cell_width, metrics.cell_height); +} + +/// Smooth Mosaics +pub fn draw1FB3C_1FB67( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + // Hand written lookup table for these shapes since I couldn't + // determine any sort of mathematical pattern in the codepoints. + const mosaic: SmoothMosaic = switch (cp) { + // '🬼' + 0x1fb3c => SmoothMosaic.from( + \\... + \\... + \\#.. + \\##. + ), + // '🬽' + 0x1fb3d => SmoothMosaic.from( + \\... + \\... + \\#\. + \\### + ), + // '🬾' + 0x1fb3e => SmoothMosaic.from( + \\... + \\#.. + \\#\. + \\##. + ), + // '🬿' + 0x1fb3f => SmoothMosaic.from( + \\... + \\#.. + \\##. + \\### + ), + // '🭀' + 0x1fb40 => SmoothMosaic.from( + \\#.. + \\#.. + \\##. + \\##. + ), + + // '🭁' + 0x1fb41 => SmoothMosaic.from( + \\/## + \\### + \\### + \\### + ), + // '🭂' + 0x1fb42 => SmoothMosaic.from( + \\./# + \\### + \\### + \\### + ), + // '🭃' + 0x1fb43 => SmoothMosaic.from( + \\.## + \\.## + \\### + \\### + ), + // '🭄' + 0x1fb44 => SmoothMosaic.from( + \\..# + \\.## + \\### + \\### + ), + // '🭅' + 0x1fb45 => SmoothMosaic.from( + \\.## + \\.## + \\.## + \\### + ), + // '🭆' + 0x1fb46 => SmoothMosaic.from( + \\... + \\./# + \\### + \\### + ), + + // '🭇' + 0x1fb47 => SmoothMosaic.from( + \\... + \\... + \\..# + \\.## + ), + // '🭈' + 0x1fb48 => SmoothMosaic.from( + \\... + \\... + \\./# + \\### + ), + // '🭉' + 0x1fb49 => SmoothMosaic.from( + \\... + \\..# + \\./# + \\.## + ), + // '🭊' + 0x1fb4a => SmoothMosaic.from( + \\... + \\..# + \\.## + \\### + ), + // '🭋' + 0x1fb4b => SmoothMosaic.from( + \\..# + \\..# + \\.## + \\.## + ), + + // '🭌' + 0x1fb4c => SmoothMosaic.from( + \\##\ + \\### + \\### + \\### + ), + // '🭍' + 0x1fb4d => SmoothMosaic.from( + \\#\. + \\### + \\### + \\### + ), + // '🭎' + 0x1fb4e => SmoothMosaic.from( + \\##. + \\##. + \\### + \\### + ), + // '🭏' + 0x1fb4f => SmoothMosaic.from( + \\#.. + \\##. + \\### + \\### + ), + // '🭐' + 0x1fb50 => SmoothMosaic.from( + \\##. + \\##. + \\##. + \\### + ), + // '🭑' + 0x1fb51 => SmoothMosaic.from( + \\... + \\#\. + \\### + \\### + ), + + // '🭒' + 0x1fb52 => SmoothMosaic.from( + \\### + \\### + \\### + \\\## + ), + // '🭓' + 0x1fb53 => SmoothMosaic.from( + \\### + \\### + \\### + \\.\# + ), + // '🭔' + 0x1fb54 => SmoothMosaic.from( + \\### + \\### + \\.## + \\.## + ), + // '🭕' + 0x1fb55 => SmoothMosaic.from( + \\### + \\### + \\.## + \\..# + ), + // '🭖' + 0x1fb56 => SmoothMosaic.from( + \\### + \\.## + \\.## + \\.## + ), + + // '🭗' + 0x1fb57 => SmoothMosaic.from( + \\##. + \\#.. + \\... + \\... + ), + // '🭘' + 0x1fb58 => SmoothMosaic.from( + \\### + \\#/. + \\... + \\... + ), + // '🭙' + 0x1fb59 => SmoothMosaic.from( + \\##. + \\#/. + \\#.. + \\... + ), + // '🭚' + 0x1fb5a => SmoothMosaic.from( + \\### + \\##. + \\#.. + \\... + ), + // '🭛' + 0x1fb5b => SmoothMosaic.from( + \\##. + \\##. + \\#.. + \\#.. + ), + + // '🭜' + 0x1fb5c => SmoothMosaic.from( + \\### + \\### + \\#/. + \\... + ), + // '🭝' + 0x1fb5d => SmoothMosaic.from( + \\### + \\### + \\### + \\##/ + ), + // '🭞' + 0x1fb5e => SmoothMosaic.from( + \\### + \\### + \\### + \\#/. + ), + // '🭟' + 0x1fb5f => SmoothMosaic.from( + \\### + \\### + \\##. + \\##. + ), + // '🭠' + 0x1fb60 => SmoothMosaic.from( + \\### + \\### + \\##. + \\#.. + ), + // '🭡' + 0x1fb61 => SmoothMosaic.from( + \\### + \\##. + \\##. + \\##. + ), + + // '🭢' + 0x1fb62 => SmoothMosaic.from( + \\.## + \\..# + \\... + \\... + ), + // '🭣' + 0x1fb63 => SmoothMosaic.from( + \\### + \\.\# + \\... + \\... + ), + // '🭤' + 0x1fb64 => SmoothMosaic.from( + \\.## + \\.\# + \\..# + \\... + ), + // '🭥' + 0x1fb65 => SmoothMosaic.from( + \\### + \\.## + \\..# + \\... + ), + // '🭦' + 0x1fb66 => SmoothMosaic.from( + \\.## + \\.## + \\..# + \\..# + ), + // '🭧' + 0x1fb67 => SmoothMosaic.from( + \\### + \\### + \\.\# + \\... + ), + else => unreachable, + }; + + const y_thirds = yThirds(metrics); + const top: f64 = 0.0; + // We average the edge positions for the y_thirds boundaries here + // rather than having to deal with varying alignments depending on + // the surrounding pieces. The most this will be off by is half of + // a pixel, so hopefully it's not noticeable. + const upper: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[0])) + @as(f64, @floatFromInt(y_thirds[1]))); + const lower: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[2])) + @as(f64, @floatFromInt(y_thirds[3]))); + const bottom: f64 = @floatFromInt(metrics.cell_height); + const left: f64 = 0.0; + const center: f64 = @round(@as(f64, @floatFromInt(metrics.cell_width)) / 2); + const right: f64 = @floatFromInt(metrics.cell_width); + + var path = canvas.staticPath(12); // nodes.len = 0 + if (mosaic.tl) path.lineTo(left, top); // +1, nodes.len = 1 + if (mosaic.ul) path.lineTo(left, upper); // +1, nodes.len = 2 + if (mosaic.ll) path.lineTo(left, lower); // +1, nodes.len = 3 + if (mosaic.bl) path.lineTo(left, bottom); // +1, nodes.len = 4 + if (mosaic.bc) path.lineTo(center, bottom); // +1, nodes.len = 5 + if (mosaic.br) path.lineTo(right, bottom); // +1, nodes.len = 6 + if (mosaic.lr) path.lineTo(right, lower); // +1, nodes.len = 7 + if (mosaic.ur) path.lineTo(right, upper); // +1, nodes.len = 8 + if (mosaic.tr) path.lineTo(right, top); // +1, nodes.len = 9 + if (mosaic.tc) path.lineTo(center, top); // +1, nodes.len = 10 + path.close(); // +2, nodes.len = 12 + + try canvas.fillPath(path.wrapped_path, .{}, .on); +} + +pub fn draw1FB68_1FB6F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '🭨' + 0x1fb68 => { + try edgeTriangle(metrics, canvas, .left); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + }, + // '🭩' + 0x1fb69 => { + try edgeTriangle(metrics, canvas, .top); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + }, + // '🭪' + 0x1fb6a => { + try edgeTriangle(metrics, canvas, .right); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + }, + // '🭫' + 0x1fb6b => { + try edgeTriangle(metrics, canvas, .bottom); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + }, + // '🭬' + 0x1fb6c => try edgeTriangle(metrics, canvas, .left), + // '🭭' + 0x1fb6d => try edgeTriangle(metrics, canvas, .top), + // '🭮' + 0x1fb6e => try edgeTriangle(metrics, canvas, .right), + // '🭯' + 0x1fb6f => try edgeTriangle(metrics, canvas, .bottom), + + else => unreachable, + } +} + +/// Vertical one eighth blocks +pub fn draw1FB70_1FB75( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + const n = cp + 1 - 0x1fb70; + + const x: u32 = @intFromFloat( + @round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(metrics.cell_width)) / 8), + ); + const w: u32 = @intFromFloat( + @round(@as(f64, @floatFromInt(metrics.cell_width)) / 8), + ); + rect(metrics, canvas, x, 0, x + w, metrics.cell_height); +} + +/// Horizontal one eighth blocks +pub fn draw1FB76_1FB7B( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + const n = cp + 1 - 0x1fb76; + + const h = @as( + u32, + @intFromFloat(@round(@as(f64, @floatFromInt(metrics.cell_height)) / 8)), + ); + const y = @min( + metrics.cell_height -| h, + @as( + u32, + @intFromFloat( + @round(@as(f64, @floatFromInt(n)) * + @as(f64, @floatFromInt(metrics.cell_height)) / 8), + ), + ), + ); + rect(metrics, canvas, 0, y, metrics.cell_width, y + h); +} + +pub fn draw1FB7C_1FB97( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + switch (cp) { + + // '🭼' LEFT AND LOWER ONE EIGHTH BLOCK + 0x1fb7c => { + block.block(metrics, canvas, .left, one_eighth, 1); + block.block(metrics, canvas, .lower, 1, one_eighth); + }, + // '🭽' LEFT AND UPPER ONE EIGHTH BLOCK + 0x1fb7d => { + block.block(metrics, canvas, .left, one_eighth, 1); + block.block(metrics, canvas, .upper, 1, one_eighth); + }, + // '🭾' RIGHT AND UPPER ONE EIGHTH BLOCK + 0x1fb7e => { + block.block(metrics, canvas, .right, one_eighth, 1); + block.block(metrics, canvas, .upper, 1, one_eighth); + }, + // '🭿' RIGHT AND LOWER ONE EIGHTH BLOCK + 0x1fb7f => { + block.block(metrics, canvas, .right, one_eighth, 1); + block.block(metrics, canvas, .lower, 1, one_eighth); + }, + // '🮀' UPPER AND LOWER ONE EIGHTH BLOCK + 0x1fb80 => { + block.block(metrics, canvas, .upper, 1, one_eighth); + block.block(metrics, canvas, .lower, 1, one_eighth); + }, + // '🮁' Horizontal One Eighth Block 1358 + 0x1fb81 => { + // We just call the draw function for each of the relevant codepoints. + // The first codepoint is actually a lie, it's before the range, but + // we need it to get the first (0th) block position. This might be a + // bit brittle, oh well, if it breaks we can fix it. + try draw1FB76_1FB7B(0x1fb74 + 1, canvas, width, height, metrics); + try draw1FB76_1FB7B(0x1fb74 + 3, canvas, width, height, metrics); + try draw1FB76_1FB7B(0x1fb74 + 5, canvas, width, height, metrics); + try draw1FB76_1FB7B(0x1fb74 + 8, canvas, width, height, metrics); + }, + + // '🮂' UPPER ONE QUARTER BLOCK + 0x1fb82 => block.block(metrics, canvas, .upper, 1, one_quarter), + // '🮃' UPPER THREE EIGHTHS BLOCK + 0x1fb83 => block.block(metrics, canvas, .upper, 1, three_eighths), + // '🮄' UPPER FIVE EIGHTHS BLOCK + 0x1fb84 => block.block(metrics, canvas, .upper, 1, five_eighths), + // '🮅' UPPER THREE QUARTERS BLOCK + 0x1fb85 => block.block(metrics, canvas, .upper, 1, three_quarters), + // '🮆' UPPER SEVEN EIGHTHS BLOCK + 0x1fb86 => block.block(metrics, canvas, .upper, 1, seven_eighths), + + // '🮇' RIGHT ONE QUARTER BLOCK + 0x1fb87 => block.block(metrics, canvas, .right, one_quarter, 1), + // '🮈' RIGHT THREE EIGHTHS BLOCK + 0x1fb88 => block.block(metrics, canvas, .right, three_eighths, 1), + // '🮉' RIGHT FIVE EIGHTHS BLOCK + 0x1fb89 => block.block(metrics, canvas, .right, five_eighths, 1), + // '🮊' RIGHT THREE QUARTERS BLOCK + 0x1fb8a => block.block(metrics, canvas, .right, three_quarters, 1), + // '🮋' RIGHT SEVEN EIGHTHS BLOCK/ + 0x1fb8b => block.block(metrics, canvas, .right, seven_eighths, 1), + + // '🮌' + 0x1fb8c => block.blockShade(metrics, canvas, .left, half, 1, .medium), + // '🮍' + 0x1fb8d => block.blockShade(metrics, canvas, .right, half, 1, .medium), + // '🮎' + 0x1fb8e => block.blockShade(metrics, canvas, .upper, 1, half, .medium), + // '🮏' + 0x1fb8f => block.blockShade(metrics, canvas, .lower, 1, half, .medium), + + // '🮐' + 0x1fb90 => block.fullBlockShade(metrics, canvas, .medium), + // '🮑' + 0x1fb91 => { + block.fullBlockShade(metrics, canvas, .medium); + block.block(metrics, canvas, .upper, 1, half); + }, + // '🮒' + 0x1fb92 => { + block.fullBlockShade(metrics, canvas, .medium); + block.block(metrics, canvas, .lower, 1, half); + }, + 0x1fb93 => { + // NOTE: This codepoint is currently un-allocated, it's a hole + // in the unicode block, so it's safe to just render it + // as an empty glyph, probably. + }, + // '🮔' + 0x1fb94 => { + block.fullBlockShade(metrics, canvas, .medium); + block.block(metrics, canvas, .right, half, 1); + }, + // '🮕' + 0x1fb95 => checkerboardFill(metrics, canvas, 0), + // '🮖' + 0x1fb96 => checkerboardFill(metrics, canvas, 1), + // '🮗' + 0x1fb97 => { + canvas.box( + 0, + @intCast(height / 4), + @intCast(width), + @intCast(2 * height / 4), + .on, + ); + canvas.box( + 0, + @intCast(3 * height / 4), + @intCast(width), + @intCast(height), + .on, + ); + }, + + else => unreachable, + } +} + +/// Upper Left to Lower Right Fill +/// 🮘 +pub fn draw1FB98( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + + // TODO: This doesn't align properly for most cell sizes, fix that. + + const thick_px = Thickness.light.height(metrics.box_thickness); + const line_count = metrics.cell_width / (2 * thick_px); + + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + const stride = @round(float_width / @as(f64, @floatFromInt(line_count))); + + for (0..line_count * 2 + 1) |_i| { + const i = @as(i32, @intCast(_i)) - @as(i32, @intCast(line_count)); + const top_x = @as(f64, @floatFromInt(i)) * stride; + const bottom_x = float_width + top_x; + canvas.line(.{ + .p0 = .{ .x = top_x, .y = 0 }, + .p1 = .{ .x = bottom_x, .y = float_height }, + }, float_thick, .on) catch {}; + } +} + +/// Upper Right to Lower Left Fill +/// 🮙 +pub fn draw1FB99( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + + // TODO: This doesn't align properly for most cell sizes, fix that. + + const thick_px = Thickness.light.height(metrics.box_thickness); + const line_count = metrics.cell_width / (2 * thick_px); + + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + const stride = @round(float_width / @as(f64, @floatFromInt(line_count))); + + for (0..line_count * 2 + 1) |_i| { + const i = @as(i32, @intCast(_i)) - @as(i32, @intCast(line_count)); + const bottom_x = @as(f64, @floatFromInt(i)) * stride; + const top_x = float_width + bottom_x; + canvas.line(.{ + .p0 = .{ .x = top_x, .y = 0 }, + .p1 = .{ .x = bottom_x, .y = float_height }, + }, float_thick, .on) catch {}; + } +} + +pub fn draw1FB9A_1FB9F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '🮚' + 0x1fb9a => { + try edgeTriangle(metrics, canvas, .top); + try edgeTriangle(metrics, canvas, .bottom); + }, + // '🮛' + 0x1fb9b => { + try edgeTriangle(metrics, canvas, .left); + try edgeTriangle(metrics, canvas, .right); + }, + // '🮜' + 0x1fb9c => try geo.cornerTriangleShade(metrics, canvas, .tl, .medium), + // '🮝' + 0x1fb9d => try geo.cornerTriangleShade(metrics, canvas, .tr, .medium), + // '🮞' + 0x1fb9e => try geo.cornerTriangleShade(metrics, canvas, .br, .medium), + // '🮟' + 0x1fb9f => try geo.cornerTriangleShade(metrics, canvas, .bl, .medium), + + else => unreachable, + } +} + +pub fn draw1FBA0_1FBAE( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '🮠' + 0x1fba0 => cornerDiagonalLines(metrics, canvas, .{ .tl = true }), + // '🮡' + 0x1fba1 => cornerDiagonalLines(metrics, canvas, .{ .tr = true }), + // '🮢' + 0x1fba2 => cornerDiagonalLines(metrics, canvas, .{ .bl = true }), + // '🮣' + 0x1fba3 => cornerDiagonalLines(metrics, canvas, .{ .br = true }), + // '🮤' + 0x1fba4 => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .bl = true }), + // '🮥' + 0x1fba5 => cornerDiagonalLines(metrics, canvas, .{ .tr = true, .br = true }), + // '🮦' + 0x1fba6 => cornerDiagonalLines(metrics, canvas, .{ .bl = true, .br = true }), + // '🮧' + 0x1fba7 => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true }), + // '🮨' + 0x1fba8 => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .br = true }), + // '🮩' + 0x1fba9 => cornerDiagonalLines(metrics, canvas, .{ .tr = true, .bl = true }), + // '🮪' + 0x1fbaa => cornerDiagonalLines(metrics, canvas, .{ .tr = true, .bl = true, .br = true }), + // '🮫' + 0x1fbab => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .bl = true, .br = true }), + // '🮬' + 0x1fbac => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true, .br = true }), + // '🮭' + 0x1fbad => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true, .bl = true }), + // '🮮' + 0x1fbae => cornerDiagonalLines(metrics, canvas, .{ .tl = true, .tr = true, .bl = true, .br = true }), + + else => unreachable, + } +} + +/// 🮯 +pub fn draw1FBAF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + box.linesChar(metrics, canvas, .{ + .up = .heavy, + .down = .heavy, + .left = .light, + .right = .light, + }); +} + +/// 🮽 +pub fn draw1FBBD( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + box.lightDiagonalCross(metrics, canvas); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; +} + +/// 🮾 +pub fn draw1FBBE( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + cornerDiagonalLines(metrics, canvas, .{ .br = true }); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; +} + +/// 🮿 +pub fn draw1FBBF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + cornerDiagonalLines(metrics, canvas, .{ + .tl = true, + .tr = true, + .bl = true, + .br = true, + }); + canvas.invert(); + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; +} + +/// 🯎 +pub fn draw1FBCE( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + block.block(metrics, canvas, .left, two_thirds, 1); +} + +// 🯏 +pub fn draw1FBCF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + + block.block(metrics, canvas, .left, one_third, 1); +} + +/// Cell diagonals. +pub fn draw1FBD0_1FBDF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '🯐' + 0x1fbd0 => cellDiagonal( + metrics, + canvas, + .middle_right, + .lower_left, + ), + // '🯑' + 0x1fbd1 => cellDiagonal( + metrics, + canvas, + .upper_right, + .middle_left, + ), + // '🯒' + 0x1fbd2 => cellDiagonal( + metrics, + canvas, + .upper_left, + .middle_right, + ), + // '🯓' + 0x1fbd3 => cellDiagonal( + metrics, + canvas, + .middle_left, + .lower_right, + ), + // '🯔' + 0x1fbd4 => cellDiagonal( + metrics, + canvas, + .upper_left, + .lower_center, + ), + // '🯕' + 0x1fbd5 => cellDiagonal( + metrics, + canvas, + .upper_center, + .lower_right, + ), + // '🯖' + 0x1fbd6 => cellDiagonal( + metrics, + canvas, + .upper_right, + .lower_center, + ), + // '🯗' + 0x1fbd7 => cellDiagonal( + metrics, + canvas, + .upper_center, + .lower_left, + ), + // '🯘' + 0x1fbd8 => { + cellDiagonal( + metrics, + canvas, + .upper_left, + .middle_center, + ); + cellDiagonal( + metrics, + canvas, + .middle_center, + .upper_right, + ); + }, + // '🯙' + 0x1fbd9 => { + cellDiagonal( + metrics, + canvas, + .upper_right, + .middle_center, + ); + cellDiagonal( + metrics, + canvas, + .middle_center, + .lower_right, + ); + }, + // '🯚' + 0x1fbda => { + cellDiagonal( + metrics, + canvas, + .lower_left, + .middle_center, + ); + cellDiagonal( + metrics, + canvas, + .middle_center, + .lower_right, + ); + }, + // '🯛' + 0x1fbdb => { + cellDiagonal( + metrics, + canvas, + .upper_left, + .middle_center, + ); + cellDiagonal( + metrics, + canvas, + .middle_center, + .lower_left, + ); + }, + // '🯜' + 0x1fbdc => { + cellDiagonal( + metrics, + canvas, + .upper_left, + .lower_center, + ); + cellDiagonal( + metrics, + canvas, + .lower_center, + .upper_right, + ); + }, + // '🯝' + 0x1fbdd => { + cellDiagonal( + metrics, + canvas, + .upper_right, + .middle_left, + ); + cellDiagonal( + metrics, + canvas, + .middle_left, + .lower_right, + ); + }, + // '🯞' + 0x1fbde => { + cellDiagonal( + metrics, + canvas, + .lower_left, + .upper_center, + ); + cellDiagonal( + metrics, + canvas, + .upper_center, + .lower_right, + ); + }, + // '🯟' + 0x1fbdf => { + cellDiagonal( + metrics, + canvas, + .upper_left, + .middle_right, + ); + cellDiagonal( + metrics, + canvas, + .middle_right, + .lower_left, + ); + }, + + else => unreachable, + } +} + +pub fn draw1FBE0_1FBEF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + switch (cp) { + // '🯠' + 0x1fbe0 => circle(metrics, canvas, .top, false), + // '🯡' + 0x1fbe1 => circle(metrics, canvas, .right, false), + // '🯢' + 0x1fbe2 => circle(metrics, canvas, .bottom, false), + // '🯣' + 0x1fbe3 => circle(metrics, canvas, .left, false), + // '🯤' + 0x1fbe4 => block.block(metrics, canvas, .upper_center, 0.5, 0.5), + // '🯥' + 0x1fbe5 => block.block(metrics, canvas, .lower_center, 0.5, 0.5), + // '🯦' + 0x1fbe6 => block.block(metrics, canvas, .middle_left, 0.5, 0.5), + // '🯧' + 0x1fbe7 => block.block(metrics, canvas, .middle_right, 0.5, 0.5), + // '🯨' + 0x1fbe8 => circle(metrics, canvas, .top, true), + // '🯩' + 0x1fbe9 => circle(metrics, canvas, .right, true), + // '🯪' + 0x1fbea => circle(metrics, canvas, .bottom, true), + // '🯫' + 0x1fbeb => circle(metrics, canvas, .left, true), + // '🯬' + 0x1fbec => circle(metrics, canvas, .top_right, true), + // '🯭' + 0x1fbed => circle(metrics, canvas, .bottom_left, true), + // '🯮' + 0x1fbee => circle(metrics, canvas, .bottom_right, true), + // '🯯' + 0x1fbef => circle(metrics, canvas, .top_left, true), + + else => unreachable, + } +} + +fn edgeTriangle( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime edge: Edge, +) !void { + const upper: f64 = 0.0; + const middle: f64 = @round(@as(f64, @floatFromInt(metrics.cell_height)) / 2); + const lower: f64 = @floatFromInt(metrics.cell_height); + const left: f64 = 0.0; + const center: f64 = @round(@as(f64, @floatFromInt(metrics.cell_width)) / 2); + const right: f64 = @floatFromInt(metrics.cell_width); + + const x0, const y0, const x1, const y1 = switch (edge) { + .top => .{ right, upper, left, upper }, + .left => .{ left, upper, left, lower }, + .bottom => .{ left, lower, right, lower }, + .right => .{ right, lower, right, upper }, + }; + + var path = canvas.staticPath(5); // nodes.len = 0 + path.moveTo(center, middle); // +1, nodes.len = 1 + path.lineTo(x0, y0); // +1, nodes.len = 2 + path.lineTo(x1, y1); // +1, nodes.len = 3 + path.close(); // +2, nodes.len = 5 + + try canvas.fillPath(path.wrapped_path, .{}, .on); +} + +fn cornerDiagonalLines( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime corners: Quads, +) void { + const thick_px = Thickness.light.height(metrics.box_thickness); + + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const float_thick: f64 = @floatFromInt(thick_px); + const center_x: f64 = @floatFromInt(metrics.cell_width / 2 + metrics.cell_width % 2); + const center_y: f64 = @floatFromInt(metrics.cell_height / 2 + metrics.cell_height % 2); + + if (corners.tl) canvas.line(.{ + .p0 = .{ .x = center_x, .y = 0 }, + .p1 = .{ .x = 0, .y = center_y }, + }, float_thick, .on) catch {}; + + if (corners.tr) canvas.line(.{ + .p0 = .{ .x = center_x, .y = 0 }, + .p1 = .{ .x = float_width, .y = center_y }, + }, float_thick, .on) catch {}; + + if (corners.bl) canvas.line(.{ + .p0 = .{ .x = center_x, .y = float_height }, + .p1 = .{ .x = 0, .y = center_y }, + }, float_thick, .on) catch {}; + + if (corners.br) canvas.line(.{ + .p0 = .{ .x = center_x, .y = float_height }, + .p1 = .{ .x = float_width, .y = center_y }, + }, float_thick, .on) catch {}; +} + +fn cellDiagonal( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime from: Alignment, + comptime to: Alignment, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const x0: f64 = switch (from.horizontal) { + .left => 0, + .right => float_width, + .center => float_width / 2, + }; + const y0: f64 = switch (from.vertical) { + .top => 0, + .bottom => float_height, + .middle => float_height / 2, + }; + const x1: f64 = switch (to.horizontal) { + .left => 0, + .right => float_width, + .center => float_width / 2, + }; + const y1: f64 = switch (to.vertical) { + .top => 0, + .bottom => float_height, + .middle => float_height / 2, + }; + + canvas.line( + .{ + .p0 = .{ .x = x0, .y = y0 }, + .p1 = .{ .x = x1, .y = y1 }, + }, + @floatFromInt(Thickness.light.height(metrics.box_thickness)), + .on, + ) catch {}; +} + +fn checkerboardFill( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + parity: u1, +) void { + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + const x_size: usize = 4; + const y_size: usize = @intFromFloat(@round(4 * (float_height / float_width))); + for (0..x_size) |x| { + const x0 = (metrics.cell_width * x) / x_size; + const x1 = (metrics.cell_width * (x + 1)) / x_size; + for (0..y_size) |y| { + const y0 = (metrics.cell_height * y) / y_size; + const y1 = (metrics.cell_height * (y + 1)) / y_size; + if ((x + y) % 2 == parity) { + canvas.rect(.{ + .x = @intCast(x0), + .y = @intCast(y0), + .width = @intCast(x1 -| x0), + .height = @intCast(y1 -| y0), + }, .on); + } + } + } +} + +fn circle( + metrics: font.Metrics, + canvas: *font.sprite.Canvas, + comptime position: Alignment, + comptime filled: bool, +) void { + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + + const float_width: f64 = @floatFromInt(metrics.cell_width); + const float_height: f64 = @floatFromInt(metrics.cell_height); + + const x: f64 = switch (position.horizontal) { + .left => 0, + .right => float_width, + .center => float_width / 2, + }; + const y: f64 = switch (position.vertical) { + .top => 0, + .bottom => float_height, + .middle => float_height / 2, + }; + const r: f64 = 0.5 * @min(float_width, float_height); + + var ctx = canvas.getContext(); + defer ctx.deinit(); + ctx.setSource(.{ .opaque_pattern = .{ + .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, + } }); + ctx.setLineWidth( + @floatFromInt(Thickness.light.height(metrics.box_thickness)), + ); + + if (filled) { + ctx.arc(x, y, r, 0, std.math.pi * 2) catch return; + ctx.closePath() catch return; + ctx.fill() catch return; + } else { + ctx.arc(x, y, r - ctx.line_width / 2, 0, std.math.pi * 2) catch return; + ctx.closePath() catch return; + ctx.stroke() catch return; + } +} diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig new file mode 100644 index 000000000..0a57a0439 --- /dev/null +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -0,0 +1,193 @@ +//! Symbols for Legacy Computing Supplement | U+1CC00...U+1CEBF +//! https://en.wikipedia.org/wiki/Symbols_for_Legacy_Computing_Supplement +//! +//! 𜰀 𜰁 𜰂 𜰃 𜰄 𜰅 𜰆 𜰇 𜰈 𜰉 𜰊 𜰋 𜰌 𜰍 𜰎 𜰏 +//! 𜰐 𜰑 𜰒 𜰓 𜰔 𜰕 𜰖 𜰗 𜰘 𜰙 𜰚 𜰛 𜰜 𜰝 𜰞 𜰟 +//! 𜰠 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 +//! 𜰰 𜰱 𜰲 𜰳 𜰴 𜰵 𜰶 𜰷 𜰸 𜰹 𜰺 𜰻 𜰼 𜰽 𜰾 𜰿 +//! 𜱀 𜱁 𜱂 𜱃 𜱄 𜱅 𜱆 𜱇 𜱈 𜱉 𜱊 𜱋 𜱌 𜱍 𜱎 𜱏 +//! 𜱐 𜱑 𜱒 𜱓 𜱔 𜱕 𜱖 𜱗 𜱘 𜱙 𜱚 𜱛 𜱜 𜱝 𜱞 𜱟 +//! 𜱠 𜱡 𜱢 𜱣 𜱤 𜱥 𜱦 𜱧 𜱨 𜱩 𜱪 𜱫 𜱬 𜱭 𜱮 𜱯 +//! 𜱰 𜱱 𜱲 𜱳 𜱴 𜱵 𜱶 𜱷 𜱸 𜱹 𜱺 𜱻 𜱼 𜱽 𜱾 𜱿 +//! 𜲀 𜲁 𜲂 𜲃 𜲄 𜲅 𜲆 𜲇 𜲈 𜲉 𜲊 𜲋 𜲌 𜲍 𜲎 𜲏 +//! 𜲐 𜲑 𜲒 𜲓 𜲔 𜲕 𜲖 𜲗 𜲘 𜲙 𜲚 𜲛 𜲜 𜲝 𜲞 𜲟 +//! 𜲠 𜲡 𜲢 𜲣 𜲤 𜲥 𜲦 𜲧 𜲨 𜲩 𜲪 𜲫 𜲬 𜲭 𜲮 𜲯 +//! 𜲰 𜲱 𜲲 𜲳 𜲴 𜲵 𜲶 𜲷 𜲸 𜲹 𜲺 𜲻 𜲼 𜲽 𜲾 𜲿 +//! 𜳀 𜳁 𜳂 𜳃 𜳄 𜳅 𜳆 𜳇 𜳈 𜳉 𜳊 𜳋 𜳌 𜳍 𜳎 𜳏 +//! 𜳐 𜳑 𜳒 𜳓 𜳔 𜳕 𜳖 𜳗 𜳘 𜳙 𜳚 𜳛 𜳜 𜳝 𜳞 𜳟 +//! 𜳠 𜳡 𜳢 𜳣 𜳤 𜳥 𜳦 𜳧 𜳨 𜳩 𜳪 𜳫 𜳬 𜳭 𜳮 𜳯 +//! 𜳰 𜳱 𜳲 𜳳 𜳴 𜳵 𜳶 𜳷 𜳸 𜳹 +//! 𜴀 𜴁 𜴂 𜴃 𜴄 𜴅 𜴆 𜴇 𜴈 𜴉 𜴊 𜴋 𜴌 𜴍 𜴎 𜴏 +//! 𜴐 𜴑 𜴒 𜴓 𜴔 𜴕 𜴖 𜴗 𜴘 𜴙 𜴚 𜴛 𜴜 𜴝 𜴞 𜴟 +//! 𜴠 𜴡 𜴢 𜴣 𜴤 𜴥 𜴦 𜴧 𜴨 𜴩 𜴪 𜴫 𜴬 𜴭 𜴮 𜴯 +//! 𜴰 𜴱 𜴲 𜴳 𜴴 𜴵 𜴶 𜴷 𜴸 𜴹 𜴺 𜴻 𜴼 𜴽 𜴾 𜴿 +//! 𜵀 𜵁 𜵂 𜵃 𜵄 𜵅 𜵆 𜵇 𜵈 𜵉 𜵊 𜵋 𜵌 𜵍 𜵎 𜵏 +//! 𜵐 𜵑 𜵒 𜵓 𜵔 𜵕 𜵖 𜵗 𜵘 𜵙 𜵚 𜵛 𜵜 𜵝 𜵞 𜵟 +//! 𜵠 𜵡 𜵢 𜵣 𜵤 𜵥 𜵦 𜵧 𜵨 𜵩 𜵪 𜵫 𜵬 𜵭 𜵮 𜵯 +//! 𜵰 𜵱 𜵲 𜵳 𜵴 𜵵 𜵶 𜵷 𜵸 𜵹 𜵺 𜵻 𜵼 𜵽 𜵾 𜵿 +//! 𜶀 𜶁 𜶂 𜶃 𜶄 𜶅 𜶆 𜶇 𜶈 𜶉 𜶊 𜶋 𜶌 𜶍 𜶎 𜶏 +//! 𜶐 𜶑 𜶒 𜶓 𜶔 𜶕 𜶖 𜶗 𜶘 𜶙 𜶚 𜶛 𜶜 𜶝 𜶞 𜶟 +//! 𜶠 𜶡 𜶢 𜶣 𜶤 𜶥 𜶦 𜶧 𜶨 𜶩 𜶪 𜶫 𜶬 𜶭 𜶮 𜶯 +//! 𜶰 𜶱 𜶲 𜶳 𜶴 𜶵 𜶶 𜶷 𜶸 𜶹 𜶺 𜶻 𜶼 𜶽 𜶾 𜶿 +//! 𜷀 𜷁 𜷂 𜷃 𜷄 𜷅 𜷆 𜷇 𜷈 𜷉 𜷊 𜷋 𜷌 𜷍 𜷎 𜷏 +//! 𜷐 𜷑 𜷒 𜷓 𜷔 𜷕 𜷖 𜷗 𜷘 𜷙 𜷚 𜷛 𜷜 𜷝 𜷞 𜷟 +//! 𜷠 𜷡 𜷢 𜷣 𜷤 𜷥 𜷦 𜷧 𜷨 𜷩 𜷪 𜷫 𜷬 𜷭 𜷮 𜷯 +//! 𜷰 𜷱 𜷲 𜷳 𜷴 𜷵 𜷶 𜷷 𜷸 𜷹 𜷺 𜷻 𜷼 𜷽 𜷾 𜷿 +//! 𜸀 𜸁 𜸂 𜸃 𜸄 𜸅 𜸆 𜸇 𜸈 𜸉 𜸊 𜸋 𜸌 𜸍 𜸎 𜸏 +//! 𜸐 𜸑 𜸒 𜸓 𜸔 𜸕 𜸖 𜸗 𜸘 𜸙 𜸚 𜸛 𜸜 𜸝 𜸞 𜸟 +//! 𜸠 𜸡 𜸢 𜸣 𜸤 𜸥 𜸦 𜸧 𜸨 𜸩 𜸪 𜸫 𜸬 𜸭 𜸮 𜸯 +//! 𜸰 𜸱 𜸲 𜸳 𜸴 𜸵 𜸶 𜸷 𜸸 𜸹 𜸺 𜸻 𜸼 𜸽 𜸾 𜸿 +//! 𜹀 𜹁 𜹂 𜹃 𜹄 𜹅 𜹆 𜹇 𜹈 𜹉 𜹊 𜹋 𜹌 𜹍 𜹎 𜹏 +//! 𜹐 𜹑 𜹒 𜹓 𜹔 𜹕 𜹖 𜹗 𜹘 𜹙 𜹚 𜹛 𜹜 𜹝 𜹞 𜹟 +//! 𜹠 𜹡 𜹢 𜹣 𜹤 𜹥 𜹦 𜹧 𜹨 𜹩 𜹪 𜹫 𜹬 𜹭 𜹮 𜹯 +//! 𜹰 𜹱 𜹲 𜹳 𜹴 𜹵 𜹶 𜹷 𜹸 𜹹 𜹺 𜹻 𜹼 𜹽 𜹾 𜹿 +//! 𜺀 𜺁 𜺂 𜺃 𜺄 𜺅 𜺆 𜺇 𜺈 𜺉 𜺊 𜺋 𜺌 𜺍 𜺎 𜺏 +//! 𜺐 𜺑 𜺒 𜺓 𜺔 𜺕 𜺖 𜺗 𜺘 𜺙 𜺚 𜺛 𜺜 𜺝 𜺞 𜺟 +//! 𜺠 𜺡 𜺢 𜺣 𜺤 𜺥 𜺦 𜺧 𜺨 𜺩 𜺪 𜺫 𜺬 𜺭 𜺮 𜺯 +//! 𜺰 𜺱 𜺲 𜺳 +//! + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; + +const z2d = @import("z2d"); + +const common = @import("common.zig"); +const Thickness = common.Thickness; +const Corner = common.Corner; +const Shade = common.Shade; +const xHalfs = common.xHalfs; +const yQuads = common.yQuads; +const rect = common.rect; + +const font = @import("../../main.zig"); + +const octant_min = 0x1cd00; +const octant_max = 0x1cde5; + +/// Octants +pub fn draw1CD00_1CDE5( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + + // Octant representation. We use the funny numeric string keys + // so its easier to parse the actual name used in the Symbols for + // Legacy Computing spec. + const Octant = packed struct(u8) { + @"1": bool = false, + @"2": bool = false, + @"3": bool = false, + @"4": bool = false, + @"5": bool = false, + @"6": bool = false, + @"7": bool = false, + @"8": bool = false, + }; + + // Parse the octant data. This is all done at comptime so + // that this is static data that is embedded in the binary. + const octants_len = octant_max - octant_min + 1; + const octants: [octants_len]Octant = comptime octants: { + @setEvalBranchQuota(10_000); + + var result: [octants_len]Octant = @splat(.{}); + var i: usize = 0; + + const data = @embedFile("octants.txt"); + var it = std.mem.splitScalar(u8, data, '\n'); + while (it.next()) |line| { + // Skip comments + if (line.len == 0 or line[0] == '#') continue; + + const current = &result[i]; + i += 1; + + // Octants are in the format "BLOCK OCTANT-1235". The numbers + // at the end are keys into our packed struct. Since we're + // at comptime we can metaprogram it all. + const idx = std.mem.indexOfScalar(u8, line, '-').?; + for (line[idx + 1 ..]) |c| @field(current, &.{c}) = true; + } + + assert(i == octants_len); + break :octants result; + }; + + const x_halfs = xHalfs(metrics); + const y_quads = yQuads(metrics); + const oct = octants[cp - octant_min]; + if (oct.@"1") rect(metrics, canvas, 0, 0, x_halfs[0], y_quads[0]); + if (oct.@"2") rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_quads[0]); + if (oct.@"3") rect(metrics, canvas, 0, y_quads[1], x_halfs[0], y_quads[2]); + if (oct.@"4") rect(metrics, canvas, x_halfs[1], y_quads[1], metrics.cell_width, y_quads[2]); + if (oct.@"5") rect(metrics, canvas, 0, y_quads[3], x_halfs[0], y_quads[4]); + if (oct.@"6") rect(metrics, canvas, x_halfs[1], y_quads[3], metrics.cell_width, y_quads[4]); + if (oct.@"7") rect(metrics, canvas, 0, y_quads[5], x_halfs[0], metrics.cell_height); + if (oct.@"8") rect(metrics, canvas, x_halfs[1], y_quads[5], metrics.cell_width, metrics.cell_height); +} + +// Separated Block Quadrants +// 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 +pub fn draw1CC21_1CC2F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = metrics; + + // Struct laid out to match the codepoint order so we can cast from it. + const Quads = packed struct(u4) { + tl: bool, + tr: bool, + bl: bool, + br: bool, + }; + + const quad: Quads = @bitCast(@as(u4, @truncate(cp - 0x1CC20))); + + const gap: i32 = @intCast(@max(1, width / 12)); + + const mid_gap_x: i32 = gap * 2 + @as(i32, @intCast(width % 2)); + const mid_gap_y: i32 = gap * 2 + @as(i32, @intCast(height % 2)); + + const w: i32 = @divExact(@as(i32, @intCast(width)) - gap * 2 - mid_gap_x, 2); + const h: i32 = @divExact(@as(i32, @intCast(height)) - gap * 2 - mid_gap_y, 2); + + if (quad.tl) canvas.box( + gap, + gap, + gap + w, + gap + h, + .on, + ); + if (quad.tr) canvas.box( + gap + w + mid_gap_x, + gap, + gap + w + mid_gap_x + w, + gap + h, + .on, + ); + if (quad.bl) canvas.box( + gap, + gap + h + mid_gap_y, + gap + w, + gap + h + mid_gap_y + h, + .on, + ); + if (quad.br) canvas.box( + gap + w + mid_gap_x, + gap + h + mid_gap_y, + gap + w + mid_gap_x + w, + gap + h + mid_gap_y + h, + .on, + ); +} diff --git a/src/font/sprite/testdata/Box.ppm b/src/font/sprite/testdata/Box.ppm deleted file mode 100644 index 6082475af7d3e2264cf04061e9f63d7c6e6fdc9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1048593 zcmeFaFRU#|ciw$iBm)l$vvgl6&|vaeF)*58W|jQS-P(jW--8Qm(@&&z|p5h;>n#T6?9bSf#00rKwn@jm9cJrOos)6RXCe zwBbJ9D`H>Dbr|K@^F0c&E=p5tuQU~_G!?5f6|1z-Smmd*nLcJ>)mW4^+{b%G>`S>0 zqda@QMn# zT6?9bSf#00rKwn@jm9cJrOos)6RXCewBbJ9D`H>Dbr|L9d|wJ;U6iKQUTG>;X)0D} zDpqNuvC2AMO zA2YFPEJ_>hY-Ya5X%5@m!+4DULu`WteYp*mFt27m>G!?6~(OBiD zw3$9;V%1obHr&U1MeIwt4x>DKzDFU}MQLj7m8N2qrec+*VwE-;tNfHU)5lD#8jI3~ z`*^R2eJR&rl&kZ7DTsAZnp%6MsaU0{Sf#00rH#faKc&s|F%zrCqO{>Y-Ya5X%5@m! z>U>`cVqKJ`)?R5UR%t3$X)0D}qp`|QX)}Gy#Hz6pX?179iQ-1 znwp!^RIJietkRyI!Sz#qYQ9RlI#%UVni`AJu8vjtMm06QQSBzgaaa93jM4E4Kc%U; zDNV&HO~oqh=^0!<<)`MWw5wxPKBcL#DDCQ4m2Xs2;~Uj(LL7J1&%+oUpYT(fnw!#8 ztkP7h(w?5d^;3RozDm0~R^?Ng8jI4dj#c?aH8s9b?Iy%=SN%LaVjfCUb5oj%Rho)b z+S4<*e#%eHS7}$rs(eaQV^P}Gu`1uFrp7m_-Gn&qs-I!5&NqtaS7~Z)N>i~)Q?W{W zdIr}|`KkFT?dn*SPibl_O1nB%)HD9G&9jo#wO^roqSI4S+qnaAusCEDow>I?dcg@ zKjo+9tF)_QRX(Muu_*28Se0*7Q{x-eZbBS))z2`G$|w3&nwp!^RIJietkRyI!Sz#q zYQ9RlI#%UVni`AJu8vjtMm06QQSBzgaaa8e^Qe5HU!|$JDNV&HO~oqh=^0!<<)`MW zw5wxPKBcL#DDCQ4m2Xs2;~Uj(LL7J1&oGb5C;C;Inw!#8tkP7h(w?5d^;3RozDm0~ zR^?Ng8jI4dj#c?aH8s9b?Iy%=SN#n0sC=SdrK!0oO~oor#VYOT8C*Z*r{=4)t7BC@ zrKzzf?dn*SZ&Xv`8`W+?9Cy{vFptV7`c;~mo6=OQ(p0R{o}R(=Q+{f`O1nB%i~)Q?W{WdIr}|`KkFT?dn*SPibl_O1nB%7yI z;<&4Ro*pp|rK!0oO~oor#VYOT8C*Z*r{=4)t7BC@rKzzf?dn*SZ&Xv`8`Tzz#bU8o zEWpG)o{Am49~t#ieavX8UzJb!jcO{N(p0`tKNYLAVZ=Td%x94z`a$#~QR7pZnuYRH z+T&tpvDvgtXll>QXftC`v1%-%ekxXJDxcCuW0l{iHjFststq>KG|M zr9Cco7Mo4Wgr@e)j5aeC6|2TF>Zf9rrt&FmG*%(%#1cO78R?;GU}&dm8SA3Z8TQ-jcUV)W3KuT=z4e*{XEq8l%|f6 z@>AO5VrQ}0v`lDf&&+5uV^OhcETeuZR%t4q(ne#I->5c>IOeJkfv$%~(a%GTPig8H zDLE_N21P0NI)_RNenGZq!A#xm-sVwI-yDQz@X`HgDBh-0q$5a@b%6#YEZ_>`uO zk@8d8<6>vA*|bb(YR}AQGhQXftC`v1%-%ekxXJDxcCuW0l{iHjFststq z>KG|Mr9Cco7Mo4Wgr@e)j5aeC6|2TF>Zf9rrt&FmG*%(%#1cO78R?;GU}&dm8SA3Z8TQ-jcUV)W3KuT=z4e*{XEq8 zl%|f6@>AO5VrQ}0v`lDf&&+5uV^OhcETeuZR%t4q(ne#I->5c>IOeJkfv$%~(a%GT zPig8HDLE_N21P0NI)_RNenGZq!A#xm-sVwI-yDQz@X`HgCKK-a^g==ZJ~pVHJZ zQhrK%Tf=-&?%P{g zbGWVAj(er1@S*^%;CkwdUKHGoXS}ywvCXqokk2H%aHK?H&C|t(rtl&Xn&?G=$L|C! zW^0-}qSCrM$-`d27^UWRJkl0rZ(-a|;kgW+l=7{_s7P&dU5$@>+v_*RdhW6L#@Nht z{rXrcKHr||QIXqAKi}QA))l zdoP348pEp7Yg+Lv+i?cR)(BP|rByt(lH2NQ2fKn;nyoG^Na1~IJ`pl@>tF0Fn}6M0 zo8(n%oBL2-Pfpu=8LXlRp6_2gtw))zs zF^spWUbwB=4ri%&t4J7{=tY5ZuRk2D_`IO&`HjG%) z(67CScxsF6qpR+D)( zuiu2Zy3@Ns4f$eCt&7q|`&F?@Q~8wNj5dr|Q`N7)BKv5qwCeS-kD{GfUXxb6e!guF zy5Gd$@q8}&`KjGDrKxpM+UQtRtkP6IAMq zY+~?uJ{LCYmWwsDE=n8iSH&t#H=_+B)>QQ?nAw-t$3BXF{nXr)ruKpIQ`%^3V(@rA z7dGpbi#4?_N*nE0#VSqZQ+_krFk($rzk->4d424o=+{rpO=)T$C_kl*#wG@j=W}7R zZn;=f>!P&LepRf}R6gZ5qYWe0RP`&E*_YSHK8k+*)ZCP&_JQ(K+GuQI@OVBKHtUv) zHMK5E8|_!cDoy26elyxIVog=Qf|-4Jee9#?*H6t&X=)!RKc$VvCI*k^b78Y?xmZ){ zqO{R|RjkrfKIJ!~4I|c6^(&a!m)FNWihljn+?1yFf$~$@Xl!Eecs>_4>z0c(wJu5< z?N`MrP32R5Gukj>O;x{wnSFVE?4#({Pt8qfY9A;+rH#fW29M`+VY6AMqY+~?uJ{LCYmWwsDE=n8iSH&t# z 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 z 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..8e54879661a7e7ff268d9ee5819a4dd253c98633 GIT binary patch literal 534 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|Vn-PZ!6KiaBo%7&0<2 zFdSO&)jyY8Ujw873H*>`U|{(F|IOu#%MJ%fv|RjN|EgB+TC%u!q(_3t& Z++hvq|84tv-WeN^#h$KyF6*2UngE#jpY;F$ literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd28ba0d428b099b2f709b64e4458d7770942d6 GIT binary patch literal 1022 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfr0s`r;B4q#hf<>3>g_1 z7!ED?>YvN4uK`kk1a?R=Ffjc8|K_kEF9QQZ!-9Wnbsus{Ga;#9U|=xdf~aZaVPIf5 zpiuu>IcC8&7F0D4Six$pIC3=yFdPYZU;nZ!@P*%s5H?Qz2gla8D6Zf>P`Jk~rs_%c zz8FWL{z>|Tt42I9ALDk^)`GFX8C73=hMh_fJpZh=q2kt3Mj~@s@ z_Y|hb59pw)!Spz$nzIl1(E|B^FuFOlJ?If}0GhgRhfRSX*c9obixWh8ngSby85lC6 WKOK2{rEn1_U3j|sxvX9m z?p%L3fu;Yrp^Oq&t7B^rSL?y^T#O&qb2Bjf|NrK&Auj_1L&Ji9YyX~QkY<9J1px(& z5H*aPU^V~OOHXP-Q^LcFrsTIE7syLU;Day&nwr~!8X8Cn7#J81xP#TaJ>19x;wjX> g)|OeYjfH_Bf%A2D-GueIETtf2p00i_>zopr0BHeV%K!iX literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..d9b2aef99d9f2940399c7b29f5191bfef318ed62 GIT binary patch literal 1275 zcmXZceN>WH90%|V4?ezuFQr1x%9XwBgoImVGr%+*J!#pC7PHi6$68A)vDq;3Bxq%= zki|~9vgS-(GgCQp6$f7cIbvoS>432$C=%7b-#xp18=K@6 z>;(WIA)DiN0stHU;2a(R0MLfCy#s*IfyBim=U*K)s&8sS{Wd;wx{_|_;@x)et_rXG z+*~lfuzMfBjV63;&KPR4{Ke#-^5s{OLK;~CQB;o1swt^RVBZ-n>%ARIj$U~wlp{mc zzJyZ4z4I-8RE}d%eopBP1Awk-$dC+~(psMvN%p16P_>>BwR_x4yx_+XyCsw(vrein z*AA#x(cWDSTz;fL-{s~VRQMeo05Cp@1Hk1PHOBaKV7&L03R;EVMs3t-_0)XwF1O=m z|LF%!bw=~Y+=NBJ6iR-(hL`=uOPrL-%*YvKK`;!{K4le^bfyQ<=?+g6k%l4tN@L3> zjp@tx80=$CQ!0vDpIM(ULMn=`NUC-x$+i?U<0%w5b^U%Y6FkkwL68w1*6Sh{%zqr` zan-Oq77JN+%uG@)WO5CsPcT>s)5#Hdx&yJ2!7SFGK6)yYxbcz8mDRHUw0~j%b-K?S z@BZLW5RG`^gX?~LvwD;ELU=45UZh`AKB|> zac{X186M8Jxfhv2ktnBl86sU%U;Ss{(+ivlRGcKY!pSUE?hZLE>M{Tw#tLz zxt!0c!w_k^Zlv6vYj~dBT`Lla$lk_8@n!Ggud)a*O!H}PpY)v5#rTxWb>MFr;=zFl z>Q?1P^?B(nx%%ldPs1GVa?7#nZcuLsx8=Yvd<066h8>EnEpN;l+wvjWI%9ve%Q@qqYfF#cR%^88B*44trrc9GZhzpm+cKzRk2pUPCcr0;emF>oy#MXgne-k)3?Wh8*0gV>y-bT1xF{+ni)$nL@3L8F2f77{k3GbZ0014o%rrDiZ2d+klgVVffiw#ChXDYDa-3Xu@_J_FF*$Zb M;$#-hdl0f3HG3jhEB literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..0a616d72cc695dac524733880abe56fa572797a1 GIT binary patch literal 1870 zcmZWq3s93+7XC>>2r&r(!UCaogAEp?lti!*v+Vw15lEE~At>^a5(cwjpj4~~tcLuN zmxv%K_yFBsc2{GN0PH-Ff5;oLX?j1^aznmV3zzcG14k1x+f58W7QQ4$BTv%V^(Ksp`9 zD2GSBX!h=s#{K-9d6zvO26fp(7>hCIJ9B zR{d|MA3xIM)xCJ&@c9pg9^X{hA9VNpSiz0r#9S+=nljAl$+4Bw2`kk-U!iORh%D61 zT|v5bgz>w9?XvH)hJOzLfXOudn2zY}1q&%>8p9anu{ki0s-p=-rdo?E{P zL=X3yl>p*@>A5j6u6{PF#}57mpJ+~UMnX*&VqOfb)+AnX5j@2E=+We(NPMd}@15L= zgKzhumx^(T6lAWwnSR;F8}~O5?$dDS|GX9nogz!+3!yP&oRO2zkaoF^ehSPE)D+d& zoPRYj{Z6--j&NJ;_LCgA_dU z=ZIo4f)C$wa;KrOXT=EV5fUubDZ+uzF zxl-xQ+Sp!SHD-(ZN3I1F=v((frn~2_u~yn2Tz}0=epTa|u2Ix|=kTOCp8}1{xE8<} z*)7&eQ+vhqu&!oz<*oU8FW{#aE5m=`ZJyrN>w>zvR6r-S`Aoz;SdSwsM2@)<5 z+52{Mx2I*jIOihF2qW*MH_m=*Zg!l;MHr?RHBiNG;=%R*v%32>U03Wpm^%_O>K#E5qqh4X zYBw#kcC6yVGq{yEnX;aN%>zl31floWR?W_4D)$guHH{O8Uti6Xe)(nIMnlA6NEdCg zQ| literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..ecbb650a1c0b08f8038ff65ecd1a70d91f5b8e37 GIT binary patch literal 3404 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 B*17-y literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..1a6cc75fa6d12fd7fe88e1caa1bfc6382b39a733 GIT binary patch literal 1101 zcmV-T1hV^yP)h{!1 z$T8kDiEZeN)WaeSJGm5oeKD^(;msG!+G>wCZE$8+Twgz<& zLTDu+vFVwR@O>vdAao@lgiy9neFcONS~>s#0F-n5&tz7=3$G6iXAnXtTgV?08HUC1 z%iK`Z4+tStF7(hvS@&H3a@_(#-NW8O8-xZG5c)GA007vCHvj+t|NrcjF%AMT3A@wj%$fykn0uvCrOb7q~ppvzDc~_Ku<6dBD9fT076bfSg9}Jn^=V5;j2qA<} zfsix^sWVgP_@H4xNIi@b0ssJz;e5-kO{NSCIkQgAb}ZAW#PKnc_wl-LI~{^s+OH0a?Un=4o>R*2qAPr zXy(-yLsp@No{j@T|1VT-5e(r!LC68296|s90H-h$$uX~#&q$X;NWI*=P(KW8ZNKUa z5$eMTA(UHa^o?dwB@jYrGxI{>FIO}OR6r=15C8zg`2YX_|Nrck!45(p3A!{u zAykZAE)oPl2%%ySLZ}#o)P(@x&3pg=0RR8(!vP5Z004lX|F7E-C@25`008_2;B=jV T=N* literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..58afe3de7ab7579b86fd68a5e0dfff03db55dd65 GIT binary patch literal 5450 zcmXw7cQ_mD*N#nyy_ME(#E4B1RF$aFMynJRrM3#C)JzF#w07*$qOIDB8bOFzd&l0z zOJk3k-_`g1efK}-dhY8y*Yn4@&VBClMBO*m0|9w~00004*S~`R0027x0ND;L0000q z!W&Nk01g)T9qosHSzAPHV3-zz)`RU3XZl%oYQrR#v8e5BA-3!vJ{BU*rQ^Pq1LGz} zufE_EpwOMe>(nrMD;GZ_=b8P`mixSK{$e`Ram&+tfkfuZWS%Gj50SW(NjM3J*TAjm z=#j`NC~QKUkqdsxv*p*~4&NE+IIGJckx1l1Qj6x?ddG6AuyUO!6bgl+0`DY>z|)@s z0D!Bjtu*(9A}wa!Wu(XrGP(q2I(;p2Rw1%~Lp+p~RJ0`8>#f}5fE~Dh{P1^N<{VuR zLZsLG9~oK=_T5ASceav-N%x=3>+Nv;Os)#;dc5r{G^`FN9g=X5$GO zpS&3j9aKi|L>9GY@BYfpSCF2=;1v20OyzqDv*|mUgKL_e-W{(kTLVPY8%z3uYyjAX zY$C2IWb%4#Z@)n4vG2;2Ur@rZ!b&!W=G8Zqq(D3pLy2}&z5CiZZM0DaMsaCy(jbT| zf%#u@53_2CJ&Ki>h&||HhKAx4#D?=S+OBgx=*Ws+-wVG_)P%4BPwgfY*tTps*MipQd}xp%}ocrayy1xVIN zRYP&ZU*(rHhFr;Q@mO>W`wsbK6s8lV5Qg(saq+gHQRo91g~>_+ox!v;=O3PCrjKZZ zZtM^HdKaG5e}X?V-y!E#$`?h-P^HD=6{>I+rF7XuSU?@al=B1PScfS1dXX z6#0Txq`N2A#yYX<;#p03I+OB_A+^i}T)1s6;+AnMM&81G?`TicUAxx^ok2{Hh;vNBM1^g|P z$DXekDtsSG7*|s;6~p`Ra&h`E!YpR(!&!aR2gmhDOX3e?S=>1^)P%L$?aOyt*B0l8 z3kwzJng+nPsuzOGF*!O>UsL?(^AHFjY6ctC)`5?TWBj#@;?71Jk@|N|z@u&T$pc5q zMn>15IBx8Y{CYbNM*u)gWaMCpjL@)1r>U)rPWGY)i}f;nfm z_uZQoKzt#x6Nv|JP2!TS7~WS|-neF;Uc;<^8TPArpDKrVl3j;&yXbo@R=?Rpii*Y% zL_4Wj&G)04&t6P7BbPmDl9JOj^jNhu8k|_@2&bQ#x+ZZ8+riUEf3N1oHEuz)6h8y0aEF?hs3;XtMri`&LC#>5j5Z@FJe zUcQ2nP$RjOluze0|LREA#p~%C%pKL*8liXBsmG}gsr#t;1*dW$_F;aM?t?Fw-Q(S~ zuJ4PIR?!5{52wp>&4JW0XK&E*UM`fw1{Zbe!3(GU-^5mA9x`ZD_7 z#JB0gW>1Eeq3T0Zue-Bi3(INuu)j_JIP>BUHk{|b#jdFE$QNilh~Rom6~$-Aqte+^ zlnXpJzAk1qzm@^_LdItfUk;ZZTxbQ1vt%2Z+NS)&#Bj@^ak=_vH1O9Rr>H6`FYHUO zVY02oO%y}33NrRv?M@N-c}#}2wfU>{i6~=d0E0e&VQi?D28=@RlZMMix1O4FT-#(c z_)=BPh+{76h{_*NePPYOUh<>Fe=BYyIQ{3{>O@MKo4+Ql4Z2GSs-Znjre=!UQxelOpc1FoNGsmmkD)U zU~?~zb*69jvMFFDlp+t(oSjkp1U+_@&N8g~YmiM(}CEEk3j@fIK33=pP z^p{nB%+4^XOQ8k&JsU7IxCrmKsf3mt5kKLmh2)T5rQ-8qY~D2NS&O2wuLRJ$tUxR9 zf&h9q8bzS*F-5}8K!C@8S`vzVXUL(Px%luvLx&WDe;*!8Eo4cx={E#omRdMa3^;X* z?;NYdrc7npLfabOUQ#Vhx_ag38H3%1#eQm4-lc`4?1wTeG^kIsDhz; zQDr4i+&Q>pZ5J>)N9XrZQU)!$;nJ!*N1|o?o2r+Jlf~ zPB8(`P4JN?HtZ+zAMx}2X}8}jJN$D;ZII+Gxo@!$mmMh{K@NddH4Q2jfBX--%{1?D z*r^*Yd`GcN_ncR1*)0)%e7(tPws$?+c!C`FHpS-C+l$Xh%Vvq$h32LiWSZ*Mzpw#* zr@5)Z%`YmsjM5%yO&NG8aLDvW;qTPyY#Oyo2cM$mc2~?s*iLj8$0<7Rk=qr|Tk4V0?gH7Nm zpM8qO=LorO*hLYQ!FiQwJ>b14<4tqq!KQS}uHrMZ^t)UBkVJ1fqV-d9qM^XenaJ_7 zx=iW)O=)e*V|1}rsf#N73__+eVTR9cX7k<4w?L=~Tq>VFJR_^DUwtm0$|uG%HLSYb zz+{DePIEGd7k=dZO+hDNr~Sn~1Kxh1nl>wACLMkJ$At28BBEuUrh|MM{hiHmddU&t4E6SoL+^z!VW~90 z{sA|r-eT71c{GGY46mT^{#}J4RMK;hoR-HsDQMz~Ov$<>56q>7C~^g5$vhik<>K)k z!FBC>)Q4U$EprZa8$SpAhj&c6S>ab?|BAEqqWiKs!z%SI^Ll^cy5;qoN<#){TabP zqg22AEX~b!Nnl)F9yoKaR3+gjCeRUg=0I>~Jmp~)Bas3Wtv|JC*hiZd8g_wlP83V8 zsBX}+9j{#4INY1D{;ZPlGrSjC+s1a?4cyXp*dF7S4&kBztFA=IY;cgOmY*0}+2Fdc zbzZXZi8>A^bk+;H%5S>b_+!?JBOn|)VDL(W=HQ{mMubaxt=(?c7^>jeP0{e)oew7d z*&-Y|S%x!(Dh@tYR+9RQ@qCwU{g_P)LR)sUomppldnh|&`l50-(ed@Z?b2L~j#-ax z`zLnbAB&7ySzcwOrLAv`;SJJ^lT^xqGdMb{hmr;#p%?D(xx^h2J!k7zfwo7k^^Ny? zpDN1~EU*%tyNM>I{;I7Bl}cwRLAT=Bm!lCt>2!t>nmMI*wwKmN_{e%lKKJ8zgr|E% z&Q8C|oSL=S(88T)Pbx6SghQI~SB?F~5&K6V<9u!|18|)Z;eg8Q^I4e3<9qFY6LUk=AbdtH81gDoKf#HZv&EMFWL?9{1;-PszI7V}>|+RM4i zcTp{90Gj9hG!f0vJQz@ZY%D_w%Gqd+C_UtELcHWV^AoiviuaF>*%2FuRS$MKE?8&l zgndYN6-Mn;s))xdyaKq#zWH_<5jrtJVILRYzwduN{UfJoP_N|==T%0ksDlb@u{6)+ z@&k6?gSa-RX8?+0@~Z`F27QccJ&8DEk~rU%w+p*CYvS}%9yr)yB`lgo|0cMgM;F(3 ztnzG67~<57Tp}KslVKj!e?eC42NVaGQ(y)bv8|bYw7$<6T>*2>hokQoRRc3KT6xjO z!%~N`ZbAM)SHOAAiW)D-rjX}XIW>Np(3EFNQ2XasIS|bjH#pS2GdOt7j12YJCyS}V z$`7}BYovZz`M*(N-SVyo%3MHk>m*r=>`IFcEGU|fNscE!(xOX_*8%3Duhmc{?+Z9k z5d{JqwsaxC1;+!14Ji2r0@P)OK8^96|J~>TfOF9$Wo+|_GVYjyQU~=MjifDQU}k7Z zAqy3(mD7S!KBlIPSojHOsivp?o4)XSkkmLG-Qwqvf%UAE@1S1pI(5q4jLj26%?(5* zR8ls*dYXqcQQ-PK5C37v6Z^IVmS1cvXrfD9N7aR%yX!dQIScFdm{s-g8zk_jh(bG` zXHN82r3E3cG0n;sEC<8MQgdzTJhQGzn(7msN9`XRv(EE(z9z&xC4H6DwzkX)t|1?R zRsT+!7}RSEvgDjER?tu5d#74gZRHzHGA{n{rA%x2?b7D9&(VUb+uM8Dl2nY! zUX|Z^)Wn9St=790g3V@R6ueSc5PF@Drr|c68n_;T7VEE{)m6}6&6>(xUh05{Ml@PKn_XO9qwxS=C&Eiveo+i51 z5b}y~V|(qNDebWE_NEu+7O1E61qzj{HQqjljZgcT+OMx zoH%g^x!3P1s4XnsGMbrFI-}s`XtOQE8l3MaW;~@OS2J-4f%Lly!U^$%FWi2gjBU%Ao1V$LVUb2^>k;+H!s`nQ-pU`(8`szsgC%tCR(N?sNB%Jq7(Mw%L8|%TVU< z**9wC2KrzL+j}TF3Bey$Uix$r+WdE@wzNfS!<<+q#nu{m40H_8Z>ThpWFv|Cuze4Q z%0czZ>hVI=XYC)qdR%hD?04k?%d9R0q!;%azv=a*=9m;c9(^~mvA-r16D7+)T=r;` zyq4;6UQn_DSfCv_S=LpJ`1TljDY*mpdO-2Z#TF(1-nIM54sC(lMU3vGTrq;gQM$9E zPJ6IXeXK*p5WxNT!td{s>>Z!snCq6Upf_i`|G;&KB|Lfj)0@KUBp$f8Fkpbd82 zktsPGNAl0)X!0uT$Hza1#ooXoSvIl3QMM3TaWNH>&#nddho7z3(VCQvh$eQr-tnFf zUv^0rvRk%RMr#%ng@%tF9BBLDh>#=f@uqIOvK>pOZJg#04uJ}DApv%#il>v;?082Z z(UIj^j$M(=LwR#5)^=aP5w*pDw03{7F#Xsk56W&X6;R#^^SAo(mWfxPL(j(lx^XmK z_d!|d&KxA#8B16E*RK{JQ~?P!P3s%OSL;f81_CzE9+o<$PGesFsfz9c@7Tn(Lmu#O}PBJyx86Pn<_DdFK zC^sWfqy0Ab8f zMsgZcii^^;xi#$Zs{3I?=)P0W$?UWK1Km1g7;N&zZm;jqfT^p|?n^b>9vb>!6RTO( zV`XOq9Vm6wXm7Y8G;?%pHNc}QA0U5p3JCM;1U8idrB#)(y&)kYcvlfX^q&~L;nwr7G}}_ literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..784622dd0d6d0db26ac98c6342373459b20fad90 GIT binary patch literal 5724 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..d4e488525135da1459ef46642d2ccb21bb9d1018 GIT binary patch literal 9997 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 F`(L$nMXLY+ literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..9be9501ba18ef61d9715e67027bf5b9ef23e861e GIT binary patch literal 4298 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 bjno~6&hMTW%=PxaPYN{^O-#9xW!V1#Cja!) literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png b/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..100d9fa18d6f3b5d73c93c83ca1e32288da0a875 GIT binary patch literal 2223 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png b/src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..304cc711f6c5ca26da0ce6c5066931775cbb542e GIT binary patch literal 1547 zcmY+Ee>~H99LGQNqiKW=M^|U+A(G5$U6GOb@hHl+$`LK;9@pyTT%jMwu+77dx}+b@ z&rPU@v)03pXf#HfJg$}>O`)?257tp=Y|9Muy~lSvy1PG~|GwToUhmiY3YoZX4eA>d z0ANkf{(!>(ARGWN@HGJd028&G4S?~>pa4SXubfHQ15}%@Wnj-DM5?)#;V{0cO@3!A zl!ecH+IBu+0}wGHV&0V|YG)RcuM?>a!6YhPT5wRyVxC05?&n06%Q%#B*@od%uLZn7 z#rF~TEdv(5@}%0p3l;~_7arI zN+{$h^V(SY4St>HZP{*yG}n~(S)N9N#lg86u4LwmZ#CI002zw z&xH}&s?#ENIzQR!Igz#g4K)=j(?J+hf& z_zLe+x-YsoV6R8W+``#BIjZj#r92WYSNr&;*Ub+&_Y*#UyfDj)@*HK_Q=cP^w$1iU zLDls544NJ$)Hm1`lMK$HoVOM0E)+oHHUcZ4rZA>I>kI@xGkGl zm7JbX=Dsl-vmfDQFHtXHwF<(nJ7-X=`ZHR@w5wCc zjrT=tcXS8t?n^HmW_;6;R)^^N%KoLwZix-A3kK0zGiws150XAiLQNAQPN}HjhH)?6 zat-xD002COeq2|F#|`vMc4h}FIvb>zg2@9r^X$>5Nw%+{QF-r|(FWPV5TC^V?n&!}_RlXZO1?XgNVRTJ zWh2$22ek+UJLUK>jXI5{k*Co#pfRL>X2ff}3Tlh_@}0IRYmR$g_#)-+uJ2}QfPhN( zH8T=HP!hTpX|@01xA#Q{O#E?D`(8r;+{3hXb*--!RS#N^pi;(iOq>1+YITT?sxQj( zL0=$J${r?+7*v!PPSP4QjKd#Pz0IR6d>regvfVhi!*E;C$B*}NANl?SV+EM*^N1w3 VL35k)9u4r*3koC#RQpAz{R0dG@)ZC8 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png b/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..012a5b952848f82e54e56fbb0568b14e15cbd914 GIT binary patch literal 2490 zcmYk73pCUHAICql&1KA#lA;hImrN?5vX!2wr{=CklBbqyxkM|MhLCuwV^prK>wl#w zVQ%F?(f^?_<<@9!ixU4_D?Hl&Y-9ib@0{29owKv^{d|7!&-?TKrZ^tjhsCVH007p( z-p&~S!2m$|$O8b7j0V*K6yhB0h_2M!(GGJA4X=de@`dk0!j(Xi{BeXiD;8l2h0^sFQH4A0U7@`S(SlLk)Bd5}n$Kvb6 z1ZOP=yl-cNl2t!3tS&)tN$zh1P^YJ^zL{S&5-5B$=HkI*k&fPB?}|v|&57%}Y?4-_ zIm)BB^SzbEc7AEEqPXv57XoJxi_K|Nk1j{Irr;apZ>eHSKecUbyW!I5=&1}L9Hdy0ZX%`zFTb~TzPHlA5T)z8%Yy4>J|1~GcaHo$ zLui|cr36%6xNES3erkvk$vdr6M z<@5U-ix|nqidPu}*Ngx{8sx+u!Yjfg;+4>jLSey2FI0+t7u8CRKUtELW3qZg((-uF zr@{_~MVUvTt+y9RH)2w-RN zesjIz*|VU_l=lnO?~^z3c3m1BY;dh^$Bg*T2HHEeT&o%#aahNu)g_xU#*hC%wSAI| zs7FUR5f386t@%@1yQ?GOoBlDkvm8r;Oe}aZRdw2)cI*_7~U-z4Q>dwv1JaCK{s9YV^nJwT%68dw-Xmys|Yu&)7W&iN4 zE$ZRTWG_z!+bSZCH`Im>$U(w*?|#zxSPOsrnbHE|$u@LAlglcmDd{MKy(4-|GP`b~ zC}2b;5`tp@_$)!jH%mHd%A>L|cUZEdop-RHqk?YzRrL^o zN_zG?zqof)@YV?@iTMf!83#4|Vxdk`>*hXGDsFm0^h!2bk}c%DRGvhDBW}Oy>w$TG0^+`^vaT{@?Sv60IZbXM~dc5>>k-Q24M8HTgi3x_N3&%8!I zkOI*C&yG0Hxf_Um&&nC~O*GxZg{x4KMr_tf+U*fXa#HJ$i(hSeS!xd^rZhvI^pkFC z6LA2@u_u05>*C_5ZL@FdJoyonu;QcKaY_}VpTnM01kKRnJDi3Id0q_i5^JH)CGxj4 z&s|L0=~GvwSA*!9SO+xQuk?D!m+;InU@pFWE)JwUX@$ zCE62C=%vY)9}l~`@^MyAerBMu_XjtZb!>Efaxih?xXl7Oc5%%+%`uno>X%yQdwFEX z>*{xBsW^7_K+7}EXSo-aC&SZB)X(L!%E@{h~1y#LYj6dmGJBR~&Rsv{$5zD4gf6r${TD%kf1V9<;)pKk0>`_s(4to#T J74G(l|1bGGWx)Ud literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png b/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..563f23d093b48bd455c244ca49e3b3448efbab10 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0y~yV7S1*z&L?}fq{YH*Mp4(3=GUMo-U3d6?5L)aO67V zz{7gs-v3LYEaFXzC6=lF+_PQNglDq2eUu#+4iaGbIrb~-^0wd?_GCCt|QQ{ zul&QzYiFl28D6h?P;KA3``t-y76t}^?bExtUBdUP@06=JZoGE3Hmj1)oj5%`0R{#K zFugeB-)-ypnRn-?o-C=?J8*6G(^K<)HC2CNVq`G5&dk8@|3An%%?<)A2mb%J(x31^ zWruC9XvGHui?fdsZy#1H-5V3D5OjCX#pCB)^v$_kcI}E=2QvMDFIXmQ=f2PH=XA#9 ziFE8$i;ezN{)_43lK5TEk4$e6{U^i0@L=t$s-3$Wmi(%lbe(;kh49`x8Um?cA)A*q zlb2JoXSyyMp3gRMU3_1Wp&|d+*P_Q_N>`7%$k(e^?O5CD zasBG_Qrq-3#G0u@eyg(C9mL0)xdV%V|b_0v~Og;P%MnpmS&WwHD2%eZ%M zzn}fIWck0t#lM&)LKGUo6i$5g)J?t~Y|^E*uTF>AF4q@$x$kGJN*$-f!{dz%4D)K? zLC0*!(em*3{ax%DLSbxkeg5(P+Tan9`ddK#U3h$l$<)uwmaOc!y-xcv^Fytz^6*`S_!j9gPMnC(B#gOKee)<6vNT5L&o1=8i)_>Ay)|*~@K`@5W>+v{?RE z{U<-?{$z6jwYTb-k7Dxnvwn17U|?9kO5bJHDND->AxCnrPyN_fF!kKMcQwa$SvpO+ zUCVRe*6x2-sz0$cZTzd^JLmMW}P}oq-AqFTej`-n0APHS6z8oHI^Fon$`X eWHF)RM68|Cr4Ns$rfU_0@|<pUXO@geCw+GruYT literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png b/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..aa1569bfaba5e3e5faea55947ed3bdf3456e6736 GIT binary patch literal 1104 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=Aydo-U3d6?5JkFl1z4 zU^ukktA8%Hz6MAE64)Wdz`*eT|C_^xybKHs4GaFQ)qTh*&4i?afq}t*6QZV(hk=3N zfI|IiZJ7nzSWwj@a6!~Ca)QnIzg~J$6Phu1SkaXH7UTl?00|U`L!1*4yYY(x&rwF9 zFelYDE!qo(v=}+H89Ap0aC)p@QF$_3FV;gNwfeOA;aZFIW&AT+4xH57{{w2ygY}1B zcqiWab3gC+)1(JR|1MRZ$Z7rR{H|qV$K}4dDM3%3O>9knCTDf7kLy{m%i0n*mt(zK z@?>3gSl>^&v-Hdq5?bA%Wd34+cO_73##gqtv{2>_459HkEX>dPkcYgy;iNLOL*GV8+)^!weZh; z$(4LMw5!Hm)>`c6&;V&jd2>YrVkK6I-u%gNIXiDGyEm#(?l`hVpO^P|mnt-=5M0^0wzXG~VH>D;QDb8FFQ_hmQLq?FIx`LeYz!%H8@A3dvD7Nmq*&~gLK~~T3YRmGK#6J{L{L9o+MJbgo6kDkL4jr z^!8cKW(NV*2SM9Jyf?H=nIxohG)U*Dn3IqSOPOtJ!{>ecHg9EWI^OXw=4M!tentQL zx4Zh`htvaK_iQ`VIQ`v+AhUw~ia&pRT$1o0F#6frj0d?98?N%$@NzGk)OdYz*Vcpw zd(Bm4Z^dbS_?u~`J2T$2ez$U+_B+nwdk@cyf4Qg9B7dpq_G#yC>mGgE{@Kqa=-d`q l`uCCpyYMX{FCt|$b;fYz@5)a&<;_6KJzf1=);T3K0RVa7(p3Ne literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png b/src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..b77aee11eedc8bb85ce379c2fc4844ee22653e64 GIT binary patch literal 1251 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png b/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..d09df5e457b30258dda2c21cd7dcd49516ef62a5 GIT binary patch literal 1114 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=Axao-U3d6?5JkFl1z4 zU^ukktA8%Hz6MAE64)Wdz`*eT|C_^xybKHs4GaFQ)qTh*&4i?afq}t*6QZV(hk=3N zfI|IiZJ7nzSWwj@a6!~Ca)QnIzg~J$6Phu1SkaXH7UTl?00|U`L!ARR=C+^)nwt-V zLe$JO^!w}}&|1u(;$^{LD#q^ca>9xg8WWE)3h?qywU)BtYf$y#Yj8>JWKIYyW_S@) z(BP=DZBFHG>(`2YNztE|#P6S1mTojZ^6fg?Z|75tX8NdI_HfywIsBJJ(Z zynCaO#LoHW>(Mv=_9>N3I52#M~lh+2R`gG{c%auyomZY3i z4RMm>+@7??rDJBG)`REiY3I+2ZnRAG)>|xd>ckGwWYvT-Jwc52c1G2eD?8>2rGCAy zd9q_!fSd8Fu-1)<2`7AdZ8pFB{rKz4f6pF0Dae2KozGt{@7?@3`+ct^`T5^WU$;&` zOaJ*#BdyPxJ-(NImH*dTHih3R^y?c2Fjeikw)itL;- zQ;=yt^P7oTp3I6LPIrfevaR`$S#j_%SO5QYToTs|))Z7mPkPF**;Z%zwkEq-mk+xV{XvWk_^=hYny2gWSWA#${7Vtzc_uAIZvv9UsBQFj_Si@mQD#3(GBID zf=q{hE9}wj6Og#~<6-pyj>d7QyE%jT{91F{JH4nq~B}9eu&=B zjOG$+SiJIvMjGF-Wu@zN*1maDRsOw7EZgAc({Gt`wxwQRD(DW$5iw>@J}tFlP0p?d zaW{B&wciQz-Mj3#y8plGRX2{d$i>fH)v-KFEqvp_GYdoBD2D%Ee@LL(sz)Fx`GDOa z2QAqRMN_j{vv<7RG^gU40jv7wkWZ85WqenhTQEIq_eUR_-6EWmrw1Q3*S+ewSa_cE z+J3R~mM(`p%+=0JHaHqjEjmOLkKjy!k<<4V$NcG?bWuAodKh#zw z*M!TKzSYdze_`I`@4@nBhJUa8PCi|H{acdo^vTkJe@nj`uc`dN)xYK0)u4*Zm49@X zwK}bJjg2k}^zPi!?)^^HV9vDehg)_j8qArdoh8AbFqwJY1{-?;Yy0=04Cv|V=d#Wz Gp$Pz$r`l}* literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png b/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..f88688c1409204a3158c8c30902d1e1ead9afce1 GIT binary patch literal 1423 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 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png b/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..3a60a59d5b294d62b39cf4f00c0a75aa1c9d8981 GIT binary patch literal 493 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E80JY5_^D(1Yoc96H( zfTuOlEWyO>14qvhjh+dCMop`?WzLFo;E7ykHBtkdoKbyIeKxa5!Ux$Boc){cGNfBfU}(9@AcH#e44Fd9Vj|G6dhK2?I*WVV@(11&W!2w^W z8be;NntyB8K4g?;LREB!1!4|dP3}WhbTu(dXlisHa^h0c$iu+Ea6qB{wQ|gYZ7isE wNid?hS$a|vnschbO5P&8^;?h&6ptX#!z#vB!P|9P?mvj<>FVdQ&MBb@0NY@q&;S4c literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png b/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..8b3134e52adbba897a0f480d92636471d713cd7b GIT binary patch literal 636 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|S$@r;B4q#hf?SALh0g z@ErY^bBLqo07uV(3`xz!6)fSR0#|~rPTTQW^2`6)8q>}%A&u)r7#QC9g$Aq2O)L)# zxRNR&GNmG5LSqw8V>xeV=<5j@f>Yx?FMpf1+eYnTzR}l|o0Fb?G?qL3Ys#uOXZvocFCI?-%66Pb-PvShCx5o9&6+JCEgVpLn}$gY~~SZdXS(E+ep0 zL{b(gfcyvn9`Css82 literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png b/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..64b1a80b8d1d89ee9a062a167cde4161afa90df2 GIT binary patch literal 1218 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq|vd)5S5QV$PeH4|AIh zL|Sfk2-+|yr;4~$DjRsYRfJv@ySdDzK(|i3`2KpA>7T9Vs!qCbSfNG0i9@kPz-hvx zclRbY9hlR*;L;DF8Jh(|q~e*1&6>O(dncT83&^bDJadynt5?6#QrdCpM^%G#;Kc|(uiLvL|{TmujqgWGbew`|wQna*b!uqgv>QDEJ za(rf5Ei$_;anpT~O22A>N#RSJDr@%E<%m}LRVzHJx-duk^}Fb`CyMVeZ@kiM6TWP| zdGNX%9q)A_a@n@&k3Ytq|2L=GMqoYrT6yo|hehW#AKiO)j(w$qmFBwpyqA>w{s&z5_^`_XpAU^@l|hX4QHTr}imP+&OX@PA$52jPW0<({HnoY*A4gzZ1L zVCDUU3s>?DGG3WWn0%GyVFE<~6g23|K(rrbxEkMw?C5INqnpF~ zkQ-f%H&jg{4+8_k0fqY4+%gNdv7kCAjG)gCgrF zL4eJ{YC+3>mgz5qG#V#q990$gx%A1AgSU=&%-k6=e z)wF!}_JuJ~`Ks}WxqH`E{yVi`{ try drawSingle(alloc, width, line_thickness), - .underline_double => try drawDouble(alloc, width, line_thickness), - .underline_dotted => try drawDotted(alloc, width, line_thickness), - .underline_dashed => try drawDashed(alloc, width, line_thickness), - .underline_curly => try drawCurly(alloc, width, line_thickness), - .overline => try drawSingle(alloc, width, line_thickness), - .strikethrough => try drawSingle(alloc, width, line_thickness), - else => unreachable, - }; - defer canvas.deinit(); - - // Write the drawing to the atlas - const region = try canvas.writeAtlas(alloc, atlas); - - return font.Glyph{ - .width = width, - .height = @intCast(region.height), - .offset_x = 0, - // Glyph.offset_y is the distance between the top of the glyph and the - // bottom of the cell. We want the top of the glyph to be at line_pos - // from the TOP of the cell, and then offset by the offset_y from the - // draw function. - .offset_y = @as(i32, @intCast(height -| line_pos)) - offset_y, - .atlas_x = region.x, - .atlas_y = region.y, - .advance_x = @floatFromInt(width), - }; -} - -/// A tuple with the canvas that the desired sprite was drawn on and -/// a recommended offset (+Y = down) to shift its Y position by, to -/// correct for underline styles with additional thickness. -const CanvasAndOffset = struct { font.sprite.Canvas, i32 }; - -/// Draw a single underline. -fn drawSingle(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const height: u32 = thickness; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - canvas.rect(.{ - .x = 0, - .y = 0, - .width = width, - .height = thickness, - }, .on); - - const offset_y: i32 = 0; - - return .{ canvas, offset_y }; -} - -/// Draw a double underline. -fn drawDouble(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - // Our gap between lines will be at least 2px. - // (i.e. if our thickness is 1, we still have a gap of 2) - const gap = @max(2, thickness); - - const height: u32 = thickness * 2 * gap; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - canvas.rect(.{ - .x = 0, - .y = 0, - .width = width, - .height = thickness, - }, .on); - - canvas.rect(.{ - .x = 0, - .y = thickness * 2, - .width = width, - .height = thickness, - }, .on); - - const offset_y: i32 = -@as(i32, @intCast(thickness)); - - return .{ canvas, offset_y }; -} - -/// Draw a dotted underline. -fn drawDotted(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const height: u32 = thickness; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - const dot_width = @max(thickness, 3); - const dot_count = @max((width / dot_width) / 2, 1); - const gap_width = try std.math.divCeil(u32, width -| (dot_count * dot_width), dot_count); - var i: u32 = 0; - while (i < dot_count) : (i += 1) { - // Ensure we never go out of bounds for the rect - const x = @min(i * (dot_width + gap_width), width - 1); - const rect_width = @min(width - x, dot_width); - canvas.rect(.{ - .x = @intCast(x), - .y = 0, - .width = rect_width, - .height = thickness, - }, .on); - } - - const offset_y: i32 = 0; - - return .{ canvas, offset_y }; -} - -/// Draw a dashed underline. -fn drawDashed(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const height: u32 = thickness; - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - const dash_width = width / 3 + 1; - const dash_count = (width / dash_width) + 1; - var i: u32 = 0; - while (i < dash_count) : (i += 2) { - // Ensure we never go out of bounds for the rect - const x = @min(i * dash_width, width - 1); - const rect_width = @min(width - x, dash_width); - canvas.rect(.{ - .x = @intCast(x), - .y = 0, - .width = rect_width, - .height = thickness, - }, .on); - } - - const offset_y: i32 = 0; - - return .{ canvas, offset_y }; -} - -/// Draw a curly underline. Thanks to Wez Furlong for providing -/// the basic math structure for this since I was lazy with the -/// geometry. -fn drawCurly(alloc: Allocator, width: u32, thickness: u32) !CanvasAndOffset { - const float_width: f64 = @floatFromInt(width); - // Because of we way we draw the undercurl, we end up making it around 1px - // thicker than it should be, to fix this we just reduce the thickness by 1. - // - // We use a minimum thickness of 0.414 because this empirically produces - // the nicest undercurls at 1px underline thickness; thinner tends to look - // too thin compared to straight underlines and has artefacting. - const float_thick: f64 = @max(0.414, @as(f64, @floatFromInt(thickness -| 1))); - - // Calculate the wave period for a single character - // `2 * pi...` = 1 peak per character - // `4 * pi...` = 2 peaks per character - const wave_period = 2 * std.math.pi / float_width; - - // The full amplitude of the wave can be from the bottom to the - // underline position. We also calculate our mid y point of the wave - const half_amplitude = 1.0 / wave_period; - const y_mid: f64 = half_amplitude + float_thick * 0.5 + 1; - - // This is used in calculating the offset curve estimate below. - const offset_factor = @min(1.0, float_thick * 0.5 * wave_period) * @min(1.0, half_amplitude * wave_period); - - const height: u32 = @intFromFloat(@ceil(half_amplitude + float_thick + 1) * 2); - - var canvas = try font.sprite.Canvas.init(alloc, width, height); - - // follow Xiaolin Wu's antialias algorithm to draw the curve - var x: u32 = 0; - while (x < width) : (x += 1) { - // We sample the wave function at the *middle* of each - // pixel column, to ensure that it renders symmetrically. - const t: f64 = (@as(f64, @floatFromInt(x)) + 0.5) * wave_period; - // Use the slope at this location to add thickness to - // the line on this column, counteracting the thinning - // caused by the slope. - // - // This is not the exact offset curve for a sine wave, - // but it's a decent enough approximation. - // - // How did I derive this? I stared at Desmos and fiddled - // with numbers for an hour until it was good enough. - const t_u: f64 = t + std.math.pi; - const slope_factor_u: f64 = (@sin(t_u) * @sin(t_u) * offset_factor) / ((1.0 + @cos(t_u / 2) * @cos(t_u / 2) * 2) * wave_period); - const slope_factor_l: f64 = (@sin(t) * @sin(t) * offset_factor) / ((1.0 + @cos(t / 2) * @cos(t / 2) * 2) * wave_period); - - const cosx: f64 = @cos(t); - // This will be the center of our stroke. - const y: f64 = y_mid + half_amplitude * cosx; - - // The upper pixel and lower pixel are - // calculated relative to the center. - const y_u: f64 = y - float_thick * 0.5 - slope_factor_u; - const y_l: f64 = y + float_thick * 0.5 + slope_factor_l; - const y_upper: u32 = @intFromFloat(@floor(y_u)); - const y_lower: u32 = @intFromFloat(@ceil(y_l)); - const alpha_u: u8 = @intFromFloat(@round(255 * (1.0 - @abs(y_u - @floor(y_u))))); - const alpha_l: u8 = @intFromFloat(@round(255 * (1.0 - @abs(y_l - @ceil(y_l))))); - - // upper and lower bounds - canvas.pixel(x, @min(y_upper, height - 1), @enumFromInt(alpha_u)); - canvas.pixel(x, @min(y_lower, height - 1), @enumFromInt(alpha_l)); - - // fill between upper and lower bound - var y_fill: u32 = y_upper + 1; - while (y_fill < y_lower) : (y_fill += 1) { - canvas.pixel(x, @min(y_fill, height - 1), .on); - } - } - - const offset_y: i32 = @intFromFloat(-@round(half_amplitude)); - - return .{ canvas, offset_y }; -} - -test "single" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .underline, - 36, - 18, - 9, - 2, - ); -} - -test "strikethrough" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .strikethrough, - 36, - 18, - 9, - 2, - ); -} - -test "single large thickness" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - // unrealistic thickness but used to cause a crash - // https://github.com/mitchellh/ghostty/pull/1548 - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .underline, - 36, - 18, - 9, - 200, - ); -} - -test "curly" { - const testing = std.testing; - const alloc = testing.allocator; - - var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); - defer atlas_grayscale.deinit(alloc); - - _ = try renderGlyph( - alloc, - &atlas_grayscale, - .underline_curly, - 36, - 18, - 9, - 2, - ); -} diff --git a/typos.toml b/typos.toml index fafc38858..a8b296755 100644 --- a/typos.toml +++ b/typos.toml @@ -32,6 +32,8 @@ extend-ignore-re = [ # Ignore typos in test expectations "testing\\.expect[^;]*;", "kHOM\\d*", + # Ignore "typos" in sprite font draw fn names + "draw[0-9A-F]+(_[0-9A-F]+)?\\(", ] [default.extend-words] From c96af1b3b15ceb03c39178b1dda2c86422247db3 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 29 Jun 2025 15:59:33 -0600 Subject: [PATCH 074/114] font/sprite: add separated sextants from sflc supplement --- ...ymbols_for_legacy_computing_supplement.zig | 80 ++++++++++++++++++ .../testdata/U+1CE00...U+1CEFF-11x21+2.png | Bin 0 -> 559 bytes .../testdata/U+1CE00...U+1CEFF-12x24+3.png | Bin 0 -> 744 bytes .../testdata/U+1CE00...U+1CEFF-18x36+4.png | Bin 0 -> 1388 bytes .../testdata/U+1CE00...U+1CEFF-9x17+1.png | Bin 0 -> 400 bytes 5 files changed, 80 insertions(+) create mode 100644 src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png create mode 100644 src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png create mode 100644 src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png create mode 100644 src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 0a57a0439..9f7e8815d 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -191,3 +191,83 @@ pub fn draw1CC21_1CC2F( .on, ); } + +/// Separated Block Sextants +pub fn draw1CE51_1CE8F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = metrics; + + // Struct laid out to match the codepoint order so we can cast from it. + const Sextants = packed struct(u6) { + tl: bool, + tr: bool, + ml: bool, + mr: bool, + bl: bool, + br: bool, + }; + + const sex: Sextants = @bitCast(@as(u6, @truncate(cp - 0x1CE50))); + + const gap: i32 = @intCast(@max(1, width / 12)); + + const mid_gap_x: i32 = gap * 2 + @as(i32, @intCast(width % 2)); + const y_extra: i32 = @as(i32, @intCast(height % 3)); + const mid_gap_y: i32 = gap * 2 + @divFloor(y_extra, 2); + + const w: i32 = @divExact(@as(i32, @intCast(width)) - gap * 2 - mid_gap_x, 2); + const h: i32 = @divFloor( + @as(i32, @intCast(height)) - gap * 2 - mid_gap_y * 2, + 3, + ); + // Distribute any leftover height in to the middle row of blocks. + const h_m: i32 = @as(i32, @intCast(height)) - gap * 2 - mid_gap_y * 2 - h * 2; + + if (sex.tl) canvas.box( + gap, + gap, + gap + w, + gap + h, + .on, + ); + if (sex.tr) canvas.box( + gap + w + mid_gap_x, + gap, + gap + w + mid_gap_x + w, + gap + h, + .on, + ); + if (sex.ml) canvas.box( + gap, + gap + h + mid_gap_y, + gap + w, + gap + h + mid_gap_y + h_m, + .on, + ); + if (sex.mr) canvas.box( + gap + w + mid_gap_x, + gap + h + mid_gap_y, + gap + w + mid_gap_x + w, + gap + h + mid_gap_y + h_m, + .on, + ); + if (sex.bl) canvas.box( + gap, + gap + h + mid_gap_y + h_m + mid_gap_y, + gap + w, + gap + h + mid_gap_y + h_m + mid_gap_y + h, + .on, + ); + if (sex.br) canvas.box( + gap + w + mid_gap_x, + gap + h + mid_gap_y + h_m + mid_gap_y, + gap + w + mid_gap_x + w, + gap + h + mid_gap_y + h_m + mid_gap_y + h, + .on, + ); +} diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png new file mode 100644 index 0000000000000000000000000000000000000000..d47d83b743de43e11332c1f080a2a0c9e8f8b167 GIT binary patch literal 559 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E9lJY5_^D(1X7V93b8 zz;I~6SN~jYeGQNTB(Ot@fq~)w|2KyXc^McO8W#LptNV~snh8k-0|SErCs@r*N3I3~ z9+m_D_gl&BnX<@h?8y@ zaxp0KxLo{wzv{MYK!z&!Aey{Odw*-8T4hDX2%mr$TNr zZ%gt+esG9f+Q`Ykz`=a*@AfkGT%R4x+}83Yu6pk$?x@@8ydmM>X|N~ZfWrV9){LCs zu>QaPw;&fN3X#AYR!DTfmE0E8K#Q{t;$UOmBBIPzmk}Oi+}i(5(>hbPo&p)_>FVdQ I&MBb@090ti>i_@% literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png new file mode 100644 index 0000000000000000000000000000000000000000..6366a0ff6030ac12f05c3b87c8e1a3b9469c5b90 GIT binary patch literal 744 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|V1~PZ!6KiaBo%7&0<2 zFdSO&)jyY8Ujw873H*>`U|{(F|IJ}TUIqq+h6VrD>OSO@W8*g(ML;=kQ~jGSvcME>aRFQ0SRKF%SF4Jevut-dZk5|{|u z12emn#f1Zn4;IJHf3y;2%udIxD&{MuJz={l1d2`j3P^Y%A~*LTD|+OvCn5pB)Zj}1 z^PuK1a)J}m|Mksf8fd9)16avhgc9jVO|YcK&3KOeJww`p3pYXO#M9N!Wt~$(69BUs B6fFP% literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png new file mode 100644 index 0000000000000000000000000000000000000000..72b744510b2285d16fc8f85960ac1350758c79f8 GIT binary patch literal 1388 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq~W7)5S5QV$PcbhKvjh z42Kqc_0Q$j*8nL%0y`ub7#RNle{c9J62>3#yt2tPnMfoDg%?zw2m1GbVr;Malp5zXiEKK0pE+#GuZB8uNesZ9xq* zHy?;Zck^Q|ITHc1D_*#BC=WZrklq<0s{){2?u&#!d< zXU?}r=XsRhx&U4E@DBsr|*8bmJKK)Ab>;6l3RoxUC7x8nc1Wa>aa+MeHSfJC$ z(q*SH!QsHpE6a~>`gyXhzR7jLqtpYOVvSwX8Py|LwBp$2BnYmkkSu6=YAyhFyx~zzCK~QT2<7XWxLZ6EY080T=DPyJKtqo^YxeRJt@H=XcFZi z;J8`9_g_$jC3cQX0Sl+2xH`LxqtgY^lic9+ zbjy&d#X!XM;@|gGyE0W42sstmF8{)P{`m2bjSH03rE8ixS!HcNSpWng9v}PLdRqHG zJHMX&iOT7C;u@9|7|o3Sl|T0jW9{Q#x^r@>-HrtyAOHUkiiQRU5$A*dc5h?mSGt)p zbLX=S@^V@e9PEWW7QAR?;+!Qmo1tB{8Gf=>z=t|B$;72p10Gj#Xj@g@oo+CFv-2%_p(v#52ha46(Ph%7h9|X}ogz4!&=uYW< n$c*ONH%uUl-XdHJQ*uCiPNQFMo8J9EP*U-9^>bP0l+XkKc_-Ju literal 0 HcmV?d00001 diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png new file mode 100644 index 0000000000000000000000000000000000000000..ca37ee32883f64d6d02a278c8c2e60c3c6f854e2 GIT binary patch literal 400 zcmeAS@N?(olHy`uVBq!ia0y~yV7S1*z&L?}fq{YH*Mp4(3=E9no-U3d6?5JkFl1z4 zU^ukktA8%Hz6MAE5~z@5U|{(F|IOu#Yz&G5tQY?Oe--&HSLK*Z$`#8srcW;|twAWZ z(N5a_>Hqb&4`*I%59WC0^?j>FNz=xAF?o_TrcU*9)p(L~2%S0k8Oo4efpDeUAw+ukI~619IB1B1Rv zcu>{OZ%-FFT*!>+RSmGH&=Gj^YhP{9xmYm{hJ>{{CtW(dRbQO7$?tKNPUE42$&Crl zpXQs%bA<_<&}L@{sf4)Tupuu414F}te`|XmGD|ZdJOKhVOkh7F)c8JR$FAlrLe1KT cj36}|g!tR4_m-WQ0`e+@r>mdKI;Vst05b=Wr~m)} literal 0 HcmV?d00001 From 4f9d7c565a13a38d21ac1b0f1fe16c79be3fc0d0 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 29 Jun 2025 16:11:55 -0600 Subject: [PATCH 075/114] font/sprite: add explicit underline cursor Resolves #7651 - uses cursor thickness rather than underline thickness. --- src/font/sprite.zig | 1 + src/font/sprite/draw/special.zig | 18 ++++++++++++++++++ src/renderer/generic.zig | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/font/sprite.zig b/src/font/sprite.zig index 4be06a918..cf86fa6dd 100644 --- a/src/font/sprite.zig +++ b/src/font/sprite.zig @@ -32,6 +32,7 @@ pub const Sprite = enum(u32) { cursor_rect, cursor_hollow_rect, cursor_bar, + cursor_underline, test { const testing = std.testing; diff --git a/src/font/sprite/draw/special.zig b/src/font/sprite/draw/special.zig index 3d75360e3..e41cac487 100644 --- a/src/font/sprite/draw/special.zig +++ b/src/font/sprite/draw/special.zig @@ -326,3 +326,21 @@ pub fn cursor_bar( .height = @intCast(height), }, .on); } + +pub fn cursor_underline( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = height; + + canvas.rect(.{ + .x = 0, + .y = @intCast(metrics.underline_position), + .width = @intCast(width), + .height = @intCast(metrics.cursor_thickness), + }, .on); +} diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index fba577231..0e97808af 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -3098,7 +3098,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .block => .cursor_rect, .block_hollow => .cursor_hollow_rect, .bar => .cursor_bar, - .underline => .underline, + .underline => .cursor_underline, .lock => unreachable, }; From e691404a57b242c598a7617acb8ced27a06af6b9 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 29 Jun 2025 16:58:34 -0600 Subject: [PATCH 076/114] prettier format --- src/font/sprite/draw/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/font/sprite/draw/README.md b/src/font/sprite/draw/README.md index c6219b83f..d16035996 100644 --- a/src/font/sprite/draw/README.md +++ b/src/font/sprite/draw/README.md @@ -1,20 +1,24 @@ -# This is a *special* directory. +# This is a _special_ directory. + The files in this directory are imported by `../Face.zig` and scanned for pub functions with names matching a specific format, which are then used to handle drawing specified codepoints. ## IMPORTANT + When you add a new file here, you need to add the corresponding import in `../Face.zig` for its draw functions to be picked up. I tried dynamically listing these files to do this automatically but it was more pain than it was worth. ## `draw*` functions + Any function named `draw` or `draw_` will be used to draw the codepoint or range of codepoints specified in the name. These are hex-encoded values with upper case letters. `draw*` functions are provided with these arguments: + ```zig /// The codepoint being drawn. For single-codepoint draw functions this can /// just be discarded, but it's needed for range draw functions to determine @@ -44,6 +48,7 @@ metrics: font.Metrics, `draw*` functions may only return `DrawFnError!void` (defined in `../Face.zig`). ## `special.zig` + The functions in `special.zig` are not for drawing unicode codepoints, rather their names match the enum tag names in the `Sprite` enum from `src/font/sprite.zig`. They are called with the same arguments as the From 2084d5f256c2260c3ad6dc7d750c46b339585f0c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 29 Jun 2025 21:32:22 -0600 Subject: [PATCH 077/114] font/sprite+renderer: never constrain sprite glyphs This was creating problems with the branch drawing glyphs at some sizes. In the future the whole "foreground modes" thing needs to be reworked, so this is just a stopgap until that gets turned in to something nicer. --- src/font/Glyph.zig | 3 +++ src/font/sprite/Face.zig | 3 ++- src/renderer/generic.zig | 25 ++++++++++++++++--------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/font/Glyph.zig b/src/font/Glyph.zig index 5449e2440..fa29e44fa 100644 --- a/src/font/Glyph.zig +++ b/src/font/Glyph.zig @@ -20,3 +20,6 @@ atlas_y: u32, /// horizontal position to increase drawing position for strings advance_x: f32, + +/// Whether we drew this glyph ourselves with the sprite font. +sprite: bool = false, diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index 25968e865..8c39daef4 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -216,7 +216,7 @@ pub fn renderGlyph( // Write the drawing to the atlas const region = try canvas.writeAtlas(alloc, atlas); - return font.Glyph{ + return .{ .width = region.width, .height = region.height, .offset_x = @as(i32, @intCast(canvas.clip_left)) - @as(i32, @intCast(padding_x)), @@ -224,6 +224,7 @@ pub fn renderGlyph( .atlas_x = region.x, .atlas_y = region.y, .advance_x = @floatFromInt(width), + .sprite = true, }; } diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 0e97808af..810e17686 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -3039,15 +3039,22 @@ pub fn Renderer(comptime GraphicsAPI: type) type { return; } - const mode: shaderpkg.CellText.Mode = switch (fgMode( - render.presentation, - cell_pin, - )) { - .normal => .fg, - .color => .fg_color, - .constrained => .fg_constrained, - .powerline => .fg_powerline, - }; + // We always use fg mode for sprite glyphs, since we know we never + // need to constrain them, and we don't have any color sprites. + // + // Otherwise we defer to `fgMode`. + const mode: shaderpkg.CellText.Mode = + if (render.glyph.sprite) + .fg + else switch (fgMode( + render.presentation, + cell_pin, + )) { + .normal => .fg, + .color => .fg_color, + .constrained => .fg_constrained, + .powerline => .fg_powerline, + }; try self.cells.add(self.alloc, .text, .{ .mode = mode, From 61b7dffcaa1d4668937f2adce16ad2844ddfe7b0 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 11:01:07 -0600 Subject: [PATCH 078/114] deps: update z2d We need to use this version of z2d so that we can get reproducible PNG exports in CI for testing, since previously the PNG export was affected by the CPU arch / features because it depended on vector width. --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- src/build/SharedDeps.zig | 11 +++++------ 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 51e2e4538..68d65fbe9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,8 +20,8 @@ }, .z2d = .{ // vancluever/z2d - .url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", - .hash = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW", + .url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz", + .hash = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg", .lazy = true, }, .zig_objc = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index 1d95ed93a..3099ca823 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -124,10 +124,10 @@ "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=" }, - "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW": { + "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg": { "name": "z2d", - "url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", - "hash": "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U=" + "url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz", + "hash": "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4=" }, "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": { "name": "zf", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index fffc639b4..133284201 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -282,11 +282,11 @@ in }; } { - name = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW"; + name = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg"; path = fetchZigArtifact { name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz"; - hash = "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U="; + url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz"; + hash = "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index d032711e5..bb0a27105 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -31,4 +31,4 @@ https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025c https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz -https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz +https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index ec97a9c9f..f173e4856 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -405,12 +405,11 @@ pub fn add( })) |dep| { step.root_module.addImport("xev", dep.module("xev")); } - if (b.lazyDependency("z2d", .{})) |dep| { - step.root_module.addImport("z2d", b.addModule("z2d", .{ - .root_source_file = dep.path("src/z2d.zig"), - .target = target, - .optimize = optimize, - })); + if (b.lazyDependency("z2d", .{ + .target = target, + .optimize = optimize, + })) |dep| { + step.root_module.addImport("z2d", dep.module("z2d")); } if (b.lazyDependency("ziglyph", .{ .target = target, From 8b6e1fe5b143a81cd4158cddf58d4fd135ad9231 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 11:14:47 -0600 Subject: [PATCH 079/114] font/sprite: update reference PNGs to match new z2d export --- .../testdata/U+1CC00...U+1CCFF-11x21+2.png | Bin 403 -> 402 bytes .../testdata/U+1CC00...U+1CCFF-12x24+3.png | Bin 534 -> 534 bytes .../testdata/U+1CC00...U+1CCFF-18x36+4.png | Bin 1022 -> 1025 bytes .../testdata/U+1CC00...U+1CCFF-9x17+1.png | Bin 316 -> 316 bytes .../testdata/U+1CD00...U+1CDFF-11x21+2.png | Bin 1275 -> 1280 bytes .../testdata/U+1CD00...U+1CDFF-12x24+3.png | Bin 1870 -> 1870 bytes .../testdata/U+1CD00...U+1CDFF-18x36+4.png | Bin 3404 -> 3411 bytes .../testdata/U+1CD00...U+1CDFF-9x17+1.png | Bin 1101 -> 1103 bytes .../testdata/U+1CE00...U+1CEFF-11x21+2.png | Bin 559 -> 562 bytes .../testdata/U+1CE00...U+1CEFF-12x24+3.png | Bin 744 -> 741 bytes .../testdata/U+1CE00...U+1CEFF-18x36+4.png | Bin 1388 -> 1388 bytes .../testdata/U+1CE00...U+1CEFF-9x17+1.png | Bin 400 -> 399 bytes .../testdata/U+1FB00...U+1FBFF-11x21+2.png | Bin 5450 -> 5448 bytes .../testdata/U+1FB00...U+1FBFF-12x24+3.png | Bin 5724 -> 5724 bytes .../testdata/U+1FB00...U+1FBFF-18x36+4.png | Bin 9997 -> 9973 bytes .../testdata/U+1FB00...U+1FBFF-9x17+1.png | Bin 4298 -> 4295 bytes .../testdata/U+2500...U+25FF-11x21+2.png | Bin 2223 -> 2220 bytes .../testdata/U+2500...U+25FF-12x24+3.png | Bin 2638 -> 2635 bytes .../testdata/U+2500...U+25FF-18x36+4.png | Bin 4541 -> 4570 bytes .../testdata/U+2500...U+25FF-9x17+1.png | Bin 1848 -> 1844 bytes .../testdata/U+2800...U+28FF-11x21+2.png | Bin 1022 -> 1022 bytes .../testdata/U+2800...U+28FF-12x24+3.png | Bin 1547 -> 1541 bytes .../testdata/U+2800...U+28FF-18x36+4.png | Bin 2490 -> 2501 bytes .../testdata/U+2800...U+28FF-9x17+1.png | Bin 917 -> 917 bytes .../testdata/U+E000...U+E0FF-11x21+2.png | Bin 1104 -> 1102 bytes .../testdata/U+E000...U+E0FF-12x24+3.png | Bin 1251 -> 1252 bytes .../testdata/U+E000...U+E0FF-18x36+4.png | Bin 2228 -> 2220 bytes .../testdata/U+E000...U+E0FF-9x17+1.png | Bin 895 -> 894 bytes .../testdata/U+F500...U+F5FF-11x21+2.png | Bin 1114 -> 1114 bytes .../testdata/U+F500...U+F5FF-12x24+3.png | Bin 1423 -> 1421 bytes .../testdata/U+F500...U+F5FF-18x36+4.png | Bin 2470 -> 2473 bytes .../testdata/U+F500...U+F5FF-9x17+1.png | Bin 871 -> 872 bytes .../testdata/U+F600...U+F6FF-11x21+2.png | Bin 493 -> 495 bytes .../testdata/U+F600...U+F6FF-12x24+3.png | Bin 636 -> 637 bytes .../testdata/U+F600...U+F6FF-18x36+4.png | Bin 1218 -> 1210 bytes .../testdata/U+F600...U+F6FF-9x17+1.png | Bin 394 -> 393 bytes 36 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png index 6623bd0ff4d42cbfb869a8aaa315e820d8c2d582..581b0bbf0547c2526878bb36c8ecbb8277689956 100644 GIT binary patch literal 402 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E8so-U3d6?5JkFl1z4 zU^ukktA8$+y#`1D64)WZz`*eT|C`Gjxf&dJm@oYQ|H^8ItX7NP!S4JZH3yXwB zB({RAgn))KtY8}sGjcL8Ff=Uqzy7x%7s%mApg<7f3WS>5f*NQ}J>UveW5^3O=il13 z4;iJIkW67o-U3d6?5JkFl1z4 zU^ukktA8%Hz6MAE64)Wdz`*eT|C`H>Tnz>S%m@DO-zB$4RP=C@z#PVdqT5y4tL{Z- z+t%2`>;2cfyOw8d!G>tbsD}yLjkX**n3wt@F!9Icw@EIKpR~MY diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png index 8e54879661a7e7ff268d9ee5819a4dd253c98633..852fc999be5a707fc47bbad639495a07ed7b56b8 100644 GIT binary patch delta 268 zcmbQnGL2<|c|E7U1_LViA<4kN@c;jt%Ykf%10-55{$2m;+rs|9V8LH2i#5J>8@fGW z2WvP`e9kic?*0Gvwm#J7sZ2I64%zdhW@!@$6B kK%xG%^qd9TSRncuL<9PN+m_uC7X39GC-(U4ONKP(eQjUJW&A{;g|C_^x zybKHs4GaFQt>tBuW~Xk6yh$Z|_*fj|D3O6dJd2s9XrqnBWl2B9x&e;-T_K6g6aSO delta 395 zcmZqV_{Tm$Mg6F!i(^Q|oHqvy85tND4lVfVpUbVU!GH>ONKV#cQVurYVqp0H|IOh> z9tH-60}A!8m17ocV?kB=fEBFfiX&HZ0K<`h_w_H!0$=#82w~&Ye{gJli{c9I1BH9+ zVyd20KW?91`ETM$qltIft(hM<&Z*!1ecqau=k`mh{nI#9E`(@IaEN9R%Fq(=SfIzopr0PwMOQ2+n{ diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png index 630d26dbfaf79d560f364845536935b414b94378..ecdb2ce10121405cfa22222a848c0ce9c99f775b 100644 GIT binary patch delta 174 zcmdnPw1;Vee&7KG0p@`J|Cj5QOb(jBd+8j6ZO&*VLQ&nXz*y_lB^(q1_p-z|KA)o6Fmf_X>`KscX8>{E9&Y4eU|=|)Q2$z6X2CWV28IOA*WGm!*5|U6 Pf^>Ph`njxgN@xNAX469e diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png index d9b2aef99d9f2940399c7b29f5191bfef318ed62..8d7de36aceef89fbd945cf8a56477f1936953b56 100644 GIT binary patch delta 1144 zcmV-;1c&?k34jWaB!9$d<#gyH@DpC&dy33l=Q+h2YQ{JL4?` z3l_XK-s0zi0RRBFO7GWWefCse^1msU7A#nBGtZ9AyF%lR#ea48-ym49;Pb&dOK%6C zxafCQSh(p^9~UfGaPD9L008p)0{{U3|LoU64#FT1K+*f3x-Od#K*n|q)4rd$m@3HY zjvGGnYwB_KkhLWb4GauC-(fg5dxgd;t)8zpcAo(Q0|OTsufM@!FAEqL_z7T1k|ase z+w0vFS1*$*Yk%{0{%tKVFmQ2jZyt`#Kh4Kbt0!0`eOUnv3|tl*G63HO{$Ierz_r17 zHS|r!#5NHEr?rU?Sdt`3NgZdyZ%5CXwV@x6&k{aWi~tP0Gx!4l0RR8&*1>XxAPhj! z|Nm;cbjk=M3O;bo-9-n?X*UlzM(7!WW2(!gn_EUtM1Ph91_lOB8g7j5|5rNA1`ioW zwg?FrcvOp!I^ae200ssIj)5gfk~FpI&#$W&=dEDa3Y0JjgPDsD-QPcA43 z?GYFl_=Dg#KK!fij{HmSF#@l#7BKK2@aM->_5cP3z6q8jNz$y2v+CR0vmMWFxil~^ za1@-XxPK)zKDnSEv`66o{Q&>~|NrdP-3r1m41iJJ|E1^UAUJ6of12!kcR^Zm6r{kw z)xqt@)VEb}1m0sUVBlqNu(uy~*#j6Dco8f~lB92SoINfcV)4_fw_1&}z!jDT1|9}a z6^>1fnG0V2NP{CVFfj0az$F9j9&q%fvENm@|p(3P%TssAr{zPWhVtGxO4S_c6b z7=QR|a8t#lTeiF%v$dYbeE|al#{{oiYss5O#5|7@qY(+-2e;>oL9FQ^B?Pd128af1}sUEqz#Q8 zT~FU-9nZF08W;$&he-z`(%K*rQ5t{?D`5c*-8Y zz`%=ONs=V(YV_!O`Y!8uw&l{mz`)_*`*Hq6xX09(>3Rz=Fz|=KnWfX6@h;M00960?AAFFgD?z0(fi*zRZ=jdE<4z%_XTRTA|%38Cz8k5tlM2=&Z-3Sz5e1x^=~hjwLQf@}l$> zS>tTxvYBC`{SGKnSM>> z4KTw)PJ1Hf8?d+#f-lo7S9q zOXqfmeSjBBjLA!Cqa(YPE5U(IlHX(|DGIZOH9A}iLwcUC0utlvsmLBcmM4@o|p#3h8a>ka`a65ynrh`LB{u+jChlxji|Y(O0*Jc_Nui zCS%G4>u>PK5gq6`#eMHEBIKex-ekUZyZ`_}a7ZSiGHciyi)zi?f0j>v@uefTyct0P zvHj=qO7E8S5hI6&U?K?eZ1jq9e5uzRpDMT~OMhTwK{wIwW`7gJN07{752omJYvb>Q z&+@x;8t*Cyf+~vR61sU!iWB7c(tQ)UvEKNQt~DF*vEW~r!AzSq_W26~D5VE;C zMo?{D{7D)Kf@oI#{hOwTyilu>!yf!eW16ra651zyyEP}dH(R-L>p6Apb#|5E!GUO? zw=NTcpzC0?hcIpUm^s7JfY!(;*ox>}7hOa<=7_LhRw zU-iB)jJ?Zu)ud`noN+>0B>sOJe68e$Htfyy6qGjd8#-&Z{jEQeS~&Pc@?aQi=o$MC zXG#455WH%bb-=`{$<2S0RO=a|hM$njw{`m4S;7|I=}kw2NQVv9TbRy+5LBUXSs^Xr z^a4SGL6~e1{uSRCsh;M1Jk)V8Wh?7B;l_!9pq~{n>PvhCNg<7UeGjefPPD2EIWu`s zitUsX39j@Y9V7NpDx*kT#>`v#gOyu(O0Wa~VBp97wzl>2pYz3Hu{diwi2@uRzJwr| VDE%q5K38J6U`Bix-W~=Q{|6!q5+ncs diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png index 0a616d72cc695dac524733880abe56fa572797a1..ab6bec96d48174c49b4822c5bfe87c766207399a 100644 GIT binary patch literal 1870 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 diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png index ecbb650a1c0b08f8038ff65ecd1a70d91f5b8e37..43035aefbcdfade74b5a321ad0330850f531af8f 100644 GIT binary patch literal 3411 zcmb7G3p7-F7e6y|X2wi23^Nig4YwG|W2EE}XFQ5x8uY0~A(XjYN|{6?Gw9(_Bi$sg z()G^6t+MAQy64vbe!W)sXq@5=iZ&=2!SpOM9EwMDAj>Qk^QX7|JyjG2&zQs=B2UyhX^7uFNi5&<0os#aQ2 zaGLt&wEBCW$k!cATORnRiY9>Ek+=)TX;TgxJV$nEShyKGd4ejO-m9EM2x(MFaZ`4N z6ViV+Kpt0KpkhHH=Ibnz4P9p7kqi<_Cfk2J_WQ|`bS;0?i0iyy{q0{7H4Ya}SXSFo z2ew-t!Ir)2)atzGd6ebNe8~v;_C;C)wtq&yd#&-!3udNi`UGw)Zy9Gco~LL^LZ#%I z3K1B8c$|!}s(}dRF@2JYtNKT}oAuz}#v!!NNc<7nRWnZ^gMkT_9n(z+M< z$m?;;A!9P@p8r%*SVrs})l%QA7VtvxrMbRc70(3G0e&P`uA{@DmQp61etJ4Eb2f8( z%VKh*ci47YPwXtt{iXWG)3y#@ua5)oO%~DYlFWR1ZLMT`c+NV=x1zV{M+<3{ZRy~E zgG|n9s)&ekC)7>7D%ZUGK#f$H^VMmyAcYF*4EC-Cbq1d;2X&f4iHuXvFboo(G^x$d zFD*-1hYD5F7#3@3m7Z({omQ-H3p&butf5DyqzcTlUyA5IUUFqQ+P8DVH~n^Fj~PQ5 z=hE^2CEH}@aZ;t;o&uZr{sL;G!V@>fC(`9i{#zY$3v{ys=K%ivDPcEdh0>J=2F}{{ zVVE zRF%h!PkwrPL?>t}|1EqkOB7zm2(io?j*sg)yzvDNTlOyb9$9gJi+YIucbym4bTQW# z8U@|W4rO__=E0>Sac#_d$WfU{pN!VZr8zSyDEaX35ZZu~&ZpefD1_~d27|B}(OE0h zCNc5@OUK7T1oZp`@uPCm4!7siUGD3~&e1T5YN*B}USA_L?hi-MS(VFZK8Y3v*T@u< zkumQZy~uZTxerE0upMu$KB}$sa*^W_q9dJSI78q)u4<+GV$2;?ul523ONF%*ivM!qK${NMWkuwq-O$m;* zGr3k`O!6V!c#gB`XqCgptq=~rHbE}dLsWvRfe2u&v?3gh z2@sZ=>}w~}o}YQwS$~B0#RXo^>jM+XH0B@1h(r1`xn)+D6|GFGQlR*Pi6+QKHC|s5 zH0}g%St|e4r;+knXV4YvO-c1q*@w3Xo?5*Z6GU_S;50GwbHL-qp;>ilw0~Q%b~lx+ zbbwThwK)AWPSf#YE%E2iuQ8%yp>doI_ZfF{>UUb?uDn+FIW1>p3C)=yvPT=xG|>h$ zb|DqLH;;;TS`xtbP|_FA*3F&ZJ2DyrYj7%P)ii{@Gi94ACX4FMqGxcE8gf18>Y;zc zFT-l`8;Fia(FtIUtlH{*Si_E0*g&(6%|p0@ktf!whw{(xjdKX=YXf(Z7WdQgbd&`-61r z?aJ1q_U3kFG>vT?b`AXUOSqodtfw_uU3xA13_;eJ^%gDXuO+1sq98@HrQx8n6kZ2g zyZ-+|=mfM#TExUF z&~eXuZnb58j9p2O>TZ~C{!gx17lzDF0%0E%_h*)VRqJ8P@eYZU;}9bNgG3sG5-9sp483 zs6%oT&6f5M0yhOKy_)u8)=`|aUSJns)l5k5muOoGABk;(pZ$HtVj2B)1Y6Ay_c<9e zbLE3L-Fs;*#!Og~#3OKj;t|Xx-l$(Kh4Qv8Dk0h<2o1uyUrr)ug|-9mz*CRF-Bp>s zy$tQi!JSISf}7vy3GOVv5{xvMQGWD4n4cuE1{o0Y4dav>l{XI8>j`QignP)&el~-* z5pss4sTPYnwied_k?JL*62q~T!TXYz1~zRmReTyYmrQ@SEZ{NAr?TX%jj%`DA^B)m zePSTASQ}j%MFB_MNtK%zV}CkWqrv#q*&8yr@sImC?q~xJkLJu+;x6E8w+(0gOTed? zZ>HqK3DInBRJ(QajS(_(8PAoiVTMSC^o)oZz(#7?#gneU_X3wK4tlZ5P5D=K$TbRq z?d<}bU%c!5d?k|&iA!(|rlk4#D;yn=j2vDaV>0(!Nre5Abni89J?0|&TZ+{W8?0y0 zg?%2Oe1Ev{>T9qOO4?G3OXMw0=aVly2t^pE1tN%mzi-`~+?{0J=04UU$%5ENl z?mzF)^SqrdA!3RpM9d<8^HL+0SEYVF8Hk0Y#l^h{P2?iNdAR}3e@(M7&ySMKcQ?g1 Yfq)`SS=TGv*?K2{kY46AXH>{4k(*OVf literal 3404 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 B*17-y diff --git a/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png b/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png index 1a6cc75fa6d12fd7fe88e1caa1bfc6382b39a733..fc111e2d77136c5735c7665615301ff09dd5f436 100644 GIT binary patch delta 1021 zcmV@s7?rm41p^Ds}dsgBB3cEJ(g96K#0%` zApih?Qpyhik?SIV3u0b%!kaIcx%ae_GYBD+Eo9Ph7|!;Ch;U(Cd$JWFgc60srU;dR z5JF2cn3aT^K?tEJ0001#;=e7;>f~`6Z}X2A8>mGQLMT}%Xc`9*Th8qxYz^ulgwRSt zV$(Ar;rmW_KnQs9flwi?Z&y{^hy_gt~{lg*FHcDj@V{LI42Jmp1?a0RR8&l|c>yArJ)p|EKZ9 zVIsuIXqE!15)Zv=Ctd7;8-A@belvVP?x4phL?8EOp(LT443%COIu8mdv{dMTP&x&2 zP>3GR6AEg72L!+bg)S2k5fNEQ>%2x+RD3hMz-S$X6j~{iB>X=aD!tFc{vH%kNTCHn zXcVGzrqJ}khCv~EI8I1JLC3F5Cs4Kt8<gb>;+KG|aNX=Y6S*28gmw#6%gzuvXPZ6;C-r`W5IP|= z^XiLHA*)bBPsahF?+cY%1Vi{w5OP2$hY$b&z$wf`a?C5`Gt%V{QZF|z)DHt&+pju9 zg!(W-2;~+UeWO`a34{>Z%)C(e%M}d*6%a}$1OV`1{*zh*B7a`Mw2>V9N_j@Q975{l z@g35_|vv0RR8( r!!Zp2004lX{;dt+3mXCe00016;B=jV40`aj00000NkvXXu0mjfBHO&G delta 1019 zcmVR|t;rnrKl%gnmINcxfh#fe@k2 zyiN!J008%M{H8m8aL&AE>T-l;FW0iCNP$qC5DXbwdl4cuLP#i6E-Tb!a9`cnGPz!N z5hB!C2mk;8&*k_Fa@}~;o~g?bn!Q{~Xt%K*iW7n%L!ipJDj`BI5*iZHW0`dbgb0lg z0ssIw;}4PTB7c1`uR7t)7tGvy+Q}J&5Xu%Z={O8$`$0swFs?n>iV#AHLSj>d%0LLA zr5Vgh!p$IrP!s?F07~)SmS%PGIE}aY$BPZrq6i_BEEF`2gNQBX_7S!Qbr3>mB_Xlt znUL^(Cp;i@B_M=QworWqgb-Rf0001#bNtU_R=*3c4}T435JD(h$R83JhQ;v9+)&gH z2q9E1^w33F_gw#S-2y`0!`?z0ga#E5`ZFN_0N98(00030|Lm194gxU@1o{7`LzTk9 z5tc#cwKF48>>VrZt&RW2eW2Wt9*2I@O; z!w4aiTWIu+W>F;<EGdLg6o0Gze5cD47rd0L1x|TmvG1@=P1av9FY8q{|_sUM??W zhmE7puR247aTp}2AbTVxu$G$o^ z6S;bvLkOWxAv87=+Y?0N~Ah00030|Lwy8 p2><{9fS~`c+Yu-z00000`~~22oq_1@evkkF002ovPDHLkV1mW(wXy&J diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png index d47d83b743de43e11332c1f080a2a0c9e8f8b167..ed0e8381614585f5bbb2eac395badc97154b2740 100644 GIT binary patch literal 562 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E7vJzX3_D(1X7V93b8 zz;I~6SN~itdkv5RB(Ot*fq~)w|2Kylc^DWN4k*;W)|OeYjRi>s0|P?>7g)_rN3IqF z9@Y!L=UZ)_;j5|Dvf#%@TY;V#llbo*Wd`f`kQ!S0@#FuZAO0EnPcAT-S1GrP?YPLj z`t7_er%rO-*3F%&%*0(2=jSKzW90#mjy-{ob{)xEc~RPO%Yoi|ClI?nj8 z&|>Lv+eDDL?l%*hEbUB3Mj-O*Yw7q4b z^YcQrW16-c!geCdzwA&41;T=p|2}Qm&#!vUZJJulj9wkz9R}PsFXN}~%dOaZDJaJg zZ1JD|5By+9URub@z`((L@b7lFM+di@oO}4A=jGR2#&-~P$aImTWAUI@TU;`T@ zxEVPa7#JEB{9pfDkP8%xNFav=qQsCFtmNO?wGSDknUEAPFfc?jg4Dc47$ZHY38sXZ XQTxAXnkq|03P`1=tDnm{r-UW|F|^Ys literal 559 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E9lJY5_^D(1X7V93b8 zz;I~6SN~jYeGQNTB(Ot@fq~)w|2KyXc^McO8W#LptNV~snh8k-0|SErCs@r*N3I3~ z9+m_D_gl&BnX<@h?8y@ zaxp0KxLo{wzv{MYK!z&!Aey{Odw*-8T4hDX2%mr$TNr zZ%gt+esG9f+Q`Ykz`=a*@AfkGT%R4x+}83Yu6pk$?x@@8ydmM>X|N~ZfWrV9){LCs zu>QaPw;&fN3X#AYR!DTfmE0E8K#Q{t;$UOmBBIPzmk}Oi+}i(5(>hbPo&p)_>FVdQ I&MBb@090ti>i_@% diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png index 6366a0ff6030ac12f05c3b87c8e1a3b9469c5b90..bfc7882152eff8b889fff1fbf733c0dd89746c07 100644 GIT binary patch literal 741 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|V0`U|{(F|IJ}TUIqq+h6VrDo_)YC&4i?afq|ib1x-yYFC)4d z14gi#%NN;>IEXM``2YXaZvNjP$t{K^uVki`hPrI21DpB5z~hMrEN#72^g#4=c4gVlsoUyw(x+$hh)8G{1SBjt-~iTq;2J;Jl3R{k zOojq37k}Ts#jH^_gY${-J!bR08eL71uKSI%>djX^eZqCMsHio|a;MYQIWY^P{MH5N zviE)XKE1J#jZI1-BVj>flcK9)i(0clTvi4Kh8+h^1)n+f z`NUde6V$_3Oq;A7a=KCZo89I;{=ff4%!}P-c;SEpGczxbiGjeMjjx$^ex0PfrR*mp zP;VaOJZvD~a`E48KSs_q9jyO^-pf8W;OVZN#GE?m&Vr~((jneO%~_nFAl@={`HZR0 zCq`EnHD}rGblf`c&Vnfaynwap<}0o-ufOs8z^6TLf2aS{zj>^YjnAZDK_fG-3`gCq zubTH}Jz=|wWSVDQfG#M){XRir1QD~o582UUZ9NgO?~kr#?^_0RbHt%)7(sq%Snz+n m^rR-#M0CIhtmG|LC39HLvA<^sljH`U|{(F|IJ}TUIqq+h6VrD>OSO@W8*g(ML;=kQ~jGSvcME>aRFQ0SRKF%SF4Jevut-dZk5|{|u z12emn#f1Zn4;IJHf3y;2%udIxD&{MuJz={l1d2`j3P^Y%A~*LTD|+OvCn5pB)Zj}1 z^PuK1a)J}m|Mksf8fd9)16avhgc9jVO|YcK&3KOeJww`p3pYXO#M9N!Wt~$(69BUs B6fFP% diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png index 72b744510b2285d16fc8f85960ac1350758c79f8..2dae7ab4a259d960f5fb5ba98df06d39145915c1 100644 GIT binary patch literal 1388 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq~W7)5S5QV$PcbhKvjh z42Kqc_0Q$>*8nL%0y`ub7#RNle{;Bzhk=3NfI|IiZJ7nzSddgOFfcsehNv;*Wnf@v zSnzLcEia=q6RH{mMu-|lPOzH)>!l|(p(&Zef~MrQpaz+!}56pFYa%Fg#_z^9tm+24y?@?Q%cw@A$B3 zPP*kzr>%2h7DoB43(!>$Uoma6cF5_IqN|=h;ksH>)S3m7mU-ZM?f$!alUE-2>Yw@k zqy)2MhRFp7wppnP%)M+<9j*P-_A=`f{VPkKex>>O{7Y|ylqWb`WM>u9@K$JC#LuM?FwKFyLl>H2v>?PO-+W>8$DzELw45a}oqsR74guxh{B=dXRHkwgT9R@8lryam$d4 z$x(#m;_v&n&bWw`{H#gu4o^tpw)K7J@|c-{fx+kVQRlY)`Fr;NcqrO3uiAE}4UW`b%%7@HTNo>8f-r zh~-eq*g8R>sZ6nB!BwXw4km*W*JM?8R+j(&&mq(#H%F#`h0~GWkI$khCGr9!S>6K0 zF@wv+-}iUzR`PIAZTV4~e^ax^`9i1)-wWrHZ2by_4*Gtu5Wlvgp#I^P>Hqo7_3U3% zVsja~!x_zt>dK$1t>!u|zjWuxi_kc5)&-i%Bl-< W8vT0T@TA@bxysYk&t;ucLK6VA$lsX& literal 1388 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq~W7)5S5QV$PcbhKvjh z42Kqc_0Q$j*8nL%0y`ub7#RNle{c9J62>3#yt2tPnMfoDg%?zw2m1GbVr;Malp5zXiEKK0pE+#GuZB8uNesZ9xq* zHy?;Zck^Q|ITHc1D_*#BC=WZrklq<0s{){2?u&#!d< zXU?}r=XsRhx&U4E@DBsr|*8bmJKK)Ab>;6l3RoxUC7x8nc1Wa>aa+MeHSfJC$ z(q*SH!QsHpE6a~>`gyXhzR7jLqtpYOVvSwX8Py|LwBp$2BnYmkkSu6=YAyhFyx~zzCK~QT2<7XWxLZ6EY080T=DPyJKtqo^YxeRJt@H=XcFZi z;J8`9_g_$jC3cQX0Sl+2xH`LxqtgY^lic9+ zbjy&d#X!XM;@|gGyE0W42sstmF8{)P{`m2bjSH03rE8ixS!HcNSpWng9v}PLdRqHG zJHMX&iOT7C;u@9|7|o3Sl|T0jW9{Q#x^r@>-HrtyAOHUkiiQRU5$A*dc5h?mSGt)p zbLX=S@^V@e9PEWW7QAR?;+!Qmo1tB{8Gf=>z=t|B$;72p10Gj#Xj@g@oo+CFv-2%_p(v#52ha46(Ph%7h9|X}ogz4!&=uYW< n$c*ONH%uUl-XdHJQ*uCiPNQFMo8J9EP*U-9^>bP0l+XkKc_-Ju diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png index ca37ee32883f64d6d02a278c8c2e60c3c6f854e2..cf8d5afc7e5294fbbe63568eb4fbcfdba549ca52 100644 GIT binary patch delta 309 zcmbQh+|N8gr9RBl#WAE}&YJ^H(hLj?|NpT3G4M7G1%m@Dezoi}$w6bt6BfqUq&Vg?N_q~c$mrl=)_q%vP;c=Eu z;~_^^<@gDA+MhWey8O&$k>Q+lGk_-$C|NpdE z1XF8sm)k#uo&0Cpn?zZn_Ag^#&^HMWs@nPO=^}>br>mdKI;Vst0N*uup8x;= diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png index 58afe3de7ab7579b86fd68a5e0dfff03db55dd65..ecf6eae26b9a2cfcd2d027d2c15f9373eb6c0d9e 100644 GIT binary patch delta 5419 zcmXY!byU<%7sp}g?k*8nnng;wJ49fm7g!`kkdTK`ek>^=4ND_PBPk8iAt513gA0p< z)Y1*_@p<0QA2VlW?mctvxpThfJ0k*L1roUd3LSM7sDIAxTn~AuC}{|M<-YjQVWEX8 z3~%1F>)y1!w^MKttmD?GBp2A!B~ahKUW`B>h5*J35zmSZ$@0T75@xj*xcK<^_#z}W zbzL8E%qUs{yOX(fu8aDhsC>*^Q8v-8oTx1Encos)2p|-jmTvH#ojju?hUQDoH0l6>%)mnF2;Jh z#1?;NKXQ%m){e1Kg%g`(0KK$|2n=%#8m*+5i_R_){WtXBb`l&;sY=Kh`a5^ldUYAL z80==IvPvOouo~W2`2iD^RpQyZAZ%2VMLXLOa%3Any5t}Pfh?6p##EAUA-z1!7^7XgiI2-^XDs z9MGSw%@cnxIe#g_6{7pQkL`P5LVgwc)Ux05*~i5j33As*SH_B!YA@G)=`fdOBwDs3 ztid~9*LSWFMinn!gjUpc60plw<`hGA?nFQ@uI$ozi^J9(d#p` zr`o%{&fbqh$D$b72VcYVW_4^V`kOL6AEM9g@3sB?)X2|6zGr6idX`0#o5j-bUu$O;c^!hczFQZw+x>QJ z6GsE631Dq2?zk!ZT-!g;Dpvy!3lbQok3551;~?kf`%drTZ#Zfo##xaapL;iL>6}Xl zNCEIeEdTW9AN2Z)_TLT@UkIWPB10OlP?yiO^{}#`k1DLi*OL&UAed#wG&&sOGOpHb zsf7?Fb0zBLJd2ri+EzIsD{VUoVSm=khMr&|cR5N-2Qk`9pl+2APE?9dHKEAy2RA*0 z2aLeojfJ*ew+J%^{E8{OW|BuNx{tL4pmFjszsD&d(Va?65784sQQbv{59MX4BTGJN zp8G0KFHK4?_Jt;QtIoJodp4z zOp#WGv7P-4on{k(#&+;YW_#?3Ii~uS6f_~C{Ohcd!p@|cE(pI$gc-ABTR{oXpShY| zGXMBYZpP!n_9OAmBv#bRa^Ec)6Gh)353pmF%xrR>Tp2xEbF0bY`O*{xpZw#TLLh&B zAxL$GDDR)OP>?HVcT?Gs&Xz|pG0^^@{C5|oFe)pIUzIH$7V`KS0$!T@Kecov2_ml3 zKVD*rTgBe)dqIKC>)D06wLI$CUQ2uT(Dfhn;8k;G`{*6d#uiz;GcRjrGPf-X3#8`< zf=`CtXNr}l0>)#|^(=lxJlEal8f+u%TEwszYjJ~`4^QEr$|M%h+QKGDZcukUU8BJm z%X)=>!pz6sMZ3td0HCY$er5y_0RfC>pIDdXr+~y z+e1EhCV+ZhHoc752H0djy`|eyk}c`*gU86 zWGwHw>Qjh_Wnnn{t&!63dkV~`vF3(-M9(Jy6;UJXe zqO4v3yT7I7akI~VvAW{20Rz72R7bBC+__O?h#c0c1!@%bX^y2feUDi&oW`ljE$to7 zAdY6cZxBiw9O22;z+B7=k53 z03)9=l2P(o*(rXq5Nx850$JJf2YBNZeXy&{tUi*;(C5o4m#Ixn2!^$v!Qk?}Vk-t5 zjUAq63Ac}yA=7hZrYR^?Ff{A?6YY0SF7{}rDjvPPv(_8az;G?$*IjYj9Z3P^U4rMM zcU1(eSr^pM18WM56L8&#N_k~}e@KlU0!s}9Gff9`9=8NTLne4QvwTkeNVndnuFy(n z430aiCG?BSg|?kOE_@}A8KVkq#jQ!l&^YTleUmn4|N6jeAK6++;@5BML5Fn>j_3Tk zW+KOxZo(+{<{bmrW(o4V2`J~6dzj;|DheBoRHB;RHFh)#1ro@6S1M`^iOxqe27?a; z2Vf#`=4^Je{W~HzW2VYLzjysR*Jo?pGU#M=zc}lq-{X?1qQa|>?#7PvqM&*i|CGl? zj0&Xb78{pCE8ukZ%b3_z(vLf8eW9QVIR8dvOKJBBq)ozI8QxCw1#hZL=Mo2;TdypcIs122eKRkeV%()8( z1y+(aK8X*)8U)d{Bqt1iGA@Vyz57bkckO6z?2|GdyEXPu_h_tdeIe_1yGb$#&_FeL zkX`)$+2(#pS`?*>NP?A7Gs-#;@gWM-fb@{31xrMFa3T>E9&4f#Mij`O;=x3Nbg07t zJ~IL&bG>{-(EaTV6cvSdTrZEXMbNbnVG5qw;RR!(AQzA8-|*U*T&p1Ni7zPcV?j#P z#5XlKCVZYt`nfIy3%T)*?09#dk1_pE?eHRJSYUn3|K~nv@~c}p*&6VVmiuLGs%E@i z9Zy7akA5qS(}`xzlpnau<0#i+Y~(=EgJ(W7MzWX*vpntS(b`^05h!!C0}h%Bx46DmnInOpOquvrJquv;vW>@tGK-DzU@daPX?sS*s zhkir3ViJ`3T1Y6KX4gJ7t1XvPnrpwv>_Z)7$nuaLR`xJX#5FK^CFwWq7`)8mva_E^ z_w|`DuPeZ9x3xgra_qPEBu_%`#Zce9dj1O~Wz*S?mmBC9oyexwMG1^H zU9P~lr`&Jq7Ugk7=Y51hrC>iW>g^p3qPz%-sX^3r2k4N|=NLvgbq{D{+kRKv|6m4_ z&BkE}bjZfB0t33shPa9n7(S0t&uJ-_Nm#Fs>yz&NkJ5#D-IoSI#nH_trriT!NKG5N zN!VVy6t>xl_be}}fIEg_uQd1jYd&_%!#QTsHv3<>cr8~C)p@&-%t@@xbfU-%5>kBF zr9EED6?30WNECBYY73KSdJCT@<7+e}_l55cUivnbfkmD@+FKUWlSHo{5)c@Nzx-*x zL~J09kj5^HeQ0ownK5131v{LyZMbvi9xJ;VHI+*8p!{be{y0OOOLQ!0o2$+G7diXCp)Z@Ae^ZE+Qt{j2dBtf zPZdCW_wXtXp1#7Wck0bsx*v5RY9Hn1g~}j_@YUoG2h)=?YZU{igr5Vy9h7<6tuZ`o zt4|iI2f|N>V_ld8d2s~h9Ygacu^?^a>fqfBc`ZqEekNlb5r!MY;CYc-g=260un#mW zEtxZIU92Z)W!tw9D&ZvtFI{K+3HPd3j(~|=&pPxjh>eTI_DVriyNN* zyDy69I)$~+Qb?D+JP{Z@_}(8j_&4FZaC<*|Jq0cK_d`JR|YlDwv#P58uKZQt^kO0o<0&PGPZXQ-6q-9 zSMn)+S3#U({79(qyS;m8&v-*dyEoLruH}5a{D*nAU9Gq|7`RLtwi~T=i%c zbDkN}Yf}XLcV|TK1JQ>^`At%P+`^E=os#M`bVR`g=MN5YZ3u}KrvWn94%M6D4XK$! zUF}+mobpPo+Mr^eGPK|oxXnlF&(eFBGA&QOx*9H8dH1xkeBguQi(My6enIB2UJ!k+#k#e+!SQ#B8n1MhqHpQPKZhdTH z10*L-cBy+-VKKRVFGS0GjaFMVdpyY!cnw}erdc6~+Fu*0*l$jlD|wKCJ1ClI+1EJ7 z`gdpSE{cAd$hB=^Twi#*iM~nqC{OU%=}WN+>oL^pEHuKTWx@Y(6TJwSGkY!IueH2u z3I|YwT+r9l@)8p5&lp>#Wyg``B0>j2%dWTh|9+5QqwYJRlu&{{sB&C747orLPYW&* zMQyE2E}+75fdWB8DEJW=nJBUoPUI3yOx455DFmy&VSU_%m4zzQ;s)zqZg~0)g@D6X z{s#nvk*E=@9!hJb2aKVDIJkhCr(=j6VKpfwX-^ty0F7B31PH@ zvo%W`*G{o-wRn71cYqv6bt(^LAs{+bVal4XD}_B%jFHOft+qw3YF-SfH%TJ)m-pj^ z=+3vvzuLTP4nGV9L({uimn~<<*T-Z;# z&YK-;8a20NLMm0l>G@UN4TkR5>yjr_UP|c$6oO>b8=B9bcnF{87Piyu>}?B9N*797 zS_w2SHN2$g9E{v8PbuzR)ugZNyouL2kzxoGd!J8t9~g2Gyab#)(QTD(L!{~S*PR`oQTivg zxIzrH`MEbati$t6M!cg57T%rL6XTq>x7OBjF159D=VWpu<9Z)cHBEc7s;fI02K5C@ z-4yg%bQaZ!F=-x$mx!gVFnp5Ox3*oM*E=MrCA_Kizg&9%E&LQC!gyVO%1Ufs+;5XX$XK-5^-W98x2-s*xRn^(E=cHYnrG4!gfX-V z-sb1-2e~fr`!wt^v-2srBH)-LQhD!&ZSYovSm5ksZ-Qq{(&@>O)D><5YX%@Sdsa~a zJ~h?cSed@EqIn4mo5->XGC`A@zM4+FI4=Hpv-OfblX~XPNW|*#s||1X|YX zwu@kwT-Y{T*pp)nIj{5PtN>Z&dr}%Xysnp#w?P?m{1=WvbAYZ@WEx5EJpUd*zO&Pg z+qEkrq#(Q<#c!y`(qveRX(-}IKWfM;dc&W2T;E{bf8h5kb zm}W?g7Z4h7&>W2-8jnfmjrTXabIBYYi(&;!3vy6*YF*D|Q%t=*)KqGUQdGszdIe{a z^Sv6Z{Oi}5ZV3Kr?_S>{GAt~tkPmlN?yGn#p#P*a0gFU9g+5Ggr19@sN8^clwW?j{ F{{VArY|{V$ delta 5421 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 p3;+Nif_PC~y+n2u2mk47>fV^ delta 5516 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 Xn7M|6v_=2?WP)zpyshyLV;lNEGRz~% diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png index d4e488525135da1459ef46642d2ccb21bb9d1018..e1a9e7841def9008a0c8cc31a8f5b5936e76ea1c 100644 GIT binary patch literal 9973 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 MsOTtHDOv{q2N{SX!2kdN literal 9997 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 F`(L$nMXLY+ diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png index 9be9501ba18ef61d9715e67027bf5b9ef23e861e..3fa06a8fc969f3641e2f6d2a02871c11b4bf20ff 100644 GIT binary patch delta 2951 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 x1XfBWLjjc%A%qY@2qAZFy7x{ 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 P#UXOUy4Y1=0?7XdBHz?M delta 2048 zcmX|>dpOez7{|A{Z8o>%mS&cbl*@|9T|#b&bWCe%>Qoe(Tjp~5F~e~QOS-w$LF&w% zxn#+<}O=`A`H28u=A+%obNyH_j#Z9ectE!KA-2UP^wnS(Lr5xbv$%}Qt@!j++G1~JAk1fWd#e?qjNbF&o%$QO} zPG1f7z%^B+c$9NM?EvKh?QlF3V?Z_1W{tRSLA}hYTN6{yD^zK4%puBCKPMnJLaIiwUM$Wk-HC!1zP%@7C$!y zJuWihH!L$#p;*+WascfJzcN8Y<;=5tg}#8%C#y&0vGc!fOvQ;=O*13=M|$fkIQpqp zZ@m`A>o|awJ^;9P0S)>>mr-9(07+p=Yf^#8>WNl?S516HB6@Hk*(sZJ-vrn z+3(OVS8^||KC;XFGzDR+z?NzUMZv#|nBQVxnZnw@meazLP24TUZ$V`>z|5m3ly9uY zeG8fjS~R855RCxZsian#vAueL$*OrW7z6^5WaplxKFq6|N;gZ9y6lfBkl;WsN?!KI z00mrRKyAhYo%}ddwswD-6^&rRkniI;i7uAwnk~-I@;cYw9p>rQg>Zi{(dzSDnB(M? zN4`(fTfcG5plO?LPwuQt>xihqk5*X%#b%Q09p#`uk$`!`TN9UV5>m7?aU7xB=>S@^ z@-^^}Ant~2l;99u(JX^}(h{_#5*VSFS1maC}{?0Q^1Btj;~H z{bRy};nH@qL)%V%w9xS&+K@k(<&x_ptjL`L1B)7o%SoC8pTb*+Mz z<$ouq!zFJadA3FJ<<0h_1u!c;9z8wAZG0}_>uz2O`oq^cI8Ua6nuEL@8xWi)GxE4q z>45&(K7VH9R;DjLeNU%!Pv4AkRd!JEtR~P0g63bGUcDSH5X<}NMhXatJ}}LJM03Fy zC^tb$10XMKR&|&1hNFx{rHYg@qG(#T;26YKb5+lQByc3BC>~T{*+UVmm%^Q{H%gm2 zDz9G8npf2vw|Qb+oZ8Vw*K6(SJ4uK)F@;F<3mT=>e~4n(yjg-*Xma6qZDUxPN&Kga z3A(N~bX#zK;y+0`*^_J_vAnd&hFjBB%Q@IJs zrNM(PLnM;cON(xo&tsa9DT)7AKp+qln{^XDIT=w{ruydf!Q;}iW8V4{mR$dwdXd7& z&@(U55PMqP`OMhLW{gM<1QG+{nf#Nri#SB`i~EX@2Z|6~FUSK$gfNw~JJAcesh5FW zqr`Yy{u#0KawOFDU)^}z|umIb+8KMZ)OqvJ` z#XV~|bti}v6p8~-Pm&GY{Tif_2B~u@ZS$N)ME)|nr-gK_fy0J(EwFo=1UeMz8(B#8 z-#ttc9vSaPG+jlo{?R#_fU()L1Lec8UBj!g^xAXK#hDwjDcaIchulzY%Z*dX#a;c9 zS8j9;4gaZz;Nk!T7l#-R$G9b{FYm2=9NG?#f&Qw0rrS2s=U$16=%Md){`G$3G1zn0 zX`^64WnW_FA2xbMlW>`-5lDptu4fFhCCg;l$?H>7Po=ZVB<2M ZbpZE~je9oBQ?s`aa&_`_ta2bx{{;)y(zyTt diff --git a/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png b/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png index ee9a9d9053b66f4852f4c3d176f42e1cead1a9cb..89011df49d34f3da3b765d0df7097c5914331991 100644 GIT binary patch literal 2635 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 GGyenQadeyj literal 2638 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 literal 4541 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 I>zopr097~6{{R30 delta 991 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 J74G(l|1bGGWx)Ud diff --git a/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png b/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png index 563f23d093b48bd455c244ca49e3b3448efbab10..4308c536144da75a46f915481571c3344ff02b81 100644 GIT binary patch delta 750 zcmbQrK9zmK=K2fwm>C%U|9^ACk?XL50P}(W|99yd9KHSM#3`O96An)jRbxNxJZo3n zIz54vX8B(}8ecr!%QSJ_y6LOASr`~LtOPNRgkCHEXPv%ix-{#fxXJ5ue_GczTD+Y8 z&hn#bLaRMH6GO#nhynZNKlyb)7_4xSPpp2pc*o@WdHI(_djBahocO>9aYrB*lOYew zh2QsYaYv+No6lYLzEM8tl-hz9J>j#;_QvdPwK(PT%wwOh?VN`4UF{ER@93QWaz)=} z`F$sr+xM4fO8wJnXCMvTN18HLooHiYeW&m+w4c&Zw*t z$HDL*>~Vd)YqnpV`chN%?&EUvr|2IEOfz!-H21|rmOZCKQvYu9TX@%L%I)~&nbNfr z86H&0Ge8`9kn50v0Lz8{|FhpWD#hknN#0}Tmvbub2r8=yE8QCts}NMX|Kw|Ke;Z-j zI~oG1uc~(La>)5!H+fmc7k4Ixh$~-RR;-7TTtx-9+h@1 z?`urRvGWvuUi2AaLA^agI>_N~ryFt|FyLYS|Nr$l*`Aex&kEivGz%&U#VMVCzrx-{ za%sMoq-xIYuxremQ~P`6>e+MiL_A)6v99BNyl(&3b9)__(l&*CV%Q+Sr=nCeon7hD Whezy;-Z2ae3=E#GelF{r5}E)%Z(eEu delta 750 zcmbQrK9zmK=6Zwc%nS_w|G&B6$kps1z;fXKe=Ge74^(#8=89H)Ft9lLDDn1T#nQbo zu?j(V_gp-F-bLS>%VpQDxOI903=9W+!7^bx_kDgpr!y{3q+_pIZ1kt{UrZmD#P523 zWO{?>KN${&2Wwwd?cC+C+JI^g!kUj5J&|J+0?(RnY=9H3qRw5Jb4C)Ljt)D z83?dk_*D*03=R3mz7{bm)fS^b7J}ZTzTRT zwS<;OtqctJ>a+f?U-9fodC)3LO}XmYOZx=eZ=RGnS-z-U_(R+>?bq8&xgLuxy8F3w z*0zh{oD3_zF+v=95aP)H|Fhq>2JOl+lDx;vFXvR=5meUq|H{{*(ngQ3HIu)K&#O?Y z+7YO53M>?nR}b>5I}^i>6|bMZVk(?+a##Jt8nr5m-FIKcy?gup?58Em{~a#=#WWG3 z(g>z<;;W}_^7UY&F0FlaI?Q&tzQD_UKVwzuI3*q)Z)9MYR|^k3WrDDwD?U~|IDb*#~-!q zXf#+kS>D=SVvB+t2Lr={(88TDcN_{z|4sVJUT%|oHzr%5#qz)EKlwTLCz}hXy;aYA z6qC1~^`ip=1H<}N`Yy9hSz2BQIg)#Q>c_@{spsy!t2wsI(rMD|TAl;9cK^Fl{fVt< z<6jluIj7%l2V1b?M>;q>-%j7iLl|4CyNOkCt~fCE`4}3 UHC?Nifq{X+)78&qol`;+03c&|iU0rr diff --git a/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png b/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png index aa1569bfaba5e3e5faea55947ed3bdf3456e6736..addd1f49696bacc14482dfbb2ae7e0d4973d46ad 100644 GIT binary patch delta 627 zcmcb>agJkxiiW7Ci(^Q|oHqvy85tND4lVfVpUY*h!GH>ONH9z+(2_IcWnf@vSnzMH z?HWdDCR8OG7$#0pWQt*#xLvV+gCGL~!~g$pBH}-OQQ$ekDCFm)I;UlF0O!<(mdOn* zUMpBsLYRa;`S#1HOey+1HNS28-I;bNLQJ!D>py~A0RjcQZt+}~pZzbh))Qx*{;~g_ zbDH)V@twlQl>Dr#G?$(@tIU1=jBUvrSvZMj0t37_c_(Pl4Bl)ta&Tg7+z zknhqjGS>fQoff%NUpJA(J$T~#$>LGIPejypR&T7ndWKWp=(6asX{$wk)>)V7e0nvdirxKRy5?r@U%r#sm>dLV73r;v44Jjg!{*bONHH)l{Qv*vupuu414F}t ze`|Fga!NCyDl*`l=%Y1pmNe5H)`{B{>kGsodLm*seo^2#$|w}(q`Ia>d!djPBd0ba z=hOgBj}plbZW~KuvzI{_qR$#9M#v=N*5V^uXxf zrRozotzVtrwQTIT+*dax=*hE*t?AF?tj_guJqvbOTjJ(&tanSEtg8;|`^-sqmY$hn z{Ly)$zVsHUniZ#~U0z-@Q}Wn@?|sFaO|A%~#7NX7g=pOl7O+cM=G~(!UGijixs7{h zd*;JsK~;US^=DGKUf$pD(X@EwiSH-5*QynD2~WFvV{g{87XFzpxsp$ZcGcL+T8sUB zJb6E(uH?oW2h~5jHht(;UzU@n8xqCt{x4l~v-dC0$*fEc0yB#ARz`-*TIXT&Y2_4^ zKT4BBnN%6OCs#8`GQOWYl}TRpzJT_B?HQ9*Y&y5<=Gb%Zv$Yuyaw9fe z<+0)AUN))m`sA*y2@m#~tIFPr)B5l?(@uA0ylMSz<+{n=m{pCv6poS>5Pd7xS8Jk zDscIAov)xzkZaPk$pMq1yjN^I(CQ?(<;mt#O{^0q3SXYY9@4!c=hcIkGnF5$%aWVn za5XBZm%CM|VcM_YXJ3z$Kby#WUfiHD!)1A}a{~XG`T!G&jtGynp@Gh!8kf}lh3dH% zstY6~O`IPc(0In1*J9Tol{D|af(Oq(l zX|>ZtU*2g!XX?HFf4>o!@^G4+lJ&Lct6e#jH)!_1oXOLov*g;UO{+9SUiP{;E#&)h zRMacQ$y3$oYew;+H#a1VttU55XuT5k^|>C)gYMM=0)_Kgd&^VAn9e%!F>5;Uu^U*< zZtQBk5_RfKf{=@wkDud_TG21}CWdls30uDBqEjcQ%Z{5oAv#OyuW32&3(zo1nySiG zc>l%J3CgntGK58Pws4t>C|v1bJ0%vBz0hzqm#f>FZQ{On|ai!%`JTW zvD+28;oYq~YcIvEz5Q_3x7J_jXTQart*?9|{9f+&p4m_4_SFh}@9)d5l9dnrQ2YMD zhu?nR(sz_T_&$Bk{^iy(eC>xH7Fa;286Wt;$@0!w-ev`B6}ja`4~o ze{W1J1XwmpZfNkn{+2VXd4EjN*{od|@>}2Dvx7Mk4DM8ewZ6rgN9V9OcVy(GF!R0x Psr7XAb6Lmil+XkKul&Ko delta 1070 zcmaFD`IvKpis&j&7srr_Id2XaGBPkQ99r}l^kF~Q}SC7UC9P9Muv&o z-I#P(Co40$*2^pEm2aB<+H&Wr7kRqfoAIoa zSy!mSu_0Ez&*T3s#&d>Fj+csryn?bhO~rNuXf#c$35(pIU{sV8^{F9CaH_AYjcu{Z zozSz)!YiV@R~0S{V7U-fe`?RXALnd}9{gld6X<DqQeotz;OQamm?ORh2PcADtRJ87D>;PIBZZu1VX zS}9pSD)8)3HgH#N5@Sx}Ub*$ant+txX0@kY`kP+an90nHi&>z&$@WA=UWA5Wqkw&P zWZv?`CrbuOXtod_mtzmYPM=5vY0(Jw4K~Iq4i2sXqafL(%y+4 z8dnDimY^UTDpQhK*}!N{&qP|8GGD%{&RxcrNp>Rw{z^>ro&vH7p5zgm2Xs?Tg9GJ9-(t3FZfH=?4Iien$!zJUL0Zr)q{Q@_9V(cR?v<&*bZ-pK#x-^D`T^^X^G z?RzgPQ+NO5Kc4M;fBsGS{Jo_8@WTQN89pd=ys@4EoILNGg4 z#YRpOR~+&W&hg{;P%X4wreoe0& zU62-LdA5kvDwb7uU-s)${(sc#e<;gsY?)r-G~542fR9C%icFaBI%^^O`ouCuuv>2~ z&E_m&yvN-)R==i|4CJc^V7by%G{ WGIAza2X6%_^>p=fS?9!*&;$U!3A#D} diff --git a/src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png b/src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png index faf502d01950cf3ca08beab990dde84bd641f639..787cbcb6c290adf9186a18cf8fcd89c4e8c60d2b 100644 GIT binary patch literal 2220 zcmai$dpO%?8^?b%a6DcicNdwXBcAI~43>wdo9`@XLGel8P_x*=d1 zFaQ7$1pE;n0Fa^sz^>B}0001Qlc#C`0Es0W!TBc81T3r+005nIk@x3NZ;TjtEAFZT z0I;$jP~U}~2coDvMVM1+7V zh|ZwcNB>%R2^?plk_Xs9|` zJPdrv>Q1id`EQUd{6UQ_l~WFRMm0Z;e`ZKn7HV{Oc;FvB_emzkp)F5F2ZguoXGG@TAQ(y>3lP+tK>7DXYiG z3!ZDLVazW+I=&q z^n@iDrW%At;qNLygPG(6sQSf7Ly;0iB?4dCnYW26k3Z3rwUFs`U8B+GLt#My7N_+f zKuTC-vEHl@XVIwZJO-j1ys?oJq2iE3;z<>&nwb&|CqA|&H~H6P^q6T+wvZYND`Yet zQ(_rb;mbx=r1`&QWsiBSL zIoctnF+dpQNlUr|a@@E&h+f?^9ORp3YSM+}ZKAQZDf$n_lVgmWi-iX-o`I;T&rVPE znoZ`a_}{b~6K;5=+hkR`?!S|=OAuE+(V7=(hnTho8B&TSKaTV;ZSa#vM^YUY*H(r( z_=b#vSxxdXq0;i0UQU8WI}{FwL%+&a(`Vy(nWgmRQX=%MK+|cN@1v_!kX6GUXM#ks zQNH?~_t~P8iSl!)k;bJL3VE*|*_O%b)b;f+#|6vd)8Q%;?Do>~Uo%M{yJ>}<)2N~I zC0`-E6Ni*=1MwiG-qaIZH}_cW;$q-pzcUvP8w#;6D*fdi-_*WdYD3+zJMk8F{?erL zhbOCAmB=~5km%mMD|qOa^_2=Lc`r%H58d|Uqhby(sEMr4KK#^Qe9zPnwMBMj=H#PZ z9L8vg9Ai3bI>dL*ZV@}7{fA%jQ7}7_jtHO9{#zj)Dl_Kf+SJNG1aY_ng|c-?x-G0% zR?+7fNtS+8lJa1nZpW!X>6gh{NxpJbtb4~IXBmEvD4xSdxbxw$D|;8Cv4{2!3{wWG z0+TjUNNXvN0;gh#o9`Wo;B8{^HWUCLtcI1 z@p1#pG#L{j{r7Z4A#3O=1`)z2VQJdEIl8y#c41n{9DQDH#R^6$h*5DB@NqbMpZ?m- z33r@*2={T8y_WKj{=+(SP&{iIc&$Q9lh3c_C-K;(vSFE&^xpIInusVK7e z)^($bHX1i?1nzt2VrvuLyg6n}`0mRoFG9&vqbSml){J>9=YMW)y5*9S{~z0774p8V t*sw$V`*&alD$eozuZ{S82C=g>S*nY>Zy4xAWygp+LvT5Iq|WJd+VA02W%&RA literal 2228 zcmai$c{tRGAIHBlM#eB`vYLZ+M9vB&xmq)haV*C0OG>eFW)Zn}D#krI#u)c8BXT^> zS{k7oP3_ROk}IUls&TGFt=W)$`mw)XO~2>Y=kM3^e%|lT=XpM#Pb$G44~L;)006+P zt;~r40Ad1wkOve10Kh?G8VLZoQ3k}z8T@n7uupG<0}Ht?GawGc+;3w3Uv zfzvS?sK7MN%3>*n+; ztsHxwU9Kd#yVu>_`;gpBg+}i{5fU?>2fhop2*2>~vibWJQ&)9AJcW4Mn4tNjFne7T zWziO0;-wdGuEyJ5TV`m{t`2UomUQj%&n)4y2keHiiyYhhTO#KzjN=UvZ^c-smqwDa zzWm`mp>Ox+=}Qk6-EnX)LPtj4&mghBTap~2@b?erz7-DSMg0y^ci;>ZxlIy=f+9-X z!g3<(!+x-e@k-^rH=VFT_Y{tzhgG**cbc1ZP7!J^;)1hziqN``Aok@_5k#^|2jZ4= za74Bp`qYIvO6-%qZw2m^*5#jA)1hO%?xnI1(beTKzc#=R&NhtkMH1w+>=4gyqEgBr z2z9UJ`Xx`bc+P{t>T{fD4DbRS<^-v8{4U=4) z6D%hXL1@!Nv4hH@kY`vN(pI4qx9de4b~B+&3tr=deBwU4wLaF8!ns=aR)h?K{1wZmr zHAA7FBwei5_gna055urH^4R@5PiNSEtEthLSCnMPX!-hxM5GS=hGM$CM0g@A7=O)x zVjWf7yvM#wIkue=+ZfV^k`S(|n#Qy$B^&+SD4kH4kD~+&k$>mO&R%;8E5hg%p8YU znB(8lz?R1qeFJ?M6jIJAl?#~~w{bC17T7@6RS)mcoij?1#M_S3KpsuOM_<{=c8q2WG|dt8NSV!0nIi5z#DQ7t>_&hLL&!GtN}zTeC>j$TGsmzkdC)E(X15~o@4 zSM-yv@Y9@d?lh+L*ibpT)<=&rcYw-h^pD%<*fgmXCVH4u>xG=C^~TEtDDiHxZm;EB zY(*C3xCGqu)vYu6Am6gy`RYJ$^usNcD_`AszT3p)4N*Wr%51>#*%ez&th8c!&GY!v z3i@4<|7@X;WojpzJ=x)TlXM|a?jKglu`5!VV6q4=9_RM zBh|KU^=IF`mV(jni~d#2U5#n4^yYQdW@9?jo(@`K0bt`$669jq81H0t;q<>U4bJ8k zim9*O?siNokc>$3{FStFlpg9cn8KhRo@m&3$sB9-Qx$nM;%=nkf3&|{-TLAN+b+FO z$j#^ZO&(PF?whZk$7Lx&B#CG`|wJK;=Y4 zPcy5L;dVZ2#=R%YU8U-8vwt~q_||gQQ*Yh++qWKHW)r%_dTK_~wQGsWW&XxzE^TmS zV_9habCt%6iJRwXXv`H@`b_hQ=yjhNr>L{UW4cjL$mSt_< z->;)$zAoa2L4&2_;s+d_zr@#^^qNv>G9gB4W#r{ePW|P(m#xx0f8p`xfa0)sQg%=0 z*>B96*TZ|?`T69k|9&eg%eU^(9$4%JV^C{d4zE{>f*UB4 z$*fKjmdvo#oRk`M2Kvl!GRYOM4hW3Fj>=;rw4O eJxL0bat;JtH8zodTxoF+WWT4YpUXO@geCxwBsJCm delta 679 zcmeyz_MdHnO1+z>i(^Q|oHqvy85tND4lVfVpUbVU!GH=XBpDbO{{Me-*pQcjfuUi+ zzqPs#Ii;CU6%}wo)HL!iFfbfYsDG^-vtSzws+t5=u$t-l8`l`{9DTh&XyPIf=_hBM z{?7^ewz1^ims-pHGp#r8%$+<(t@V(-6UW30qAdR#-^ovW-CMuz#In16RW%CimzQN& zFgoUW6>eG~t{SybSgb?2BJXdz35%mGAmHBmJ`fjojI4KbM$yT&YZUc9edz_KE7I9nx9)0YWJ{ z#||>hoDtnVx8dHC-AcXnZ<(teCBD6^^t5d8@x!;0gXd}8n(JlanEiT#;O^y;X_+^a zn42!lt_cLc-M8+FoD#ab?`w9R_}h0y?EUXjO~3C?+y3^$mWra{bvqZD z|CZy5wEwwn<43lKN|IV`?gOJ8+_vrT$G6W?*nX{FtaJW`!(*e`t2su@ zzDHY=ujVK*zjgoibgK9Yzq_)lYTADJ?cDV};Qo>T^V45_Z`0Y#DiK>=}-Kmitilc`e?bv0Rz z-I$HkA?xRd*_|Cl>>z=^zL?$LSHuny`16z5g9AnEAc5cCnLRpE#10bp?Ty*vV@2#B zfnQ#jJv~*#mXs{Hgi0tQ;Z}^52r)BC1^oQX?D@GOHm7XKWt2l18Mk7r1dN$cD&WUQ zW-l)lu~VX~{yQZZ(G<#mXhz+tFg6SfnW>RyHhq7*KHcA3oE+}W{odip#m)WG>&JIJ zfqIMrA&Qu}ND0?q9`}@w6 zX~8Z9dzgIi$Z~+$DOE~`R)Tho#}b@Fa7yVe0Ad+^xp=@$1MVRs1ctjL+%!iB3~vkY zE-^x2cprqf#SsF-8$Wyx7$Gox(}6EO!vHp!OeT}bWHN0>+iDkV?ef?YBm<6={eSfY z$$%qe|6e^pl5ARk5EV_g;jEa=iZchKrSKcC)3y{WR2fahMlo|ZHeWPba=+CIzv4S> zOThwgG!+}g%)!{S?vi<=L7-CO{5khPX*hd&hr2|&3 zQmt?ew_TPK&l;xSWS&Z1(i81Iq!aE^Z8x5YXO|*j@^Y&9rlmS*bi#9}_0i%vhe(*r zQpJy@?k+Jsy?Bfbc)ew*WWXD;>nuwp1KyBbXIU!s6xd`k^;EwA00960?VCXgz%U2| z|NrXrSlWYstx?0q%s>yah%AIul<)xnz%<7-j~I4@TY_On7{H$KEmX&tR~>cjDQ>^e zp40bJKQ=M_)c0e{d)M}36PJ@4QT=t#Gpu>=3Xinr)lztAsr)ULg+i@GU0d={ONq3H tTka(=oR*8>w7MZMoR%wU7yuxX3!bn%KckTSqyPX4002ovPDHLkV1fuyi!T5G delta 872 zcmV-u1DE{T2-*mcH#rM0C;$Ke000000000000000xEcTe0RR8&!!Zp2004lX{;e(H z2^#_c00000000000000005A!WOe&F@8v@(~k+&fNa2Ju3Vaj7`$|cmFD;ln**p)v0{3&A|MJ8e3rIRe|sBSe3W8 z3MBb)03j5)oS6{QAXeDSnJeJ@bf>TO&0T9dR%f$n38gLI8Au!w};ifr4 zV0c@AcZm@K!}}n-EshWv-uU5rzzBiin+|;O83wS)WHOmdCX;D9+E%+@YnR8KAQ^C^ z?EkALNCq4!`~T_*l4R3@f2e4>4QIt{R-8E?Ers8BowlW5p~`3~Hj0_UvH7CelKZVz z_!Zx2TM8C{qp8>^W)8-tb+0U~?Cg4X_j(p1+xb3o>c-AllC-YSDlAdVyA)RoSYP<1FJi8PLlb2J)H!amkqZ6J(t&bMZ zIYh!_mMVTMt?v@k(~HO0fV(YAB?I1&?XoPD40uDf%d%AJDX__8GIgt800030|LvR6 z3V<*OMgRY+*JIR!e`V^rbLT)0auK;OmMG%`0QjHnnnw&f!X?45BMe~A_$I1jd{-TH z?I~`*(4N!xQy(@l{nYzo%X8QEViT8>8&Umr_cN?{@CuK#=G9VoX{r1zriDVSMO|BR yQA>%mhfD4yFr1c);k3FTFr1bvY8U_@lLMZxJUXFs9e8>3C&2~hwSJ^ zLe`M$KLlxOCZMOp3L zlfUfQ*G;>>SKYnMVvpht7-+ceT<@Y^eMlwAz3#(#_=3Rk=;m|Jjf2BhPgU&O&NemmC6d zEx7ED&r;}lq;mGtrguGdk<&JQ@tmabTYpynhw3b@eVcH@ePz~6qnYvh%{#Qqo7a7gH`eeC z-p0Ff#~i-t!D{EblLG9;?e+b6f?fw+vR?D(#Al`RQ@ra79p9?0(avrAHRX-hy015Q zCds;&sZN(Z#d=ktY<*|UtGQnfMf==~{1lfx<>ry6KE)x;H&RrJRtEQn$h_Jpsj^(o zQc_Yq{NVmMOX{EA(Mf+DD6MjT&y(9XSMW}H|L)V@x+`oa*V}*MzPN&S(tZA?=5G6W ztC+#bWY)vnW&?p%TTe+v<&>753xQT#i%VGCK5~RCI@s|30iXK1J5PQu`S(J2&eQ3W zb00|7f8yOzDpoz%Cl-w$dauq zlLAUlhO{xQ3>X2)`? znkPFCs^3waYRqQ4ibZirUfh)QuTAUUf2z|o%~&7brzKt%@G>i?XWOGWvDSyqbxmIH z6()CSGh6GUzbgcC&s**O-7?o!+x)~S=c;l;)v3!>Ipy#DRNE5VZ!tG2E(-WL*k_i(SVT1rA)EXH17qLmCTv+F5&oKEKi%NY;QQ`!KW|lOz zR6p@*)AuHhN4?J089l$Oo^8b0KG}3-&zY06R8zM;SvT+Une(Uo4U5B-RW`bn^i0s?`qh<}a^AM9dwfa#er|W>n|)GwQu}uv_I+%jTJCqdYU#{X u^($_xIT&~}I5P1FC4elIU}2D8xNwM@jXf`T5wk%($cdh=elF{r5}E*Ph$>hB delta 1245 zcmeC>?&qGMA{yrD;uumf=gk2_Mg|6kLkqt8=W^>$v{b6seaI=z#DD?{xEL7z|9^A1 zk%xhS;ebN@Yi*eY+gMPPFeGq5)G%@~FfcSM_`iO!JQo+Lk^@X=N`4EXE7>5%$S`rc z8>7y|f6Dbb5cM;3Cp>ZxXuYl+uu;U0skQ%@dvl+=aBu&y<1Pyy8wOtdcu{2A`UiC$ zzf7mETxtEz<6ijIXK$D6e*X52&-oAS0!|!?Eoj7q{{n)Qx5M=c|E+x#vWWZo>rlOrs0gc#VoAe@K>C&7HF8_FJ1rIqx^u|9cz$y)t*>p?4CS>(jK)H8{@jw%YT{ z=hm(>n@u_we@KrOZV8;TBr@*W+-vHmji%ap3MKz@{jB`QahB8hKd*oPKeoO~Zu2BB ze#7iKw>r_~zhAq5n-?Ut^WPH3rrJ-l3xDr*jH;9< ziT-2Se90vAvcu8T(j9AzV)WbU9skBu?cHN=V$E@n_Xb|guUW58IsNZZ)6Ur|Vl%C` z_&;3tB&vVDc&%I#I08Tu!9_Aia5NMsbL{d>XrKRUWpjDUqOAfhK!I+c18_&+I zW#<&l-Tyv*-Ur1vzlNvbW*!Rr6HmOCsXw9sNsBKg-!qc9ySL-pwh*4IMJ7wOFHH&% zUaGiPSF~A5W4fYW;f^;`3Z=@ubiJn4`=4cr>WXRGUS#Gr`}B(9?&5o@_a=S%7m&rI zF6qVP1d=s55p5~G_-tw1b0K-og*!^ln5Hh>RKHQe)O~Rp(|OU%3Hw2sU++0L;f+LC z>Yn!BJGA64x0NoMyD4SQww3zlgNx_+YApJsr~U2ax;?d7D~s)>uaLR?USd+w-@N)I zu7&!gHK&W4^((xzRM~7-vnVdfi<=t$wQ2qPPjxFzU#y5frxjin@HQ`~ciZDB?_?h) z96wpEs?GoMX0y}R%8<^ow5jv#9zHm8ig(ke3HO41JfCKI9$N0dPx9@xGcS|YKbxzzB4XuD|7SJ7Qhe1O7Hm?RI?*z6^O7~w?p!UN^)vFPX4<=~ z7L{uprEV)GdzM_y{`TwF{Ttu?F4#0Z+hr9xYwh0;ES>w_+^aP2y&Hbm_py2Tj!c>5 zPtIjVuQv()%3pe$g@cXLz@x#DiAN}50z)$c14EQp1tS}KUU28;@Bctf^mO%eS?83{ F1OPonF{1zg diff --git a/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png b/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png index feb4c4f8b0f4d013b581e94a49982f8ca577de80..e7415b7aef7801266881cf7c865815fcc67c41c3 100644 GIT binary patch literal 2473 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 aj)B3tI%j^tac3S-()D!pb6Mw<&;$Swkz~;T delta 826 zcmaFC_MB~kO1++^i(^Q|oHqvy85tND4lVfVpUbVU!GH=XBpDbO{{Me-*pQcjfuUi+ zzqPs#Ii;CU6%}wo)HL!iFfbfYsDG^-vtSzws+t5=h#E#th&k&g)=SpUFwA@9AkbFJ z9M53u_hF0arQHWtFRU=HnCd#c&q(&&rCnambERDt#cJxZc3Cj2%rHMBv1k8Bi{_Lw zQvdwB4sEdB{BQ4@H}5|53pjBowt$Eyeg$jGCfBaZFzM0y{8W15IrR+3`TIV4KNsGz zfdBrFwVyeoF6_?#bMYsO?u)ba=Ko&RtkJZW=5Cg{FHq~kE5RGcRrR&E-fF&%roFl( z&#WJ9FO<0sWpo5x=+6Ih{pXC*pgsAM*Zj5Yyt8uYFO~<&98DSTPDI*okJNj1`D4a} z=;n(RR`ELK$FHT^e-pMVPy4KJF73k=Ufp*$iqc#rUw!u^xQsD*`z#luQ<6-)3+nym zu|Hj45WB4IUBa$e#{;>V;wAt5dop#AjEGo>+S+wXo|>{$G8nbYIX$J>>D1<+f5y7$ zmM5-EIK1_D^SR}lYdO9J-AJw0;_$OB;Cr=F!J_|==ho#-4~{XEcV1u-bmCA zp>{=kQ&eS3+2X(p%G^pV0#0&`yKmcRYLx5}REeLu?!qTtQ2JWj&U5bEn+sD88s9N6 zN&UdX!Rsq&<6Pz(oF>qsIW;-sL9p%A&le6%_YTo}%I0iv)-hD_6?0M6Uq@RxlUEA^ k`6uR|*N|Xc%wW&JU|pRvU$K$pAt)Jpy85}Sb4q9e0K5@qEdT%j diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png b/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png index 3a60a59d5b294d62b39cf4f00c0a75aa1c9d8981..b216174e42ec037c12671a7cc87a0452fff178fd 100644 GIT binary patch delta 327 zcmaFM{GNG&O8quZ7srr_Id86AT-$8G)A}&yKt|64L5a>l3B|=JEb({Ed@qX&9BO0v z_x|mPLkXN6&I}FzR>TSaop)-ZDf4VwG3ME&b+-;|TA>&8=!g7q?NyqRrE%u3PuI-f z;b$^g=CTP-u-3-?uiAd>w0NfRUT)*X14qvhjh+dCMop`?WzLFo;E7ykHBtkdoKbyIeKxa5!Ux$Boc){cGNfBfU}(9@AcH#e44Fd9Vj|G6dhK2?I*WVV@(11&W!2w^W z8be;NntyB8K4g?;LREB!1!4|dP3}WhbTu(dXlisHa^h0c$iu+Ea6qB{wQ|gYZ7isE wNid?hS$a|vnschbO5P&8^;?h&6ptX#!z#vB!P|9P?mvj<>FVdQ&MBb@0NY@q&;S4c diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png b/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png index 8b3134e52adbba897a0f480d92636471d713cd7b..2243248b07668a08bcd7ba0e5d146238fb1b7b57 100644 GIT binary patch delta 402 zcmeyv@|R_TO1+Dxi(^Q|oHy4W=C&E|v^>l?#L;trqvt?|q~_uZmT*ylD?wMMW%N3} z|NJ>i=w(Rb{vZa13ae1xsTD5wTpa_a2L=Q@a&%eA3rg^)2?`&(f{rEjfgUcvjLSqw;NXh~QW>!W97&!3Oj$yJMqY~qq$$^Z@+H05? z82D~o delta 362 zcmey%@`q)DO1-nEi(^Q|oHy4W=C&B{9Q~Mch@j@fyQ{z1^f19@3 zM(tw0(btrllb(JwmOK1w%BnYK`*umo+dU5Yo&43~R?M?+%9T&jd%9mNRDT@zW0s6? zP}QchC%VsHylnaFyyf+@n141a{y#Md_A{Pu`}|T5_kYK>ocFCI?-%66Pb-PvShCx5 zo9&6+JCEgVpLn}$gY~~SZdXS(E+ep8L{b(gFu;Mwd+x~(7?qgTuuSG+RASVbtjnmZ zro+U*@c;jt!;L%)3=9Vp>R)TiEZD{Z*8>I?Oq0tPGZ;lC-)B^2@@E9;Hd@aB()ac- oBPRm`L&Jjq>!l|(F+sJ-u*g?FVdQ&MBb@0Ap^FIRF3v diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png b/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png index 64b1a80b8d1d89ee9a062a167cde4161afa90df2..f3a887d20c1f9b7646b2e588ac2110d5089c3005 100644 GIT binary patch literal 1210 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfq|vb)5S5QV$PeH4|5MI zh_vjT=$^phc1ok?5J%4>!MM<^y$shU?zpx`c;EZ*)%xjid?!86Y~xgH5pd#AY!O&8 zW9Hfvff?z_E3WKkd3KXS>#KfarCF2LU+;u@y$gc=3*nLKNaud}xkz(Oa#y6JX>l6! zjm@=_%V%cu9{RUM{Cw;)ujYXGwR`8Csh94^;oWI$nHPO#zhr8gF~>nGr)0VKdiy@N znGbnfUWt2ct$bVeuGi?XWXC1_l&zii|Bu{~QCe!|yX;Kd^9dU-pZRGeo~m$9|H?kU znfp@}X57D!CCPF3(TV)RGxDoerz*_YKW*K#g9-lp4~PMW895mk7#bG* zU;kT>3lv*OU=1tS4uq21f*Kk~@(c_NAH<->81jOR`L}lMLq=&PR7LuX5Od&aav!py zt67b1j_yNFf@;n_;73>EjqWY!NlmEEIS`Ak1k>lmP#41k2h-)$A!Sr|m zH(H|beaMdHUJNC?54q8lY=C;Hk%xhS;ebN@Yvq^)+gMPNUjhrrqPK{MLYNY~mZ9so SQSWLskOogzKbLh*2~7Yo)_@oQ delta 663 zcmdnRd5Cj@N`0rNi(^Q|oHsKc<~AFMwA}0vv|&+OnHt=$*2)!zHbD2wlZk>4X z{q-)>KU>dLopj@{LW_VChhmF>(}YLw?oDnwFsFCHr5{2wHVcMG#WNL~HF-VuPB`Zl zkXgfd<|c<$uYRMYwByo`ss`!GD@^vWJj-F3dThGG9Nn|?PoJ${DW6&xe@-iR6JyV_ z`!_V6MzJQ={5n-UrD$o>g!N(T)SvDb<@n6BT4Z)x;->o|m44L%lfsucRo3jS%Mq>g zt5$ecbzzS7>vz#>PZZx{-gu?iCVbg^^Wb$kI^OF<LwesG_ z4~x!gKDzhp9Q#TIE6sKHdFwAJ_x%w({%7f}+3Za}(=+qsc%Ipp?X6UJ#P9HKal#RK z#e41tJM3Hb_cJxc7JQ8BGK~K;@$tjy4}Wtxt+qVdai1;sg7>5Ciotda3=IGOzqx40 z%b>t;#Nq$C!Vkg=dCEOSzc{f;ehJ%uaKXy^2^X&98)UpPmoWJ%&BMe10-Ruyp+R3} zawU`EWNs$T$%~n!nbxpOKER~N{6T91QCrwoIPLA~HFig+q{(uXl@pOcq_s(>8o>FVdQ&MBb@ E0Cq|KJ^%m! diff --git a/src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png b/src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png index a768ba7aab599f6660e0f8a6f6425e3a89942d42..82ce82468166189d95acac312068a368f06f667b 100644 GIT binary patch delta 260 zcmeBT?qr^zVjAG-;uumf=gqaVoktZ!*aECKw1hi(`EX84@SHYDNaoY=GpAg4__UOt zQ>)pj5!Wd7E-gU-|spZ>JpgFSVb&?tI+OW%^(K)p6W& zem_6r;PN{xxjq9 zoe}fH6~e?GOle^F+dnZ|5Ewc>(0mh^pAh>_piX7 z_IvgloBVfjzMFER`o+rc{}r1uJ)F-UXFz}tulF%9F#P}j<}f2C0|P_Dg8%D(3vzKG z6o9}7!HFMKR9PYF40*xo{;lnO$Slo-s_+lfWKl*Xy&5Kv(zghuz7N?!N)Kq7DDQhR SHF{16NSmjtpUXO@geCxNO=P10 From 05eeaddb0445fb3b2aedd74da00f3b284b329cac Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 11:21:50 -0600 Subject: [PATCH 080/114] update flatpak hash --- flatpak/zig-packages.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 81024bb26..08fa9568b 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -151,9 +151,9 @@ }, { "type": "archive", - "url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", - "dest": "vendor/p/z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW", - "sha256": "c2226cebf2d48b2f80a42e6ced53f2a5b06e92306be2f8f1deffe5f4ead3ef45" + "url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz", + "dest": "vendor/p/z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg", + "sha256": "c1f69d7a07a2c5c6e0c51cd1b5fe8cd87df8093baf0ccdb65d5221bae7c5046e" }, { "type": "archive", From b1f788a768579a857b0acf8b9c60a70e436846a2 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 30 Jun 2025 10:07:21 -0700 Subject: [PATCH 081/114] macos: don't overwrite the .fullScreen styleMask option in reapplyHiddenStyle --- .../HiddenTitlebarTerminalWindow.swift | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift index 5f4d6b177..d7f8accb2 100644 --- a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift @@ -19,20 +19,27 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { NotificationCenter.default.removeObserver(self) } + private let hiddenStyleMask: NSWindow.StyleMask = [ + // We need `titled` in the mask to get the normal window frame + .titled, + + // Full size content view so we can extend + // content in to the hidden titlebar's area + .fullSizeContentView, + + .resizable, + .closable, + .miniaturizable, + ] + /// Apply the hidden titlebar style. private func reapplyHiddenStyle() { - styleMask = [ - // We need `titled` in the mask to get the normal window frame - .titled, - - // Full size content view so we can extend - // content in to the hidden titlebar's area - .fullSizeContentView, - - .resizable, - .closable, - .miniaturizable, - ] + // Apply our style mask while preserving the .fullScreen option + if styleMask.contains(.fullScreen) { + styleMask = hiddenStyleMask.union([.fullScreen]) + } else { + styleMask = hiddenStyleMask + } // Hide the title titleVisibility = .hidden From 886e33d7b70febe2175e96c6506f5538b98b358d Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 30 Jun 2025 11:21:27 -0700 Subject: [PATCH 082/114] make hiddenStyleMask static --- .../Window Styles/HiddenTitlebarTerminalWindow.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift index d7f8accb2..996506f0b 100644 --- a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift @@ -19,7 +19,7 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { NotificationCenter.default.removeObserver(self) } - private let hiddenStyleMask: NSWindow.StyleMask = [ + private static let hiddenStyleMask: NSWindow.StyleMask = [ // We need `titled` in the mask to get the normal window frame .titled, @@ -36,9 +36,9 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { private func reapplyHiddenStyle() { // Apply our style mask while preserving the .fullScreen option if styleMask.contains(.fullScreen) { - styleMask = hiddenStyleMask.union([.fullScreen]) + styleMask = Self.hiddenStyleMask.union([.fullScreen]) } else { - styleMask = hiddenStyleMask + styleMask = Self.hiddenStyleMask } // Hide the title From 8c5122876fc62bc523b562d08f64872091672e01 Mon Sep 17 00:00:00 2001 From: RME Date: Mon, 30 Jun 2025 22:11:49 +0200 Subject: [PATCH 083/114] Fixed po/ko_KR.UTF-8.po Co-authored-by: Hojin You --- po/ko_KR.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po index be7fd2502..42cb2682f 100644 --- a/po/ko_KR.UTF-8.po +++ b/po/ko_KR.UTF-8.po @@ -248,7 +248,7 @@ msgstr "열린 탭 보기" #: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "⚠️ Ghostty는 디버그 빌드 실행 중! 성능이 저하됩니다." +msgstr "⚠️ Ghostty 디버그 빌드로 실행 중입니다! 성능이 저하됩니다." #: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" From a00a727e779f674b1d79068803dd336d42e372f7 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 16:37:26 -0600 Subject: [PATCH 084/114] test(font/Atlas): add test case for `setFromLarger` --- src/font/Atlas.zig | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index aac2e7e8d..7b31e2794 100644 --- a/src/font/Atlas.zig +++ b/src/font/Atlas.zig @@ -587,6 +587,35 @@ test "writing data" { try testing.expectEqual(@as(u8, 4), atlas.data[66]); } +test "writing data from a larger source" { + const alloc = testing.allocator; + var atlas = try init(alloc, 32, .grayscale); + defer atlas.deinit(alloc); + + const reg = try atlas.reserve(alloc, 2, 2); + const old = atlas.modified.load(.monotonic); + // zig fmt: off + atlas.setFromLarger(reg, &[_]u8{ + 8, 8, 8, 8, 8, + 8, 8, 1, 2, 8, + 8, 8, 3, 4, 8, + 8, 8, 8, 8, 8, + }, 5, 2, 1); + // zig fmt: on + const new = atlas.modified.load(.monotonic); + try testing.expect(new > old); + + // 33 because of the 1px border and so on + try testing.expectEqual(@as(u8, 1), atlas.data[33]); + try testing.expectEqual(@as(u8, 2), atlas.data[34]); + try testing.expectEqual(@as(u8, 3), atlas.data[65]); + try testing.expectEqual(@as(u8, 4), atlas.data[66]); + + // None of the `8`s from the source data outside of the + // specified region should have made it on to the atlas. + try testing.expectEqual(null, std.mem.indexOfScalar(u8, atlas.data, 8)); +} + test "grow" { const alloc = testing.allocator; var atlas = try init(alloc, 4, .grayscale); // +2 for 1px border From 95fbeb5b821cfd3cfdbb3340a8638c0031eb7336 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 16:44:21 -0600 Subject: [PATCH 085/114] style(font/sprite): annotate type for value --- src/font/sprite/Face.zig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index 8c39daef4..1463fb38b 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -54,7 +54,7 @@ const Range = struct { /// Automatically collect ranges for functions with names /// in the format `draw` or `draw_`. -const ranges = ranges: { +const ranges: []const Range = ranges: { @setEvalBranchQuota(1_000_000); // Structs containing drawing functions for codepoint ranges. @@ -137,7 +137,11 @@ const ranges = ranges: { i = n.max; } - break :ranges r; + // We need to copy in to a const rather than a var in order to take + // the reference at comptime so that we can break with a slice here. + const fixed = r; + + break :ranges &fixed; }; fn getDrawFn(cp: u32) ?*const DrawFn { From f773baa418350cd4109a8ad14c8683ff335689eb Mon Sep 17 00:00:00 2001 From: trag1c Date: Tue, 1 Jul 2025 09:36:07 +0200 Subject: [PATCH 086/114] remove blank line in CODEOWNERS --- CODEOWNERS | 1 - 1 file changed, 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index e8b632433..56768d5ae 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -181,6 +181,5 @@ /po/ga_IE.UTF-8.po @ghostty-org/ga_IE /po/ko_KR.UTF-8.po @ghostty-org/ko_KR - # Packaging - Snap /snap/ @ghostty-org/snap From 114c3f56656354ebee0cf434a1aabf56681522f1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 1 Jul 2025 12:06:37 -0700 Subject: [PATCH 087/114] Fix abnormal exit detection on macOS I made an oopsie with #7705 and omitted the check entirely on macOS when the original logic only omitted the exit code check. --- src/Surface.zig | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 390adf91b..cef71f265 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1002,10 +1002,11 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { if (info.runtime_ms <= self.config.abnormal_command_exit_runtime_ms) runtime: { // On macOS, our exit code detection doesn't work, possibly // because of our `login` wrapper. More investigation required. - if (comptime builtin.target.os.tag.isDarwin()) break :runtime; + if (comptime !builtin.target.os.tag.isDarwin()) { + // If the exit code is 0 then we it was a good exit. + if (info.exit_code == 0) break :runtime; + } - // If the exit code is 0 then we it was a good exit. - if (info.exit_code == 0) break :runtime; log.warn("abnormal process exit detected, showing error message", .{}); // Update our terminal to note the abnormal exit. In the future we From fbdaea745698d20c994853524b9be5076954e8ba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 1 Jul 2025 12:15:45 -0700 Subject: [PATCH 088/114] Update src/Surface.zig Co-authored-by: Gregory Anders --- src/Surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index cef71f265..dc7b0e3bf 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1003,7 +1003,7 @@ fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void { // On macOS, our exit code detection doesn't work, possibly // because of our `login` wrapper. More investigation required. if (comptime !builtin.target.os.tag.isDarwin()) { - // If the exit code is 0 then we it was a good exit. + // If the exit code is 0 then it was a good exit. if (info.exit_code == 0) break :runtime; } From dd9ca556f953e257899811b513c76ad681b1b95c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 15:57:40 -0600 Subject: [PATCH 089/114] font/sprite: add sflc supplement circle pieces --- ...ymbols_for_legacy_computing_supplement.zig | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 9f7e8815d..01258b041 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -192,6 +192,60 @@ pub fn draw1CC21_1CC2F( ); } +/// Twelfth and Quarter circle pieces. +/// 𜰰 𜰱 𜰲 𜰳 𜰴 𜰵 𜰶 𜰷 𜰸 𜰹 𜰺 𜰻 𜰼 𜰽 𜰾 𜰿 +/// +/// 𜰰𜰱𜰲𜰳 +/// 𜰴𜰵𜰶𜰷 +/// 𜰸𜰹𜰺𜰻 +/// 𜰼𜰽𜰾𜰿 +/// +/// These are actually ellipses, sized to touch +/// the edge of their enclosing set of cells. +pub fn draw1CC30_1CC3F( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + switch (cp) { + // 𜰰 UPPER LEFT TWELFTH CIRCLE + 0x1CC30 => try circlePiece(canvas, width, height, metrics, 0, 0, 2, 2), + // 𜰱 UPPER CENTRE LEFT TWELFTH CIRCLE + 0x1CC31 => try circlePiece(canvas, width, height, metrics, 1, 0, 2, 2), + // 𜰲 UPPER CENTRE RIGHT TWELFTH CIRCLE + 0x1CC32 => try circlePiece(canvas, width, height, metrics, 2, 0, 2, 2), + // 𜰳 UPPER RIGHT TWELFTH CIRCLE + 0x1CC33 => try circlePiece(canvas, width, height, metrics, 3, 0, 2, 2), + // 𜰴 UPPER MIDDLE LEFT TWELFTH CIRCLE + 0x1CC34 => try circlePiece(canvas, width, height, metrics, 0, 1, 2, 2), + // 𜰵 UPPER LEFT QUARTER CIRCLE + 0x1CC35 => try circlePiece(canvas, width, height, metrics, 0, 0, 1, 1), + // 𜰶 UPPER RIGHT QUARTER CIRCLE + 0x1CC36 => try circlePiece(canvas, width, height, metrics, 1, 0, 1, 1), + // 𜰷 UPPER MIDDLE RIGHT TWELFTH CIRCLE + 0x1CC37 => try circlePiece(canvas, width, height, metrics, 3, 1, 2, 2), + // 𜰸 LOWER MIDDLE LEFT TWELFTH CIRCLE + 0x1CC38 => try circlePiece(canvas, width, height, metrics, 0, 2, 2, 2), + // 𜰹 LOWER LEFT QUARTER CIRCLE + 0x1CC39 => try circlePiece(canvas, width, height, metrics, 0, 1, 1, 1), + // 𜰺 LOWER RIGHT QUARTER CIRCLE + 0x1CC3A => try circlePiece(canvas, width, height, metrics, 1, 1, 1, 1), + // 𜰻 LOWER MIDDLE RIGHT TWELFTH CIRCLE + 0x1CC3B => try circlePiece(canvas, width, height, metrics, 3, 2, 2, 2), + // 𜰼 LOWER LEFT TWELFTH CIRCLE + 0x1CC3C => try circlePiece(canvas, width, height, metrics, 0, 3, 2, 2), + // 𜰽 LOWER CENTRE LEFT TWELFTH CIRCLE + 0x1CC3D => try circlePiece(canvas, width, height, metrics, 1, 3, 2, 2), + // 𜰾 LOWER CENTRE RIGHT TWELFTH CIRCLE + 0x1CC3E => try circlePiece(canvas, width, height, metrics, 2, 3, 2, 2), + // 𜰿 LOWER RIGHT TWELFTH CIRCLE + 0x1CC3F => try circlePiece(canvas, width, height, metrics, 3, 3, 2, 2), + else => unreachable, + } +} + /// Separated Block Sextants pub fn draw1CE51_1CE8F( cp: u32, @@ -271,3 +325,93 @@ pub fn draw1CE51_1CE8F( .on, ); } + +fn circlePiece( + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, + x: f64, + y: f64, + w: f64, + h: f64, +) !void { + // Radius in pixels of the arc we need to draw. + const wdth: f64 = @as(f64, @floatFromInt(width)) * w; + const hght: f64 = @as(f64, @floatFromInt(height)) * h; + + // Position in pixels (rather than cells) for x/y + const xp: f64 = @as(f64, @floatFromInt(width)) * x; + const yp: f64 = @as(f64, @floatFromInt(height)) * y; + + // Set the clip so we don't include anything outside of the cell. + canvas.clip_left = canvas.padding_x; + canvas.clip_right = canvas.padding_x; + canvas.clip_top = canvas.padding_y; + canvas.clip_bottom = canvas.padding_y; + + // Coefficient for approximating a circular arc. + const c: f64 = (std.math.sqrt2 - 1.0) * 4.0 / 3.0; + const cw = c * wdth; + const ch = c * hght; + + const thick: f64 = @floatFromInt(metrics.box_thickness); + const ht = thick * 0.5; + + var path = canvas.staticPath(2); + + if (xp < wdth) { + if (yp < hght) { + // Upper left arc. + path.moveTo(wdth - xp, ht - yp); + path.curveTo( + wdth - cw - xp, + ht - yp, + ht - xp, + hght - ch - yp, + ht - xp, + hght - yp, + ); + } else { + // Lower left arc. + path.moveTo(ht - xp, hght - yp); + path.curveTo( + ht - xp, + hght + ch - yp, + wdth - cw - xp, + hght * 2 - ht - yp, + wdth - xp, + hght * 2 - ht - yp, + ); + } + } else { + if (yp < hght) { + // Upper right arc. + path.moveTo(wdth - xp, ht - yp); + path.curveTo( + wdth + cw - xp, + ht - yp, + wdth * 2 - ht - xp, + hght - ch - yp, + wdth * 2 - ht - xp, + hght - yp, + ); + } else { + // Lower right arc. + path.moveTo(wdth * 2 - ht - xp, hght - yp); + path.curveTo( + wdth * 2 - ht - xp, + hght + ch - yp, + wdth + cw - xp, + hght * 2 - ht - yp, + wdth - xp, + hght * 2 - ht - yp, + ); + } + } + + try canvas.strokePath(path.wrapped_path, .{ + .line_cap_mode = .butt, + .line_width = @floatFromInt(metrics.box_thickness), + }, .on); +} From 0414e9e2819dfa08bf47a9aca8c3cd8ea5d7eafe Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 16:19:15 -0600 Subject: [PATCH 090/114] font/sprite: add (some) sflc supplement box drawing chars --- ...ymbols_for_legacy_computing_supplement.zig | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 01258b041..9ae92cc72 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -61,6 +61,8 @@ const xHalfs = common.xHalfs; const yQuads = common.yQuads; const rect = common.rect; +const box = @import("box.zig"); + const font = @import("../../main.zig"); const octant_min = 0x1cd00; @@ -246,6 +248,81 @@ pub fn draw1CC30_1CC3F( } } +/// TODO: These two characters should be easy, but it's not clear how they're +/// meant to align with adjacent cells, what characters they're meant to +/// be used with: +/// - 1CC1F 𜰟 BOX DRAWINGS DOUBLE DIAGONAL UPPER RIGHT TO LOWER LEFT +/// - 1CC20 𜰠 BOX DRAWINGS DOUBLE DIAGONAL UPPER LEFT TO LOWER RIGHT +pub fn draw1CC1B_1CC1E( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + const w: i32 = @intCast(width); + const h: i32 = @intCast(height); + const t: i32 = @intCast(metrics.box_thickness); + switch (cp) { + // 𜰛 BOX DRAWINGS LIGHT HORIZONTAL AND UPPER RIGHT + 0x1CC1B => { + box.linesChar(metrics, canvas, .{ .left = .light, .right = .light }); + canvas.box(w - t, 0, w, @divFloor(h, 2), .on); + }, + // 𜰜 BOX DRAWINGS LIGHT HORIZONTAL AND LOWER RIGHT + 0x1CC1C => { + box.linesChar(metrics, canvas, .{ .left = .light, .right = .light }); + canvas.box(w - t, @divFloor(h, 2), w, h, .on); + }, + // 𜰝 BOX DRAWINGS LIGHT TOP AND UPPER LEFT + 0x1CC1D => { + canvas.box(0, 0, w, t, .on); + canvas.box(0, 0, t, @divFloor(h, 2), .on); + }, + // 𜰞 BOX DRAWINGS LIGHT BOTTOM AND LOWER LEFT + 0x1CC1E => { + canvas.box(0, h - t, w, h, .on); + canvas.box(0, @divFloor(h, 2), t, h, .on); + }, + else => unreachable, + } +} + +pub fn draw1CE16_1CE19( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + const w: i32 = @intCast(width); + const h: i32 = @intCast(height); + const t: i32 = @intCast(metrics.box_thickness); + switch (cp) { + // 𜸖 BOX DRAWINGS LIGHT VERTICAL AND TOP RIGHT + 0x1CE16 => { + box.linesChar(metrics, canvas, .{ .up = .light, .down = .light }); + canvas.box(@divFloor(w, 2), 0, w, t, .on); + }, + // 𜸗 BOX DRAWINGS LIGHT VERTICAL AND BOTTOM RIGHT + 0x1CE17 => { + box.linesChar(metrics, canvas, .{ .up = .light, .down = .light }); + canvas.box(@divFloor(w, 2), h - t, w, h, .on); + }, + // 𜸘 BOX DRAWINGS LIGHT VERTICAL AND TOP LEFT + 0x1CE18 => { + box.linesChar(metrics, canvas, .{ .up = .light, .down = .light }); + canvas.box(0, 0, @divFloor(w, 2), t, .on); + }, + // 𜸙 BOX DRAWINGS LIGHT VERTICAL AND BOTTOM LEFT + 0x1CE19 => { + box.linesChar(metrics, canvas, .{ .up = .light, .down = .light }); + canvas.box(0, h - t, @divFloor(w, 2), h, .on); + }, + else => unreachable, + } +} + /// Separated Block Sextants pub fn draw1CE51_1CE8F( cp: u32, From b4d83e6349e34022f73ef550cfccaf8f361a3533 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 30 Jun 2025 18:46:49 -0600 Subject: [PATCH 091/114] font/sprite: align quadrants better with other glyphs Use `xHalfs` and `yHalfs` so that the dimensions of each quadrant are appropriately aligned with block elements like the one half block, which could be 1px taller than the bottom quadrants before this change. This is in line with what we do for sextants, the fact that on odd-sized cells there's a 1px overlap is considered acceptable there so I assume it's acceptable here too. --- src/font/sprite/draw/block.zig | 14 ++++++++------ src/font/sprite/draw/common.zig | 8 ++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/font/sprite/draw/block.zig b/src/font/sprite/draw/block.zig index 27c6ae516..f7faacea7 100644 --- a/src/font/sprite/draw/block.zig +++ b/src/font/sprite/draw/block.zig @@ -15,6 +15,8 @@ const common = @import("common.zig"); const Shade = common.Shade; const Quads = common.Quads; const Alignment = common.Alignment; +const xHalfs = common.xHalfs; +const yHalfs = common.yHalfs; const rect = common.rect; const font = @import("../../main.zig"); @@ -174,11 +176,11 @@ fn quadrant( canvas: *font.sprite.Canvas, comptime quads: Quads, ) void { - const center_x = metrics.cell_width / 2 + metrics.cell_width % 2; - const center_y = metrics.cell_height / 2 + metrics.cell_height % 2; + const x_halfs = xHalfs(metrics); + const y_halfs = yHalfs(metrics); - if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y); - if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y); - if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height); - if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height); + if (quads.tl) rect(metrics, canvas, 0, 0, x_halfs[0], y_halfs[0]); + if (quads.tr) rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_halfs[0]); + if (quads.bl) rect(metrics, canvas, 0, y_halfs[1], x_halfs[0], metrics.cell_height); + if (quads.br) rect(metrics, canvas, x_halfs[1], y_halfs[1], metrics.cell_width, metrics.cell_height); } diff --git a/src/font/sprite/draw/common.zig b/src/font/sprite/draw/common.zig index 2f608180e..d10128cdf 100644 --- a/src/font/sprite/draw/common.zig +++ b/src/font/sprite/draw/common.zig @@ -204,6 +204,14 @@ pub fn xHalfs(metrics: font.Metrics) [2]u32 { return .{ half_width, metrics.cell_width - half_width }; } +/// yHalfs[0] should be used as the bottom edge of a top-aligned half. +/// yHalfs[1] should be used as the top edge of a bottom-aligned half. +pub fn yHalfs(metrics: font.Metrics) [2]u32 { + const float_height: f64 = @floatFromInt(metrics.cell_height); + const half_height: u32 = @intFromFloat(@round(0.5 * float_height)); + return .{ half_height, metrics.cell_height - half_height }; +} + /// Use these values as such: /// yThirds[0] bottom edge of the first third. /// yThirds[1] top edge of the second third. From adace942d014072470ba170d036be53f38f153a7 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 13:20:10 -0600 Subject: [PATCH 092/114] font/sprite: update reference images --- .../testdata/U+1CC00...U+1CCFF-11x21+2.png | Bin 402 -> 1032 bytes .../testdata/U+1CC00...U+1CCFF-12x24+3.png | Bin 534 -> 1295 bytes .../testdata/U+1CC00...U+1CCFF-18x36+4.png | Bin 1025 -> 2193 bytes .../testdata/U+1CC00...U+1CCFF-9x17+1.png | Bin 316 -> 794 bytes .../testdata/U+1CE00...U+1CEFF-11x21+2.png | Bin 562 -> 632 bytes .../testdata/U+1CE00...U+1CEFF-12x24+3.png | Bin 741 -> 819 bytes .../testdata/U+1CE00...U+1CEFF-18x36+4.png | Bin 1388 -> 1492 bytes .../testdata/U+1CE00...U+1CEFF-9x17+1.png | Bin 399 -> 443 bytes .../testdata/U+2500...U+25FF-11x21+2.png | Bin 2220 -> 2225 bytes .../testdata/U+2500...U+25FF-9x17+1.png | Bin 1844 -> 1853 bytes 10 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png index 581b0bbf0547c2526878bb36c8ecbb8277689956..e04e7726b965fab41da5ed0c4f7e3501709db29f 100644 GIT binary patch literal 1032 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=GWYJzX3_D(1Yoq0M*L zK%&L*{{Ksw+ADUu)~#4AIW6?S|Aiis%pm0;AXYtZ{o~uMlfMbc92aVO9N|K#OMd;^-$%bI|M2D0PiB8XWa(s1dk42l1BL^3`V0&V|Np<4oxOIcfxz*?1{M_s zMote0CLyhdV3Lv3V@1nNo%P=7%hEH|e>dLwufOwT<2Hl$ex7>&-mH*)QCj?e;;W$h zJHAbIa(>veb-jobSNfXTr56G$lHY2#nrZSJ$Zu11ySDTxr;hGsJzs}JjXC?a*f4TA zCu;DtGJ?JELw?KR`0dvwhuLq7SK~A3u0ET7^Sa)AH=c7QZ|ozoXTK^DzAVM{|B|BG zgegxKeg3#V_=le5*Gd1`%i~}Fc#*0&`_NUV+9*a(fA69-^EwY*k*~}BttoJ}G-;E` z9TyW>wpaiDr5&E0-NV)Y=w$rN3?JUDjZNKOR>v+_aC~czj{C#3Uv?bw^VO1ORRkSg zp)4O8zgmh*Q(fcA%MLlFO2cavTuLR{x~kv!Es(>pX80xDPG0~yJZiB z&gQ%tFthCPr#(Ll*sFehkbD#RZpJ~AkS<46-Kxv=KPF9Wa;>h~EvY|M>hdy!{+xHy z<5;3(BT6k()4b=d$v^dW_u*zaVX3)cmtWV)drfP~ZMYU8KTm4&8eZk@U&dN958e8w zCR-nVV`bv1N2b~5{GC!dkFJ{eR7xq(Gi%<_#QBG7u3uWUw_vHpvUlCDj_bYK!gM|O z$0pHooiU+a{f`kS{s{)=XcYl zZ55lze`-zY$4RPId|iu*mK(_HRA!v~KY7xwVy?iDk3Lhc1w8ccDx3EBscZGDOnuWg z55-mYI;Xyf^0_-{q4)2F3#`}h)UI49Z}#P3%%U~_?iYPBm|MZ4uIqP=^L){^EpszZ zpTE?!#(2-e*NwgpKdo2gl8n#fx!<0{HNBocxblxIA0#z1Fg)jnq{zdDybKHs4GaFQ z^?k@L%>JZH3yXwB zB({RAgn))KtY8}sGjcL8Ff=Uqzy7x%7s%mApg<7f3WS>5f*NQ}J>UveW5^3O=il13 z4;iJIkW67v}eGzaNYY4FCVXx#8HxoG9UX@$dUx?6#b$m4CWoRu*SO@yasKCHbvC5tS?5vs56So)$v|eWva$^#j(7>Y7!=j>~2xhrBsm$3wqp{%S zl9JPd`|`aNpYt4DYR?~tA@UjAZcgO0Ss>keEdvPxHGK1--v^V?Y7yE;O&{iut|8nq+qo)o$tTf6>Oji?Zt z;k&bGipTYtOx0(<63SHW_piEfxla4`Rl)-eIjv zwq;5w6!2Fc517ScTFbM&=#=n#KGt1mC4<{JAc=xNCbboc;sk0 zQ*XCYm-*wTyJY|B7Hz**>-g$S&G*w9a~9fifB9wWqrN+&PT0fZz{IbL57wu7Y${yv zv)%RcUgND@F59Mke08h()HBgGWx)rB1(whCEqr}gp(s$7Icavs#5k+Rd38q?24!xa z^!~}M-9N9kh%qlp%&sh$TiO=($G~$Z^XV6V{>fE#I4`{<@ze8RiH_R-;`#t8=_7C<;H#J1fbXu9? zR1tRUqL$7}(Oo56`90~a4ofdBiCY=6yu`aQ#4AiI?=|O=;6MH!9wfJ1dM;?&SYjuy z`txV;v_y$EY-nOVBq1L*_-*{+?w(R{|*y4l{Cs-TQyN^rR+K=e}Wq7y?)ETaXJ>fFZyKVX%kZ t9&Y4eU|=|)Q2$z6X2CWVgaQVJc?|DM9vx1tS`V^@!PC{xWt~$(699W|8YKV# literal 534 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|Vn-PZ!6KiaBo%7&0<2 zFdSO&)jyZhUjw873H*>`U|{(F|IOtzkAOAf4*iz?+{q2}3&Q#wtqbE6i(-U1Y zRKwODa1|AN%YD+oWOD4GM3==ppAs%Cl%b+#jZN|BxmxprCb$}N!kev;Wln%*-DLZ# zYFAUEGWM$1;c><*N!#o1W5a_c z%WQ9B(>%-i0~;Nud)k>W|K_@KyJ^r&E)T-f(MB9>J$VHW%s&#wWD~?#X#CQDmEYB^`dfyZlBg zx7|nmZf4n?*k|G(E4Vs~lMORYXROZfwCb=wd*O=AutIdeojyD1IN-HyED-+#KGnea zxYBOV;h3klHN{DA!)A`@&a*$_oQLFRV`&?edFbR?DtW6jnXzHqHggn^oNWyXaP@z8 z12tf}t2)o(`n!D_+B0(sf3W%1=HMx|-6li-aq$WTW$!;(@x3)Dcl2awg&&ENep_!4 ztcv#bXI)8yoy$UC`z*WRaX3|z!~VLzC8H$tqu~Y>M$A_(<8I##N-9UAS896pNbtQO zcW>K@mOn61n!Dsa6J+CQp|?zsCkcZuwJegNpGDr2ACEw`PZd<;6}BkY0bfUtRO0wh~5`M!sLeN{{)YrYh`w^(}vfH%HmSruxuh2=}qRX$h<}Eft%j zbbLPX^H(EcIorr)JtwJcy?ILODIW-?)+?f6;0d_lsmNzX#F~H;k~pT_Lq4R6703W{ zQr6e5{mmkEt8?-jb8h?2x;(;L3VL~0l6cclGW~Zk#nPSlG(Ny6RUyo|3rpZRn&Nijk<(hWYYA)k>#n_k|E?B1Zu17fdc3LH@ z+AX*_(W9LSFU>p56gnLv2iZ9u3F=dXFKM#SSxcM0IQ?iAkCuC=xJA|Bw#B znH(xUKGE=)BZqR@bu(d|hEW1WBR$v&awSVA@sYLaC(*^1=yNX<61TJ{UaV;5#8_)% zdQV}4zwgN0d9n5H0fWi75@KVecMT29h=MV4#&-I`U{TD0u$2BOv;DWGv{(3gK@iIy z=d!qtcYF-Jv|#}ot0IUwCT$~K!o=N-F?`cuWoKZ}Nge4gw#qa9LQ4{-6L#zcYfvR0 zx*J;CT<(_NN)WEfaD1E!rIMzkQQstLl0HCzw(daK|S6p{?!@R?s`bB>(B)!cHG6y63G6b<}{y{gNA&mhKpgj zUII~Kl73DB(NDk54)AotxnDC|xav=-=Et4PS{DF-aM4Ugh8$uH{WzZIVmoAR+$ka=x332vLss4}_}&0DyE|SpRxokr-T3h`Epjf|%EmrHqMKDy4BLF`H$4Z7Csj zQ70z#ksp3%i=nk(XQ9e$R*ODi>F>(|R5ScVPo&Q3hN*I`rG}*@>d4y;T{T310r}Ju z?bRtudlJ4(-H*ECL5w<~?pVrDC%$k@Q{Un*9DgeF>7WG4HJ92MgsZjfi$jn-N4#BZ c{by2FgVP)9c-}#b>mRnto#H{xbYVsO3$o~Ewg3PC literal 1025 zcmeAS@N?(olHy`uVBq!ia0y~yU|hh!z?{Irz`(%Zuk~1hfr0sir;B4q#hf<>3>g_1 z7!ED?>YvN$uK`kk1a?R=Ffjc8|K@Nb4+8_k0fqY4+A<5au^_2nU|@K_4N+sr%fP_U zu;Aa?T3$wJCR8;Bj9@ia9JvlTFdV(`dw!I7)9$86KQ4Y?x?2);NPUYQ!~MtK9;|hr zXC7BE^SQBqUclMO(jlLpuwAv;>1awkm!YoXb^iA^C%r2E23u7dIV&_Sb82L{r8L1I zc%cJRuAs+)l>rKk+c;D%glJ4~h-MMW&=T=bc(8vbGnyxKA9A95Vh$5pU`Wrw2n-8| zM-MY{f&=6Kdg)0`Xkn3$uH?5M7bvQbz#3MFO>k3g3u>Un%?B}P)WCy%?L&-U&xTqB zSCjjY72Q|s(api)^VR5T&OYErH^&=Y&EB^R=xT(aYT*9D^m;LR=wN#N3^a7$zQXkR zff#hx^gd)pbIk!AbTybh$5eyq^8+wn!QJ!sEV|DRu%JaKMzSb?nSwiP4)}o`EPZrw jf=EwOpaGZL!3PWs8PT7PEVpxC07^cdu6{1-oD!M<4z3?y diff --git a/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png b/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png index ecdb2ce10121405cfa22222a848c0ce9c99f775b..459822e6359f512fa3654227681071c1847c59e0 100644 GIT binary patch delta 684 zcmdnPG>dJ5ay`>SPZ!6KiaBqtUu0u86ll9x`+s_#=FSe^&P>(%H9M^0alJL=LwX2o3G4Zapmuq zZRYdOKTP$T*BR#*@T;il(sXTB$7Sz+Tm1}>d{ZYgnP0W3tG>Bu6^khQbXLX;E?%jeBTUvj1@E^uC~&*^JTmHlA%ZefIx}%=GpDn+oPFPAfOiD?}=&+C6j|D@VK{qpH?*W0BeaZNHqjBLwTe<)K<4-hK2?I*Go@oVuBe30X(c| zN`4D+ft-s3J_s|Qsktqvp@F1;fq~(GKU9q&FW9PoYu7$xlt#1b4-3egw+QDz&GBb^ W-CZ|9lza7RkWrqlelF{r5}E+AP+o-q diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png index ed0e8381614585f5bbb2eac395badc97154b2740..03305c81c24482574aa134051787ecb75293f0ab 100644 GIT binary patch literal 632 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=B;6o-U3d6?5L)Fyv}5 z5OFzp=l`Xs2#$?Wj)^~w?@kh_%)Q10Qq91?kg(@*(fPM2yZ^j@ywAYk$KN$77Bi-@ zuy3C9AR$ah=8RWUbEId1fi@>!n(D!WYrc4J**R$SaC3g?99W@YOz3~74>+KaGuFmThR(+N6QujHdx?ATFGg$3|eW#zR z_8t9y{vBu6v3sW$vhr1Fw~JZ4Xx;pDj?2@O_NP# z+eN;F@#z*^h_?KqVQ~K&GuXbH8@X5&d0a03|Nr{BUXxQ#$6;&nZ8MY-^&(Q+-@mWh zDxhZ5KbN;9pHsQ<+-9Gi^C*m+@1-nenXOA`%U;IC!Z%1K6FHHgYl;@Gu|zyZs!SqDyb9t6mUDgWLUZSqv0`-ou00KF6*2UngF19_2~cr literal 562 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=E7vJzX3_D(1X7V93b8 zz;I~6SN~itdkv5RB(Ot*fq~)w|2Kylc^DWN4k*;W)|OeYjRi>s0|P?>7g)_rN3IqF z9@Y!L=UZ)_;j5|Dvf#%@TY;V#llbo*Wd`f`kQ!S0@#FuZAO0EnPcAT-S1GrP?YPLj z`t7_er%rO-*3F%&%*0(2=jSKzW90#mjy-{ob{)xEc~RPO%Yoi|ClI?nj8 z&|>Lv+eDDL?l%*hEbUB3Mj-O*Yw7q4b z^YcQrW16-c!geCdzwA&41;T=p|2}Qm&#!vUZJJulj9wkz9R}PsFXN}~%dOaZDJaJg zZ1JD|5By+9URub@z`((L@b7lFM+di@oO}4A=jGR2#&-~P$aImTWAUI@TU;`T@ zxEVPa7#JEB{9pfDkP8%xNFav=qQsCFtmNO?wGSDknUEAPFfc?jg4Dc47$ZHY38sXZ XQTxAXnkq|03P`1=tDnm{r-UW|F|^Ys diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png index bfc7882152eff8b889fff1fbf733c0dd89746c07..c17e08c39e013a569c43f4e933fd0280ecbf106a 100644 GIT binary patch literal 819 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|V19PZ!6KiaBpCY~*TC zU~mom|9^RQCTF{}2xrl`A1)WH*+6QcV8gY-ow}cY*1TTNzD_MbK*Q$e9tU}y+d>)~ zA}m}?tc^_$jtdk5DnwtfFfjc8|K`RBM1I&=X=TAH07IY1;gq;yy^i02=xzt z{yENJJi2nL!#Fw2X~k{`>#`%aiTJuei28y&{@*dZqT2X~E$ubeFFS zi00e#s7Cj_UUcU3<3;6>$qNoRG&V9Zv$8RPHM`nFEV*@%lgUxQ<>H^sW(-H;wj|me zu70qT-!*W<@{9evTldetQdHEMWx3O7>ztT{QGV+Jbk)OGOk;Nc@%!`v2WDno9utEL z2OK_JytcgXwQBf^vXi2#VvAa{rpGM21>!O?FfizSc%u32(M_4G9df!+xom#q zKmY3g5p}!Q7+!#xDNwWVYvY`~dmwJSiR8vsxh+TEf4H}i&-LP)dC#gn^8$2F@~%?d z>9|$Je8sdUY*&RC7#JR`pKEh;j!plaTa%?jUZ3P$_4Ntc)vBWAEZd!qTjhRq-esS5 z{`S23Y2PQA@|hGYXk_M<$#~%K;nKC`71@)dL%fTcvycpCVPIegvxUZ+Aul-I{;l2n zmO&aEln5XWRRfP(=}AqfN)Gs-E5VHZJFJit0=H@HLq;^44#a?sd5cif`;Zx-W;#Q_ WYUdN5JCfZ&Zu4~Yb6Mw<&;$SxGBgVS literal 741 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|V0`U|{(F|IJ}TUIqq+h6VrDo_)YC&4i?afq|ib1x-yYFC)4d z14gi#%NN;>IEXM``2YXaZvNjP$t{K^uVki`hPrI21DpB5z~hMrEN#72^g#4=c4gVlsoUyw(x+$hh)8G{1SBjt-~iTq;2J;Jl3R{k zOojq37k}Ts#jH^_gY${-J!bR08eL71uKSI%>djX^eZqCMsHio|a;MYQIWY^P{MH5N zviE)XKE1J#jZI1-BVj>flcK9)i(0clTvi4Kh8+h^1)n+f z`NUde6V$_3Oq;A7a=KCZo89I;{=ff4%!}P-c;SEpGczxbiGjeMjjx$^ex0PfrR*mp zP;VaOJZvD~a`E48KSs_q9jyO^-pf8W;OVZN#GE?m&Vr~((jneO%~_nFAl@={`HZR0 zCq`EnHD}rGblf`c&Vnfaynwap<}0o-ufOs8z^6TLf2aS{zj>^YjnAZDK_fG-3`gCq zubTH}Jz=|wWSVDQfG#M){XRir1QD~o582UUZ9NgO?~kr#?^_0RbHt%)7(sq%Snz+n m^rR-#M0CIhtmG|LC39HLvA<^sljH3>g_1 z7!ED?>YvN$uK`kk1a?R=Ffjc8|K_40*8v3qhJ*k2TWMZa5fDu|D7oR&x}9I)N*e;N zKYsfB`-ks**(_bc8h#6Q%uMFqGSQ)s=_sd&dcYhQ<-!G{fb85kHE7W`Y= z`;b|h3GPlXn8Spo#`hsRK{dP&xzW|gFrt}L%gcza#s}itD~?<(1|lv8f6uQYoh%*l`U%@r ztDTNp{qkVkxvy=YemTh5U?AXf@!#%k44v$}k8>*CTeb7nYYvAtB{5_D$Vgj@9XQ0d;X;U|IgCx&^*0KJ%UAQkBH4dr3(+1KHwB< zWYcH$-(bTAinxZ|kSM)n$aUC2fc4_<`&O~D5*LE`!XGulskG8>#Mz^Qu-)*6?s#?8K^> zA;zki5wb#o_3R=A)?O~rB?=7EGjBPnb?^TD-`ybaLa#cz%)uw8o8+Jg*^sLxK)~hT z@B6pTxCod|nCTw6WA6pC$#tE~o-!HPMh}El__kO`e&C#Rn;DWw85sUNI{3Twbo@E_ zIGef)pVz9Jueb&Z^OL-*zCK~QT2<7XWxLaH>%2P)qWtp$z|w~yY3<5E&K3s&my3Tk zn>Br0;2YWd{@}b<2E6mH_A5RA|L)DCstJ52x40Q@z z$!$Rm)UxV;Ke}tzK19!qZ=e>z%LdGR{|CKj!1Qwt)I*Itpz=nc{*8nL%0y`ub7#RNle{;Bzhk=3NfI|IiZJ7nzSddgOFfcsehNv;*Wnf@v zSnzLcEia=q6RH{mMu-|lPOzH)>!l|(p(&Zef~MrQpaz+!}56pFYa%Fg#_z^9tm+24y?@?Q%cw@A$B3 zPP*kzr>%2h7DoB43(!>$Uoma6cF5_IqN|=h;ksH>)S3m7mU-ZM?f$!alUE-2>Yw@k zqy)2MhRFp7wppnP%)M+<9j*P-_A=`f{VPkKex>>O{7Y|ylqWb`WM>u9@K$JC#LuM?FwKFyLl>H2v>?PO-+W>8$DzELw45a}oqsR74guxh{B=dXRHkwgT9R@8lryam$d4 z$x(#m;_v&n&bWw`{H#gu4o^tpw)K7J@|c-{fx+kVQRlY)`Fr;NcqrO3uiAE}4UW`b%%7@HTNo>8f-r zh~-eq*g8R>sZ6nB!BwXw4km*W*JM?8R+j(&&mq(#H%F#`h0~GWkI$khCGr9!S>6K0 zF@wv+-}iUzR`PIAZTV4~e^ax^`9i1)-wWrHZ2by_4*Gtu5Wlvgp#I^P>Hqo7_3U3% zVsja~!x_zt>dK$1t>!u|zjWuxi_kc5)&-i%Bl-< W8vT0T@TA@bxysYk&t;ucLK6VA$lsX& diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png index cf8d5afc7e5294fbbe63568eb4fbcfdba549ca52..a822c4f58a76d4becfb6991ca4974fe3c528e2d2 100644 GIT binary patch literal 443 zcmeAS@N?(olHy`uVBq!ia0y~yV7S1*z&L?}fq{YH*Mp4(3=E7-o-U3d6?5KPH{@zC z;BmNk@BgK)ZUv1UB6bh=u01=+K1~;-m?2@_<0AXGdilwF-fv|!i|76|4iWqVVnHU)U|9^AGkc&ZqgZ0Ai`&JxMYuE}k+P^Cta(LmqHVUE+1U6W^ z&tEfnXT192l6RT4mW>P6e~6pxy40|yFC|bZVB^O~1qOx(yBWde-EibOsvy98;rIPt z@(2BO%w+F3>&5W+EZEiYKJ)C%^_iEK{`Gw^A{ z&}Fgq&;Q!$inK2eMVfYAs@3J2n@}ZlbfN3Qv=32=3=9t}Q3Fj;p@NJGX(jH7JteLKzPbz&S032S#wx^#N0zBp@>A6Wmv zxxxfaXtOieJYoX-=dd9!0|P_Df`4m$AF@j`Av^&BZ_w57KIBGMQ^N={=j~xePOv%u e*Dsdm0tGiWe_Qolf1!XtkU^fVelF{r5}E+BhllY1 diff --git a/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png b/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png index 00b508dcb221d03ea7ebb416569824a41637229b..9ae5b45ff498b398202e44aa19158721e7bd2b29 100644 GIT binary patch delta 184 zcmZ1@xKVI|3S-4a)kN0%)4WWEJPwXti%q;1n`}{cRuK4Hs8n|EY)7ey`FrU<%LFQ7 zKa{r|ec;dZ&-#OZ%Yg%}54aQyG%K0}Br0YecAli-sox#_RwDPY=B;DJB|Gf-*MCc$ zCnHy#o>?bv@FBS%o|y*(_DJlgee!L)h$gGtkIy^>zmGO3ZhiGKEkQ5=3?5i*Q1P6k j;^|ewxLJ^`ok=9@hsdp;hADjv3=9mOu6{1-oD!MiGO`-N_|qZhyV=z*F(V zz6b9)CLf&7@~8a4e2&IO;e(P+4?;dLDH?n*Yj0QaoODlY`z=$eW6>o&-(P&HkrVsv zHt&sGbb98;vllpcVw!JDC4j+$tq*?n$JyRd3YGU?^Xdi<49sDirs6qC#go~VVY3)p cJCn!)p>0}mN-Y~07#J8lUHx3vIVCg!0QNpiM*si- diff --git a/src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png b/src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png index ea100d7f117b4c4c506f6b174c9cdc11e9a108ee..cf96b7d15bd266af18ffb33076d8e614b44d4018 100644 GIT binary patch delta 688 zcmV;h0#E(44!sVLBLW8mu_b#1f6WPmFc5&zl(wa9X_h8AOz(SxAA2YX0Z#(byo z`-rx{exV-zp&q7Cwo9@NNq!@7=q}?ELj?6-EJ%!1Dnjk@f1POXp5C8xG0000000017Hv<3w|NrdVK@Gz&4259`g-|Gie=rDzFbIV( z2!SvNg%SvbuxWD~+wEzGX|eQw(keOXp>I~xoXigZ0000000000000000001f@6s7N|;~>Xur9S*kf#phX9JyB0}+qGd@H zsA?eBqR^n;qW?VKw{A9ie|_zXpXa~ZO>3S&({6$GNmrktYIo%bwEE%ry+xr2w~fyJ z`p}d^gIps=XD>&1ZH^7HS8Z@^`wAOd*z!b85vUvkwNFp@D2XLQAb$i1hl6v2^guDT zaty@&=?IKaME=^T z1IUD@BKarr#gvwZN&NbR1br|Y%?!4e4i}53!b^g}+n%Ju#iC&thG7^Vp&w2To5lhj}Kf`HF&E4kukLN>*0`*dB+g{a$l88E@f_8Ri>+Z(zdJTn#5(Deitq zi~c=cP-#$ZQ4}-{e`(P+;RVe!oSciGpVd8h;fq|>XMYntn7u5VNa$7)^v6Rn4foT* z1uZa9>)X|(p{__6!d{m4rDp%rK=w=+!n8?~ zqnS_|$W(*T4=1^Y&_I0$jD9%DU7iN&3}N)cN#?^fFz*hdA|Fn&J0%U@_tOuW-S%i_ce-*z) z!~)y9HXL0W&ThLe$vC8VM`1{par|8zmJZ5aEJ%= z2%9>`vE80_m=;U_pR`JedgwQ+sZZty00000000000000000000-}l!qN{0<^$6`x2 zdU)#>r4b}*wm|LvQk^L^@Oqu1BI`F{Spf8Dg^2{i2%XrFZV8CLDC9D!Cp9KV++EW&N0v%fwRl50HHa!7DyY4v6X8eY2U5| z(w1*%M;rYD00960?VZsL!Y~X(*?e*&=ER9N9hQRe^5k|IC0_tGT~Gt zzt2!w9VYSlghVOG8|+28TwFX9Y|vpN&>&qdE*gem7>4l^+Tmokj*M;X!RoC(oLsZT zZk`EizPun8!%3GGC9A6eY>)S+o$oQ6^do$oVSWL4fZ3nsQaI^oaa+2S=(^wqQGsHK zyr7{=iPixxXw1XOe~|>8X7}QaA99=D{i||kHrhCzy;?!gg@Z&OZ&=nF&MaHlcGA9ZEyi)nK&4 zN$w#u Date: Tue, 1 Jul 2025 15:04:21 -0600 Subject: [PATCH 093/114] font/sprite: introduce `Fraction` enum for cell fractions I've included a compatibility test here to make sure that the numbers from this are in line with the numbers produced by xHalfs, yThirds, etc. After this commit I'll introduce a helper function that fills based on a span specified with this enum to replace any uses of xHalfs and friends. Once I do that I'll remove them and the compatibility test, this should be a much cleaner interface for this and make it easier to consistently align block elements with each other. --- src/font/sprite/draw/common.zig | 156 ++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/font/sprite/draw/common.zig b/src/font/sprite/draw/common.zig index d10128cdf..ad9788b94 100644 --- a/src/font/sprite/draw/common.zig +++ b/src/font/sprite/draw/common.zig @@ -122,6 +122,162 @@ pub const Alignment = struct { pub const bottom_right = lower_right; }; +/// A value that indicates some fraction across +/// the cell either horizontally or vertically. +/// +/// This has some redundant names in it so that you can +/// use whichever one feels most semantically appropriate. +pub const Fraction = enum { + // Names for the min edge + start, + left, + top, + zero, + + // Names based on eighths + one_eighth, + two_eighths, + three_eighths, + four_eighths, + five_eighths, + six_eighths, + seven_eighths, + + // Names based on quarters + one_quarter, + two_quarters, + three_quarters, + + // Names based on thirds + one_third, + two_thirds, + + // Names based on halves + one_half, + half, + + // Alternative names for 1/2 + center, + middle, + + // Names for the max edge + end, + right, + bottom, + one, + full, + + /// Get the x position for this fraction across a particular + /// size (width or height), assuming it will be used as the + /// min (left/top) coordinate for a block. + /// + /// `size` can be any integer type, since it will be coerced + pub inline fn min(self: Fraction, size: anytype) i32 { + const s: f64 = @as(f64, @floatFromInt(size)); + // For min coordinates, we want to align with the complementary + // fraction taken from the end, this ensures that rounding evens + // out, so that for example, if `size` is `7`, and we're looking + // at the `half` line, `size - round((1 - 0.5) * size)` => `3`; + // whereas the max coordinate directly rounds, which means that + // both `start` -> `half` and `half` -> `end` will be 4px, from + // `0` -> `4` and `3` -> `7`. + return @intFromFloat(s - @round((1.0 - self.fraction()) * s)); + } + + /// Get the x position for this fraction across a particular + /// size (width or height), assuming it will be used as the + /// max (right/bottom) coordinate for a block. + /// + /// `size` can be any integer type, since it will be coerced + /// with `@floatFromInt`. + pub inline fn max(self: Fraction, size: anytype) i32 { + const s: f64 = @as(f64, @floatFromInt(size)); + // See explanation of why these are different in `min`. + return @intFromFloat(@round(self.fraction() * s)); + } + + pub inline fn fraction(self: Fraction) f64 { + return switch (self) { + .start, + .left, + .top, + .zero, + => 0.0, + + .one_eighth, + => 0.125, + + .one_quarter, + .two_eighths, + => 0.25, + + .one_third, + => 1.0 / 3.0, + + .three_eighths, + => 0.375, + + .one_half, + .two_quarters, + .four_eighths, + .half, + .center, + .middle, + => 0.5, + + .five_eighths, + => 0.625, + + .two_thirds, + => 2.0 / 3.0, + + .three_quarters, + .six_eighths, + => 0.75, + + .seven_eighths, + => 0.875, + + .end, + .right, + .bottom, + .one, + .full, + => 1.0, + }; + } +}; + +test "sprite font fraction" { + const testing = std.testing; + + for (4..64) |s| { + const metrics: font.Metrics = .calc(.{ + .cell_width = @floatFromInt(s), + .ascent = @floatFromInt(s), + .descent = 0.0, + .line_gap = 0.0, + .underline_thickness = 2.0, + .strikethrough_thickness = 2.0, + }); + + try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[0])), Fraction.half.max(s)); + try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[1])), Fraction.half.min(s)); + + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[0])), Fraction.one_third.max(s)); + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[1])), Fraction.one_third.min(s)); + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[2])), Fraction.two_thirds.max(s)); + try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[3])), Fraction.two_thirds.min(s)); + + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[0])), Fraction.one_quarter.max(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[1])), Fraction.one_quarter.min(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[2])), Fraction.two_quarters.max(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[3])), Fraction.two_quarters.min(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[4])), Fraction.three_quarters.max(s)); + try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[5])), Fraction.three_quarters.min(s)); + } +} + /// Fill a rect, clamped to within the cell boundaries. /// /// TODO: Eliminate usages of this, prefer `canvas.box`. From 190c744a6fb547f9ca3b0424f549b258ac0d2617 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 1 Jul 2025 15:20:19 -0500 Subject: [PATCH 094/114] linux: add install target to systemd user service This will allow users to enable Ghostty startup on login. Users will need to explicitly enable startup on login via this command: ```sh systemctl enable --user com.mitchellh.ghostty.service ``` --- dist/linux/systemd.service.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dist/linux/systemd.service.in b/dist/linux/systemd.service.in index b0ef3d59a..3ff848ddd 100644 --- a/dist/linux/systemd.service.in +++ b/dist/linux/systemd.service.in @@ -1,7 +1,11 @@ [Unit] Description=@NAME@ +After=graphical-session.target [Service] Type=dbus BusName=@APPID@ ExecStart=@GHOSTTY@ --launched-from=systemd + +[Install] +WantedBy=graphical-session.target From c838d3d7d251f1420d7bdaa1c6428caf9f6416d6 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 16:00:35 -0600 Subject: [PATCH 095/114] font/sprite: remove `yHalfs` and friends, use `Fraction` Introduces `fill`, which fills between two `Fraction`s, use this instead of `yHalfs` and friends wherever they're used, which also means we can remove `rect`. This commit does change alignment of the vertical/horizontal eighths in certain cell sizes, but the change is for the better IMO. Also changes the center-point alignment of smooth mosaics for odd cell widths, but the change is no more than half a pixel at worst and is probably an improvement ultimately. --- src/font/sprite/draw/block.zig | 15 +- src/font/sprite/draw/box.zig | 15 -- src/font/sprite/draw/common.zig | 168 +++++++----------- .../draw/symbols_for_legacy_computing.zig | 66 +++---- ...ymbols_for_legacy_computing_supplement.zig | 22 +-- typos.toml | 2 - 6 files changed, 108 insertions(+), 180 deletions(-) diff --git a/src/font/sprite/draw/block.zig b/src/font/sprite/draw/block.zig index f7faacea7..571f25a79 100644 --- a/src/font/sprite/draw/block.zig +++ b/src/font/sprite/draw/block.zig @@ -15,9 +15,7 @@ const common = @import("common.zig"); const Shade = common.Shade; const Quads = common.Quads; const Alignment = common.Alignment; -const xHalfs = common.xHalfs; -const yHalfs = common.yHalfs; -const rect = common.rect; +const fill = common.fill; const font = @import("../../main.zig"); const Sprite = @import("../../sprite.zig").Sprite; @@ -176,11 +174,8 @@ fn quadrant( canvas: *font.sprite.Canvas, comptime quads: Quads, ) void { - const x_halfs = xHalfs(metrics); - const y_halfs = yHalfs(metrics); - - if (quads.tl) rect(metrics, canvas, 0, 0, x_halfs[0], y_halfs[0]); - if (quads.tr) rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_halfs[0]); - if (quads.bl) rect(metrics, canvas, 0, y_halfs[1], x_halfs[0], metrics.cell_height); - if (quads.br) rect(metrics, canvas, x_halfs[1], y_halfs[1], metrics.cell_width, metrics.cell_height); + if (quads.tl) fill(metrics, canvas, .zero, .half, .zero, .half); + if (quads.tr) fill(metrics, canvas, .half, .full, .zero, .half); + if (quads.bl) fill(metrics, canvas, .zero, .half, .half, .full); + if (quads.br) fill(metrics, canvas, .half, .full, .half, .full); } diff --git a/src/font/sprite/draw/box.zig b/src/font/sprite/draw/box.zig index 91d78d2b2..f14e5a3f9 100644 --- a/src/font/sprite/draw/box.zig +++ b/src/font/sprite/draw/box.zig @@ -24,7 +24,6 @@ const Quads = common.Quads; const Corner = common.Corner; const Edge = common.Edge; const Alignment = common.Alignment; -const rect = common.rect; const hline = common.hline; const vline = common.vline; const hlineMiddle = common.hlineMiddle; @@ -695,20 +694,6 @@ pub fn lightDiagonalCross( lightDiagonalUpperLeftToLowerRight(metrics, canvas); } -fn quadrant( - metrics: font.Metrics, - canvas: *font.sprite.Canvas, - comptime quads: Quads, -) void { - const center_x = metrics.cell_width / 2 + metrics.cell_width % 2; - const center_y = metrics.cell_height / 2 + metrics.cell_height % 2; - - if (quads.tl) rect(metrics, canvas, 0, 0, center_x, center_y); - if (quads.tr) rect(metrics, canvas, center_x, 0, metrics.cell_width, center_y); - if (quads.bl) rect(metrics, canvas, 0, center_y, center_x, metrics.cell_height); - if (quads.br) rect(metrics, canvas, center_x, center_y, metrics.cell_width, metrics.cell_height); -} - pub fn arc( metrics: font.Metrics, canvas: *font.sprite.Canvas, diff --git a/src/font/sprite/draw/common.zig b/src/font/sprite/draw/common.zig index ad9788b94..67b9dc778 100644 --- a/src/font/sprite/draw/common.zig +++ b/src/font/sprite/draw/common.zig @@ -135,6 +135,7 @@ pub const Fraction = enum { zero, // Names based on eighths + eighth, one_eighth, two_eighths, three_eighths, @@ -144,17 +145,19 @@ pub const Fraction = enum { seven_eighths, // Names based on quarters + quarter, one_quarter, two_quarters, three_quarters, // Names based on thirds + third, one_third, two_thirds, // Names based on halves - one_half, half, + one_half, // Alternative names for 1/2 center, @@ -167,6 +170,43 @@ pub const Fraction = enum { one, full, + /// This can be indexed to get the fraction for `i/8`. + pub const eighths: [9]Fraction = .{ + .zero, + .one_eighth, + .two_eighths, + .three_eighths, + .four_eighths, + .five_eighths, + .six_eighths, + .seven_eighths, + .one, + }; + + /// This can be indexed to get the fraction for `i/4`. + pub const quarters: [5]Fraction = .{ + .zero, + .one_quarter, + .two_quarters, + .three_quarters, + .one, + }; + + /// This can be indexed to get the fraction for `i/3`. + pub const thirds: [4]Fraction = .{ + .zero, + .one_third, + .two_thirds, + .one, + }; + + /// This can be indexed to get the fraction for `i/2`. + pub const halves: [3]Fraction = .{ + .zero, + .one_half, + .one, + }; + /// Get the x position for this fraction across a particular /// size (width or height), assuming it will be used as the /// min (left/top) coordinate for a block. @@ -196,6 +236,19 @@ pub const Fraction = enum { return @intFromFloat(@round(self.fraction() * s)); } + /// Get this fraction across a particular size (width/height). + /// If you need an integer, use `min` or `max` instead, since + /// they contain special logic for consistent alignment. This + /// is for when you're drawing with paths and don't care about + /// pixel alignment. + /// + /// `size` can be any integer type, since it will be coerced + /// with `@floatFromInt`. + pub inline fn float(self: Fraction, size: anytype) f64 { + return self.fraction() * @as(f64, @floatFromInt(size)); + } + + /// Get a float for the fraction this represents. pub inline fn fraction(self: Fraction) f64 { return switch (self) { .start, @@ -204,23 +257,26 @@ pub const Fraction = enum { .zero, => 0.0, + .eighth, .one_eighth, => 0.125, + .quarter, .one_quarter, .two_eighths, => 0.25, + .third, .one_third, => 1.0 / 3.0, .three_eighths, => 0.375, + .half, .one_half, .two_quarters, .four_eighths, - .half, .center, .middle, => 0.5, @@ -248,52 +304,21 @@ pub const Fraction = enum { } }; -test "sprite font fraction" { - const testing = std.testing; - - for (4..64) |s| { - const metrics: font.Metrics = .calc(.{ - .cell_width = @floatFromInt(s), - .ascent = @floatFromInt(s), - .descent = 0.0, - .line_gap = 0.0, - .underline_thickness = 2.0, - .strikethrough_thickness = 2.0, - }); - - try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[0])), Fraction.half.max(s)); - try testing.expectEqual(@as(i32, @intCast(xHalfs(metrics)[1])), Fraction.half.min(s)); - - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[0])), Fraction.one_third.max(s)); - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[1])), Fraction.one_third.min(s)); - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[2])), Fraction.two_thirds.max(s)); - try testing.expectEqual(@as(i32, @intCast(yThirds(metrics)[3])), Fraction.two_thirds.min(s)); - - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[0])), Fraction.one_quarter.max(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[1])), Fraction.one_quarter.min(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[2])), Fraction.two_quarters.max(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[3])), Fraction.two_quarters.min(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[4])), Fraction.three_quarters.max(s)); - try testing.expectEqual(@as(i32, @intCast(yQuads(metrics)[5])), Fraction.three_quarters.min(s)); - } -} - -/// Fill a rect, clamped to within the cell boundaries. -/// -/// TODO: Eliminate usages of this, prefer `canvas.box`. -pub fn rect( +/// Fill a section of the cell, specified by a +/// horizontal and vertical pair of fraction lines. +pub fn fill( metrics: font.Metrics, canvas: *font.sprite.Canvas, - x1: u32, - y1: u32, - x2: u32, - y2: u32, + x0: Fraction, + x1: Fraction, + y0: Fraction, + y1: Fraction, ) void { canvas.box( - @intCast(@min(@max(x1, 0), metrics.cell_width)), - @intCast(@min(@max(y1, 0), metrics.cell_height)), - @intCast(@min(@max(x2, 0), metrics.cell_width)), - @intCast(@min(@max(y2, 0), metrics.cell_height)), + x0.min(metrics.cell_width), + y0.min(metrics.cell_height), + x1.max(metrics.cell_width), + y1.max(metrics.cell_height), .on, ); } @@ -351,58 +376,3 @@ pub fn hline( ) void { canvas.box(x1, y, x2, y + @as(i32, @intCast(thickness_px)), .on); } - -/// xHalfs[0] should be used as the right edge of a left-aligned half. -/// xHalfs[1] should be used as the left edge of a right-aligned half. -pub fn xHalfs(metrics: font.Metrics) [2]u32 { - const float_width: f64 = @floatFromInt(metrics.cell_width); - const half_width: u32 = @intFromFloat(@round(0.5 * float_width)); - return .{ half_width, metrics.cell_width - half_width }; -} - -/// yHalfs[0] should be used as the bottom edge of a top-aligned half. -/// yHalfs[1] should be used as the top edge of a bottom-aligned half. -pub fn yHalfs(metrics: font.Metrics) [2]u32 { - const float_height: f64 = @floatFromInt(metrics.cell_height); - const half_height: u32 = @intFromFloat(@round(0.5 * float_height)); - return .{ half_height, metrics.cell_height - half_height }; -} - -/// Use these values as such: -/// yThirds[0] bottom edge of the first third. -/// yThirds[1] top edge of the second third. -/// yThirds[2] bottom edge of the second third. -/// yThirds[3] top edge of the final third. -pub fn yThirds(metrics: font.Metrics) [4]u32 { - const float_height: f64 = @floatFromInt(metrics.cell_height); - const one_third_height: u32 = @intFromFloat(@round(one_third * float_height)); - const two_thirds_height: u32 = @intFromFloat(@round(two_thirds * float_height)); - return .{ - one_third_height, - metrics.cell_height - two_thirds_height, - two_thirds_height, - metrics.cell_height - one_third_height, - }; -} - -/// Use these values as such: -/// yQuads[0] bottom edge of first quarter. -/// yQuads[1] top edge of second quarter. -/// yQuads[2] bottom edge of second quarter. -/// yQuads[3] top edge of third quarter. -/// yQuads[4] bottom edge of third quarter -/// yQuads[5] top edge of fourth quarter. -pub fn yQuads(metrics: font.Metrics) [6]u32 { - const float_height: f64 = @floatFromInt(metrics.cell_height); - const quarter_height: u32 = @intFromFloat(@round(0.25 * float_height)); - const half_height: u32 = @intFromFloat(@round(0.50 * float_height)); - const three_quarters_height: u32 = @intFromFloat(@round(0.75 * float_height)); - return .{ - quarter_height, - metrics.cell_height - three_quarters_height, - half_height, - metrics.cell_height - half_height, - three_quarters_height, - metrics.cell_height - quarter_height, - }; -} diff --git a/src/font/sprite/draw/symbols_for_legacy_computing.zig b/src/font/sprite/draw/symbols_for_legacy_computing.zig index a17ddb494..19e62cf4b 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing.zig @@ -28,13 +28,12 @@ const z2d = @import("z2d"); const common = @import("common.zig"); const Thickness = common.Thickness; const Alignment = common.Alignment; +const Fraction = common.Fraction; const Corner = common.Corner; const Quads = common.Quads; const Edge = common.Edge; const Shade = common.Shade; -const xHalfs = common.xHalfs; -const yThirds = common.yThirds; -const rect = common.rect; +const fill = common.fill; const box = @import("box.zig"); const block = @import("block.zig"); @@ -121,16 +120,12 @@ pub fn draw1FB00_1FB3B( const sex: Sextants = @bitCast(@as(u6, @intCast( idx + (idx / 0x14) + 1, ))); - - const x_halfs = xHalfs(metrics); - const y_thirds = yThirds(metrics); - - if (sex.tl) rect(metrics, canvas, 0, 0, x_halfs[0], y_thirds[0]); - if (sex.tr) rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_thirds[0]); - if (sex.ml) rect(metrics, canvas, 0, y_thirds[1], x_halfs[0], y_thirds[2]); - if (sex.mr) rect(metrics, canvas, x_halfs[1], y_thirds[1], metrics.cell_width, y_thirds[2]); - if (sex.bl) rect(metrics, canvas, 0, y_thirds[3], x_halfs[0], metrics.cell_height); - if (sex.br) rect(metrics, canvas, x_halfs[1], y_thirds[3], metrics.cell_width, metrics.cell_height); + if (sex.tl) fill(metrics, canvas, .zero, .half, .zero, .one_third); + if (sex.tr) fill(metrics, canvas, .half, .full, .zero, .one_third); + if (sex.ml) fill(metrics, canvas, .zero, .half, .one_third, .two_thirds); + if (sex.mr) fill(metrics, canvas, .half, .full, .one_third, .two_thirds); + if (sex.bl) fill(metrics, canvas, .zero, .half, .two_thirds, .end); + if (sex.br) fill(metrics, canvas, .half, .full, .two_thirds, .end); } /// Smooth Mosaics @@ -465,17 +460,12 @@ pub fn draw1FB3C_1FB67( else => unreachable, }; - const y_thirds = yThirds(metrics); const top: f64 = 0.0; - // We average the edge positions for the y_thirds boundaries here - // rather than having to deal with varying alignments depending on - // the surrounding pieces. The most this will be off by is half of - // a pixel, so hopefully it's not noticeable. - const upper: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[0])) + @as(f64, @floatFromInt(y_thirds[1]))); - const lower: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[2])) + @as(f64, @floatFromInt(y_thirds[3]))); + const upper: f64 = Fraction.one_third.float(metrics.cell_height); + const lower: f64 = Fraction.two_thirds.float(metrics.cell_height); const bottom: f64 = @floatFromInt(metrics.cell_height); const left: f64 = 0.0; - const center: f64 = @round(@as(f64, @floatFromInt(metrics.cell_width)) / 2); + const center: f64 = Fraction.half.float(metrics.cell_width); const right: f64 = @floatFromInt(metrics.cell_width); var path = canvas.staticPath(12); // nodes.len = 0 @@ -571,13 +561,14 @@ pub fn draw1FB70_1FB75( const n = cp + 1 - 0x1fb70; - const x: u32 = @intFromFloat( - @round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(metrics.cell_width)) / 8), + fill( + metrics, + canvas, + Fraction.eighths[n], + Fraction.eighths[n + 1], + .top, + .bottom, ); - const w: u32 = @intFromFloat( - @round(@as(f64, @floatFromInt(metrics.cell_width)) / 8), - ); - rect(metrics, canvas, x, 0, x + w, metrics.cell_height); } /// Horizontal one eighth blocks @@ -593,21 +584,14 @@ pub fn draw1FB76_1FB7B( const n = cp + 1 - 0x1fb76; - const h = @as( - u32, - @intFromFloat(@round(@as(f64, @floatFromInt(metrics.cell_height)) / 8)), + fill( + metrics, + canvas, + .left, + .right, + Fraction.eighths[n], + Fraction.eighths[n + 1], ); - const y = @min( - metrics.cell_height -| h, - @as( - u32, - @intFromFloat( - @round(@as(f64, @floatFromInt(n)) * - @as(f64, @floatFromInt(metrics.cell_height)) / 8), - ), - ), - ); - rect(metrics, canvas, 0, y, metrics.cell_width, y + h); } pub fn draw1FB7C_1FB97( diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 9ae92cc72..fd193a0d5 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -57,9 +57,7 @@ const common = @import("common.zig"); const Thickness = common.Thickness; const Corner = common.Corner; const Shade = common.Shade; -const xHalfs = common.xHalfs; -const yQuads = common.yQuads; -const rect = common.rect; +const fill = common.fill; const box = @import("box.zig"); @@ -122,17 +120,15 @@ pub fn draw1CD00_1CDE5( break :octants result; }; - const x_halfs = xHalfs(metrics); - const y_quads = yQuads(metrics); const oct = octants[cp - octant_min]; - if (oct.@"1") rect(metrics, canvas, 0, 0, x_halfs[0], y_quads[0]); - if (oct.@"2") rect(metrics, canvas, x_halfs[1], 0, metrics.cell_width, y_quads[0]); - if (oct.@"3") rect(metrics, canvas, 0, y_quads[1], x_halfs[0], y_quads[2]); - if (oct.@"4") rect(metrics, canvas, x_halfs[1], y_quads[1], metrics.cell_width, y_quads[2]); - if (oct.@"5") rect(metrics, canvas, 0, y_quads[3], x_halfs[0], y_quads[4]); - if (oct.@"6") rect(metrics, canvas, x_halfs[1], y_quads[3], metrics.cell_width, y_quads[4]); - if (oct.@"7") rect(metrics, canvas, 0, y_quads[5], x_halfs[0], metrics.cell_height); - if (oct.@"8") rect(metrics, canvas, x_halfs[1], y_quads[5], metrics.cell_width, metrics.cell_height); + if (oct.@"1") fill(metrics, canvas, .zero, .half, .zero, .one_quarter); + if (oct.@"2") fill(metrics, canvas, .half, .full, .zero, .one_quarter); + if (oct.@"3") fill(metrics, canvas, .zero, .half, .one_quarter, .two_quarters); + if (oct.@"4") fill(metrics, canvas, .half, .full, .one_quarter, .two_quarters); + if (oct.@"5") fill(metrics, canvas, .zero, .half, .two_quarters, .three_quarters); + if (oct.@"6") fill(metrics, canvas, .half, .full, .two_quarters, .three_quarters); + if (oct.@"7") fill(metrics, canvas, .zero, .half, .three_quarters, .end); + if (oct.@"8") fill(metrics, canvas, .half, .full, .three_quarters, .end); } // Separated Block Quadrants diff --git a/typos.toml b/typos.toml index a8b296755..1fb54ecc6 100644 --- a/typos.toml +++ b/typos.toml @@ -39,8 +39,6 @@ extend-ignore-re = [ [default.extend-words] Pn = "Pn" thr = "thr" -# Should be "halves", but for now skip it as it would make diff huge -halfs = "halfs" # Swift oddities Requestor = "Requestor" iterm = "iterm" From ffe06f1ccdc82a4f0effa13729ab066640537615 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 16:26:57 -0600 Subject: [PATCH 096/114] font/sprite: add sixteenth blocks from slfc supplement --- ...ymbols_for_legacy_computing_supplement.zig | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index fd193a0d5..40c330d2c 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -55,6 +55,7 @@ const z2d = @import("z2d"); const common = @import("common.zig"); const Thickness = common.Thickness; +const Fraction = common.Fraction; const Corner = common.Corner; const Shade = common.Shade; const fill = common.fill; @@ -399,6 +400,88 @@ pub fn draw1CE51_1CE8F( ); } +/// Sixteenth Blocks +pub fn draw1CE90_1CEAF( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = width; + _ = height; + const q = Fraction.quarters; + switch (cp) { + // 𜺐 UPPER LEFT ONE SIXTEENTH BLOCK + 0x1CE90 => fill(metrics, canvas, q[0], q[1], q[0], q[1]), + // 𜺑 UPPER CENTRE LEFT ONE SIXTEENTH BLOCK + 0x1CE91 => fill(metrics, canvas, q[1], q[2], q[0], q[1]), + // 𜺒 UPPER CENTRE RIGHT ONE SIXTEENTH BLOCK + 0x1CE92 => fill(metrics, canvas, q[2], q[3], q[0], q[1]), + // 𜺓 UPPER RIGHT ONE SIXTEENTH BLOCK + 0x1CE93 => fill(metrics, canvas, q[3], q[4], q[0], q[1]), + // 𜺔 UPPER MIDDLE LEFT ONE SIXTEENTH BLOCK + 0x1CE94 => fill(metrics, canvas, q[0], q[1], q[1], q[2]), + // 𜺕 UPPER MIDDLE CENTRE LEFT ONE SIXTEENTH BLOCK + 0x1CE95 => fill(metrics, canvas, q[1], q[2], q[1], q[2]), + // 𜺖 UPPER MIDDLE CENTRE RIGHT ONE SIXTEENTH BLOCK + 0x1CE96 => fill(metrics, canvas, q[2], q[3], q[1], q[2]), + // 𜺗 UPPER MIDDLE RIGHT ONE SIXTEENTH BLOCK + 0x1CE97 => fill(metrics, canvas, q[3], q[4], q[1], q[2]), + // 𜺘 LOWER MIDDLE LEFT ONE SIXTEENTH BLOCK + 0x1CE98 => fill(metrics, canvas, q[0], q[1], q[2], q[3]), + // 𜺙 LOWER MIDDLE CENTRE LEFT ONE SIXTEENTH BLOCK + 0x1CE99 => fill(metrics, canvas, q[1], q[2], q[2], q[3]), + // 𜺚 LOWER MIDDLE CENTRE RIGHT ONE SIXTEENTH BLOCK + 0x1CE9A => fill(metrics, canvas, q[2], q[3], q[2], q[3]), + // 𜺛 LOWER MIDDLE RIGHT ONE SIXTEENTH BLOCK + 0x1CE9B => fill(metrics, canvas, q[3], q[4], q[2], q[3]), + // 𜺜 LOWER LEFT ONE SIXTEENTH BLOCK + 0x1CE9C => fill(metrics, canvas, q[0], q[1], q[3], q[4]), + // 𜺝 LOWER CENTRE LEFT ONE SIXTEENTH BLOCK + 0x1CE9D => fill(metrics, canvas, q[1], q[2], q[3], q[4]), + // 𜺞 LOWER CENTRE RIGHT ONE SIXTEENTH BLOCK + 0x1CE9E => fill(metrics, canvas, q[2], q[3], q[3], q[4]), + // 𜺟 LOWER RIGHT ONE SIXTEENTH BLOCK + 0x1CE9F => fill(metrics, canvas, q[3], q[4], q[3], q[4]), + + // 𜺠 RIGHT HALF LOWER ONE QUARTER BLOCK + 0x1CEA0 => fill(metrics, canvas, q[2], q[4], q[3], q[4]), + // 𜺡 RIGHT THREE QUARTERS LOWER ONE QUARTER BLOCK + 0x1CEA1 => fill(metrics, canvas, q[1], q[4], q[3], q[4]), + // 𜺢 LEFT THREE QUARTERS LOWER ONE QUARTER BLOCK + 0x1CEA2 => fill(metrics, canvas, q[0], q[3], q[3], q[4]), + // 𜺣 LEFT HALF LOWER ONE QUARTER BLOCK + 0x1CEA3 => fill(metrics, canvas, q[0], q[2], q[3], q[4]), + // 𜺤 LOWER HALF LEFT ONE QUARTER BLOCK + 0x1CEA4 => fill(metrics, canvas, q[0], q[1], q[2], q[4]), + // 𜺥 LOWER THREE QUARTERS LEFT ONE QUARTER BLOCK + 0x1CEA5 => fill(metrics, canvas, q[0], q[1], q[1], q[4]), + // 𜺦 UPPER THREE QUARTERS LEFT ONE QUARTER BLOCK + 0x1CEA6 => fill(metrics, canvas, q[0], q[1], q[0], q[3]), + // 𜺧 UPPER HALF LEFT ONE QUARTER BLOCK + 0x1CEA7 => fill(metrics, canvas, q[0], q[1], q[0], q[2]), + // 𜺨 LEFT HALF UPPER ONE QUARTER BLOCK + 0x1CEA8 => fill(metrics, canvas, q[0], q[2], q[0], q[1]), + // 𜺩 LEFT THREE QUARTERS UPPER ONE QUARTER BLOCK + 0x1CEA9 => fill(metrics, canvas, q[0], q[3], q[0], q[1]), + // 𜺪 RIGHT THREE QUARTERS UPPER ONE QUARTER BLOCK + 0x1CEAA => fill(metrics, canvas, q[1], q[4], q[0], q[1]), + // 𜺫 RIGHT HALF UPPER ONE QUARTER BLOCK + 0x1CEAB => fill(metrics, canvas, q[2], q[4], q[0], q[1]), + // 𜺬 UPPER HALF RIGHT ONE QUARTER BLOCK + 0x1CEAC => fill(metrics, canvas, q[3], q[4], q[0], q[2]), + // 𜺭 UPPER THREE QUARTERS RIGHT ONE QUARTER BLOCK + 0x1CEAD => fill(metrics, canvas, q[3], q[4], q[0], q[3]), + // 𜺮 LOWER THREE QUARTERS RIGHT ONE QUARTER BLOCK + 0x1CEAE => fill(metrics, canvas, q[3], q[4], q[1], q[4]), + // 𜺯 LOWER HALF RIGHT ONE QUARTER BLOCK + 0x1CEAF => fill(metrics, canvas, q[3], q[4], q[2], q[4]), + + else => unreachable, + } +} + fn circlePiece( canvas: *font.sprite.Canvas, width: u32, From 2fa4fc89027be57890e50973fff01a50f956b8b9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 1 Jul 2025 14:58:39 -0700 Subject: [PATCH 097/114] reload configuration on SIGUSR2 This is done at the apprt-level for a couple reasons. (1) For libghostty, we don't have a way to know what the embedding application is doing, so its risky to create signal handlers that might overwrite the application's signal handlers. (2) It's extremely messy to deal with signals and multi-threading. Apprts have framework access that handles this for us. For GTK, we use g_unix_signal_add. For macOS, we use `DispatchSource.makeSignalSource`. This is an awkward API but made for this purpose. --- macos/Sources/App/macOS/AppDelegate.swift | 34 +++++++++++++++++++++++ src/apprt/gtk/App.zig | 23 +++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 734fcbc20..418005927 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -112,6 +112,9 @@ class AppDelegate: NSObject, /// The observer for the app appearance. private var appearanceObserver: NSKeyValueObservation? = nil + /// Signals + private var signals: [DispatchSourceSignal] = [] + /// The custom app icon image that is currently in use. @Published private(set) var appIcon: NSImage? = nil { didSet { @@ -249,6 +252,9 @@ class AppDelegate: NSObject, // Setup our menu setupMenuImages() + + // Setup signal handlers + setupSignals() } func applicationDidBecomeActive(_ notification: Notification) { @@ -406,6 +412,34 @@ class AppDelegate: NSObject, return dockMenu } + /// Setup signal handlers + private func setupSignals() { + // Register a signal handler for config reloading. It appears that all + // of this is required. I've commented each line because its a bit unclear. + // Warning: signal handlers don't work when run via Xcode. They have to be + // run on a real app bundle. + + // We need to ignore signals we register with makeSignalSource or they + // don't seem to handle. + signal(SIGUSR2, SIG_IGN) + + // Make the signal source and register our event handle. We keep a weak + // ref to ourself so we don't create a retain cycle. + let sigusr2 = DispatchSource.makeSignalSource(signal: SIGUSR2, queue: .main) + sigusr2.setEventHandler { [weak self] in + guard let self else { return } + Ghostty.logger.info("reloading configuration in response to SIGUSR2") + self.ghostty.reloadConfig() + } + + // The signal source starts unactivated, so we have to resume it once + // we setup the event handler. + sigusr2.resume() + + // We need to keep a strong reference to it so it isn't disabled. + signals.append(sigusr2) + } + /// Setup all the images for our menu items. private func setupMenuImages() { // Note: This COULD Be done all in the xib file, but I find it easier to diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 7786f976a..c61254fbd 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -373,6 +373,13 @@ pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void { .{}, ); + // Setup a listener for SIGUSR2 to reload the configuration. + _ = glib.unixSignalAdd( + std.posix.SIG.USR2, + sigusr2, + self, + ); + // We don't use g_application_run, we want to manually control the // loop so we have to do the same things the run function does: // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 @@ -1508,6 +1515,22 @@ pub fn quitNow(self: *App) void { self.running = false; } +// SIGUSR2 signal handler via g_unix_signal_add +fn sigusr2(ud: ?*anyopaque) callconv(.c) c_int { + const self: *App = @ptrCast(@alignCast(ud orelse + return @intFromBool(glib.SOURCE_CONTINUE))); + + log.info("received SIGUSR2, reloading configuration", .{}); + self.reloadConfig(.app, .{ .soft = false }) catch |err| { + log.err( + "error reloading configuration for SIGUSR2: {}", + .{err}, + ); + }; + + return @intFromBool(glib.SOURCE_CONTINUE); +} + /// This is called by the `activate` signal. This is sent on program startup and /// also when a secondary instance launches and requests a new window. fn gtkActivate(_: *adw.Application, core_app: *CoreApp) callconv(.c) void { From 0cd95a791f15dfc17d257c5d40b92a1fe1a26ec9 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 16:52:17 -0600 Subject: [PATCH 098/114] font/sprite: add sflc supplement circle/ellipse glyphs --- .../draw/symbols_for_legacy_computing.zig | 2 +- ...ymbols_for_legacy_computing_supplement.zig | 129 +++++++++++++----- 2 files changed, 93 insertions(+), 38 deletions(-) diff --git a/src/font/sprite/draw/symbols_for_legacy_computing.zig b/src/font/sprite/draw/symbols_for_legacy_computing.zig index 19e62cf4b..164aa1ac3 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing.zig @@ -1367,7 +1367,7 @@ fn checkerboardFill( } } -fn circle( +pub fn circle( metrics: font.Metrics, canvas: *font.sprite.Canvas, comptime position: Alignment, diff --git a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig index 40c330d2c..f43949eb9 100644 --- a/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -61,6 +61,7 @@ const Shade = common.Shade; const fill = common.fill; const box = @import("box.zig"); +const sflc = @import("symbols_for_legacy_computing.zig"); const font = @import("../../main.zig"); @@ -210,37 +211,37 @@ pub fn draw1CC30_1CC3F( ) !void { switch (cp) { // 𜰰 UPPER LEFT TWELFTH CIRCLE - 0x1CC30 => try circlePiece(canvas, width, height, metrics, 0, 0, 2, 2), + 0x1CC30 => try circlePiece(canvas, width, height, metrics, 0, 0, 2, 2, .tl), // 𜰱 UPPER CENTRE LEFT TWELFTH CIRCLE - 0x1CC31 => try circlePiece(canvas, width, height, metrics, 1, 0, 2, 2), + 0x1CC31 => try circlePiece(canvas, width, height, metrics, 1, 0, 2, 2, .tl), // 𜰲 UPPER CENTRE RIGHT TWELFTH CIRCLE - 0x1CC32 => try circlePiece(canvas, width, height, metrics, 2, 0, 2, 2), + 0x1CC32 => try circlePiece(canvas, width, height, metrics, 2, 0, 2, 2, .tr), // 𜰳 UPPER RIGHT TWELFTH CIRCLE - 0x1CC33 => try circlePiece(canvas, width, height, metrics, 3, 0, 2, 2), + 0x1CC33 => try circlePiece(canvas, width, height, metrics, 3, 0, 2, 2, .tr), // 𜰴 UPPER MIDDLE LEFT TWELFTH CIRCLE - 0x1CC34 => try circlePiece(canvas, width, height, metrics, 0, 1, 2, 2), + 0x1CC34 => try circlePiece(canvas, width, height, metrics, 0, 1, 2, 2, .tl), // 𜰵 UPPER LEFT QUARTER CIRCLE - 0x1CC35 => try circlePiece(canvas, width, height, metrics, 0, 0, 1, 1), + 0x1CC35 => try circlePiece(canvas, width, height, metrics, 0, 0, 1, 1, .tl), // 𜰶 UPPER RIGHT QUARTER CIRCLE - 0x1CC36 => try circlePiece(canvas, width, height, metrics, 1, 0, 1, 1), + 0x1CC36 => try circlePiece(canvas, width, height, metrics, 1, 0, 1, 1, .tr), // 𜰷 UPPER MIDDLE RIGHT TWELFTH CIRCLE - 0x1CC37 => try circlePiece(canvas, width, height, metrics, 3, 1, 2, 2), + 0x1CC37 => try circlePiece(canvas, width, height, metrics, 3, 1, 2, 2, .tr), // 𜰸 LOWER MIDDLE LEFT TWELFTH CIRCLE - 0x1CC38 => try circlePiece(canvas, width, height, metrics, 0, 2, 2, 2), + 0x1CC38 => try circlePiece(canvas, width, height, metrics, 0, 2, 2, 2, .bl), // 𜰹 LOWER LEFT QUARTER CIRCLE - 0x1CC39 => try circlePiece(canvas, width, height, metrics, 0, 1, 1, 1), + 0x1CC39 => try circlePiece(canvas, width, height, metrics, 0, 1, 1, 1, .bl), // 𜰺 LOWER RIGHT QUARTER CIRCLE - 0x1CC3A => try circlePiece(canvas, width, height, metrics, 1, 1, 1, 1), + 0x1CC3A => try circlePiece(canvas, width, height, metrics, 1, 1, 1, 1, .br), // 𜰻 LOWER MIDDLE RIGHT TWELFTH CIRCLE - 0x1CC3B => try circlePiece(canvas, width, height, metrics, 3, 2, 2, 2), + 0x1CC3B => try circlePiece(canvas, width, height, metrics, 3, 2, 2, 2, .br), // 𜰼 LOWER LEFT TWELFTH CIRCLE - 0x1CC3C => try circlePiece(canvas, width, height, metrics, 0, 3, 2, 2), + 0x1CC3C => try circlePiece(canvas, width, height, metrics, 0, 3, 2, 2, .bl), // 𜰽 LOWER CENTRE LEFT TWELFTH CIRCLE - 0x1CC3D => try circlePiece(canvas, width, height, metrics, 1, 3, 2, 2), + 0x1CC3D => try circlePiece(canvas, width, height, metrics, 1, 3, 2, 2, .bl), // 𜰾 LOWER CENTRE RIGHT TWELFTH CIRCLE - 0x1CC3E => try circlePiece(canvas, width, height, metrics, 2, 3, 2, 2), + 0x1CC3E => try circlePiece(canvas, width, height, metrics, 2, 3, 2, 2, .br), // 𜰿 LOWER RIGHT TWELFTH CIRCLE - 0x1CC3F => try circlePiece(canvas, width, height, metrics, 3, 3, 2, 2), + 0x1CC3F => try circlePiece(canvas, width, height, metrics, 3, 3, 2, 2, .br), else => unreachable, } } @@ -285,6 +286,62 @@ pub fn draw1CC1B_1CC1E( } } +/// 𜸀 RIGHT HALF AND LEFT HALF WHITE CIRCLE +pub fn draw1CE00( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + sflc.circle(metrics, canvas, .left, false); + sflc.circle(metrics, canvas, .right, false); +} + +/// 𜸁 LOWER HALF AND UPPER HALF WHITE CIRCLE +pub fn draw1CE01( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + _ = width; + _ = height; + sflc.circle(metrics, canvas, .top, false); + sflc.circle(metrics, canvas, .bottom, false); +} + +/// 𜸋 LEFT HALF WHITE ELLIPSE +pub fn draw1CE0B( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + try circlePiece(canvas, width, height, metrics, 0, 0, 1, 0.5, .tl); + try circlePiece(canvas, width, height, metrics, 0, 0, 1, 0.5, .bl); +} + +/// 𜸌 RIGHT HALF WHITE ELLIPSE +pub fn draw1CE0C( + cp: u32, + canvas: *font.sprite.Canvas, + width: u32, + height: u32, + metrics: font.Metrics, +) !void { + _ = cp; + try circlePiece(canvas, width, height, metrics, 1, 0, 1, 0.5, .tr); + try circlePiece(canvas, width, height, metrics, 1, 0, 1, 0.5, .br); +} + pub fn draw1CE16_1CE19( cp: u32, canvas: *font.sprite.Canvas, @@ -491,6 +548,7 @@ fn circlePiece( y: f64, w: f64, h: f64, + corner: Corner, ) !void { // Radius in pixels of the arc we need to draw. const wdth: f64 = @as(f64, @floatFromInt(width)) * w; @@ -516,9 +574,8 @@ fn circlePiece( var path = canvas.staticPath(2); - if (xp < wdth) { - if (yp < hght) { - // Upper left arc. + switch (corner) { + .tl => { path.moveTo(wdth - xp, ht - yp); path.curveTo( wdth - cw - xp, @@ -528,8 +585,19 @@ fn circlePiece( ht - xp, hght - yp, ); - } else { - // Lower left arc. + }, + .tr => { + path.moveTo(wdth - xp, ht - yp); + path.curveTo( + wdth + cw - xp, + ht - yp, + wdth * 2 - ht - xp, + hght - ch - yp, + wdth * 2 - ht - xp, + hght - yp, + ); + }, + .bl => { path.moveTo(ht - xp, hght - yp); path.curveTo( ht - xp, @@ -539,21 +607,8 @@ fn circlePiece( wdth - xp, hght * 2 - ht - yp, ); - } - } else { - if (yp < hght) { - // Upper right arc. - path.moveTo(wdth - xp, ht - yp); - path.curveTo( - wdth + cw - xp, - ht - yp, - wdth * 2 - ht - xp, - hght - ch - yp, - wdth * 2 - ht - xp, - hght - yp, - ); - } else { - // Lower right arc. + }, + .br => { path.moveTo(wdth * 2 - ht - xp, hght - yp); path.curveTo( wdth * 2 - ht - xp, @@ -563,7 +618,7 @@ fn circlePiece( wdth - xp, hght * 2 - ht - yp, ); - } + }, } try canvas.strokePath(path.wrapped_path, .{ From cff6860fd936b3265adf01db068f8b86a7458aa6 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 1 Jul 2025 17:03:10 -0600 Subject: [PATCH 099/114] font/sprite: update reference images --- .../testdata/U+1CE00...U+1CEFF-11x21+2.png | Bin 632 -> 1006 bytes .../testdata/U+1CE00...U+1CEFF-12x24+3.png | Bin 819 -> 1255 bytes .../testdata/U+1CE00...U+1CEFF-18x36+4.png | Bin 1492 -> 2247 bytes .../testdata/U+1CE00...U+1CEFF-9x17+1.png | Bin 443 -> 751 bytes .../testdata/U+1FB00...U+1FBFF-11x21+2.png | Bin 5448 -> 5427 bytes .../testdata/U+1FB00...U+1FBFF-12x24+3.png | Bin 5724 -> 5718 bytes .../testdata/U+1FB00...U+1FBFF-18x36+4.png | Bin 9973 -> 9974 bytes .../testdata/U+1FB00...U+1FBFF-9x17+1.png | Bin 4295 -> 4334 bytes 8 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png index 03305c81c24482574aa134051787ecb75293f0ab..b3cda82d1391bf8a59367dae98436e1ed243ef7f 100644 GIT binary patch delta 943 zcmeyt@{WCiay|1_PZ!6KiaBp?T+BVJAi$PjmcZh6f}!>VN6!R7BPQhvmi&PJ0~~UV zf)Wqczgiaf_Li4}$)l?O-yg~J`R!z2OXd-rm~r5nv%R0yKUu_e+}E@!s^Z1ui7rXy7us^vZ(0)b^3dkzRmT#`+LJKVgAzeC>IwO z7ZsL*`wX+c)|J*KSBLAgvR-dj+W4R;Kt(J@pzBBzx6{Ok0!IxYZaogyq=T#rJ#=;` zh;*`wwM`T z#eVbDLQ%d=1>5IL&ak|hXu0%akxOFh_tPi&xavbCKvvmj?a?dTE|Q|%9%gYOjL-Ij z90S-THx0R13^`aY{Jvjbb^B&3ON{denYn>_>oQYJYVjBSXWozfUvk<-OkY#zZ}E%CCQNiAlWb z;mZ0@Ex#WS$6fR0Yc}9%JNSG4ti07bLgn2W^WV|z|8!gbDE0hq;o6p8J<2T-p|6y!!w8Xoyofm4ULV@n`Ffd3<^RX zADDX#%rc9Zq>@>6I(D7j{QB~~G5>+ Kb6Mw<&;$VCm8w<% literal 632 zcmeAS@N?(olHy`uVBq!ia0y~yVEDkm!1#fKfq{Wxujk%h3=B;6o-U3d6?5L)Fyv}5 z5OFzp=l`Xs2#$?Wj)^~w?@kh_%)Q10Qq91?kg(@*(fPM2yZ^j@ywAYk$KN$77Bi-@ zuy3C9AR$ah=8RWUbEId1fi@>!n(D!WYrc4J**R$SaC3g?99W@YOz3~74>+KaGuFmThR(+N6QujHdx?ATFGg$3|eW#zR z_8t9y{vBu6v3sW$vhr1Fw~JZ4Xx;pDj?2@O_NP# z+eN;F@#z*^h_?KqVQ~K&GuXbH8@X5&d0a03|Nr{BUXxQ#$6;&nZ8MY-^&(Q+-@mWh zDxhZ5KbN;9pHsQ<+-9Gi^C*m+@1-nenXOA`%U;IC!Z%1K6FHHgYl;@Gu|zyZs!SqDyb9t6mUDgWLUZSqv0`-ou00KF6*2UngF19_2~cr diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png index c17e08c39e013a569c43f4e933fd0280ecbf106a..e076da7c52f1c09e553d278e5025d464d17985db 100644 GIT binary patch literal 1255 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|U!CPZ!6KiaBp?<`=dY z2(T8&C@2a|XkbxMc&OsSG)ak(bK(Y}ICWMl-KRJFrq4Z7`2PFSxsTH<<$k(nNGT~T zs%G5q`*|u)@T9lLy=Sq_-+bmk#J|1+Z;ILEb44EfzRRkzjZeL+p+~WY$)){M+##Mf z%uLS;{?5F$N6h`(iTj=!XVs7Y)1GkQc#-0x2?5*HrtE&5y<79`^Ns&M1m2o(I5sa? z$W!m{b#In}0(DO7cVz1=0|lM#D#IJO^sxLWAJ$+x`E95bSWBUgu% ze)_Gy!-TD*FZPWHdnW7hN>=eMF1xHh%<^I93wJ-3F+U@6rzo`Ga@@h|b}nG>C9UCa z_Kx}IkL>4@uX%n=VnP$6=D(tkJTIF3^ST)|!F*)mkjMpg1_p-z|KHp&GCcbbEv0w|WCpdpYQtp6}0Cj$dR z!-D_oZwqQ@z+@r7Kp0}j<%?`b97LEe{Qv)IH~(*oTLx{*O8HLj(o%}D2P z`~Ux|)%VX04qu_Wd|g1aU+ltdb9Oo9T9&rnDtaLJI(uii<7~qpOW{YuPxG$+AJk|qF$u()HU{^tVz-#-bKw>o_PVf zCwW(??sVMB!oa{#vDMn&uza4o-`kVCtG+&AyINJ$oMpSyaqGN03!?aKlD;=S{lD#f z|I_U$z0ABa84nzo*`+K5Vs8D?oHy$U+to>T7DOQ#%*gQI5G3NR8*(u@3b0=MJwHmB zdFw*4){PUtCrtMH7JTyuLtS>w_PWYH+Ry}`a6!?D9h?*t0t8+NENpgg1hF(YN;)n; zSt2Y<3=9t@eHVXFQE7QkPEXGM$4fD|_Q|Tz)6YogL~cttmT+>Dsxh1QG~Jno5u0*O z_8e?VEr-P4^^Lp@20W}6|88H#-{G8Du;jbghwG(sCdp^|`2Wq+x^QB3G&o5fI(Oyi z+32`~zteyG(0F!rBO;xCsemRtP>K{6hZuXLdmO+{cp@M;dp@0!B4NFgI oLRImH1*GCF!jQEO8IcV+5ShrfSW)2KULlZqp00i_>zopr0E+Sig8%>k literal 819 zcmeAS@N?(olHy`uVBq!ia0y~yU{qjWU~=GKU|?Y2`0`{n0|V19PZ!6KiaBpCY~*TC zU~mom|9^RQCTF{}2xrl`A1)WH*+6QcV8gY-ow}cY*1TTNzD_MbK*Q$e9tU}y+d>)~ zA}m}?tc^_$jtdk5DnwtfFfjc8|K`RBM1I&=X=TAH07IY1;gq;yy^i02=xzt z{yENJJi2nL!#Fw2X~k{`>#`%aiTJuei28y&{@*dZqT2X~E$ubeFFS zi00e#s7Cj_UUcU3<3;6>$qNoRG&V9Zv$8RPHM`nFEV*@%lgUxQ<>H^sW(-H;wj|me zu70qT-!*W<@{9evTldetQdHEMWx3O7>ztT{QGV+Jbk)OGOk;Nc@%!`v2WDno9utEL z2OK_JytcgXwQBf^vXi2#VvAa{rpGM21>!O?FfizSc%u32(M_4G9df!+xom#q zKmY3g5p}!Q7+!#xDNwWVYvY`~dmwJSiR8vsxh+TEf4H}i&-LP)dC#gn^8$2F@~%?d z>9|$Je8sdUY*&RC7#JR`pKEh;j!plaTa%?jUZ3P$_4Ntc)vBWAEZd!qTjhRq-esS5 z{`S23Y2PQA@|hGYXk_M<$#~%K;nKC`71@)dL%fTcvycpCVPIegvxUZ+Aul-I{;l2n zmO&aEln5XWRRfP(=}AqfN)Gs-E5VHZJFJit0=H@HLq;^44#a?sd5cif`;Zx-W;#Q_ WYUdN5JCfZ&Zu4~Yb6Mw<&;$SxGBgVS diff --git a/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png b/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png index 6f4a004d497fbb023e9bfb3d94b4268f823015e4..366be38671868fa0eeefedb0a19356ef4d3c2b59 100644 GIT binary patch literal 2247 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 literal 443 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 IHQSK?0qw~kI{*Lx diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png index 35504e45bc06f179958280e2cec69348399d1fef..2eff59c76114698bf12ffc228db36a935a03c16e 100644 GIT binary patch delta 1862 zcmV-M2f6s%EY>WLBmo_GP0%W<>-+#q-&&P7_)n}p1*l82 zJhO2!A+pvKaUinNiZEzZ2)#W(eVXNwt+&>NN}cLL30j@-h!q1DV(|8V0J${F6PvfK z3za(6g)+1{*%2!SF2vYv0b=LB*^hT@|F%4RO`Hx$k3_{0H_sz!i6|DG!-xHS2bo9n@~%~Ao1~3_NM+rkpQcC{_{UGMIo?B zd)kFz#d`1kNJ|H4@$pmwQ*)u#-JuylazK=Y=y=kedR@O_J@d|5rlq6U#CR%uQ;(E< ziFF_qCftYc&B6Y3mh6R~wc{X5 z3qPJhKxjMyLWp_-VFZABh~=BzzK7$LSF|l0D02(pGijDGbsKkPp%r$fq0#g0u_d2;;n&%@DM7sBx1WPvK*p0&%#N`G`vWCKMGs2?ceU~j zGU-5Ws^O<2a@Xi~2%6E)0U(^LDnNF!iWAyt2kIuPFrk&(K(<5BjJ^*5VY)*CGU*OX ziA+0Cm+pX+h&qw{)(|xMQ6VPGG@zC+!=&1R1DS*wCe`SFuF?BnRI4wKDc1p@nsUXo z?1BTClq;rX*}jL{L(t$yg_yJ-0Xo58Vt>hjYITy>XZs#*4?%+;72>kb5KzH^OG8Tz zRI3k{hC-oGC=?2XLZMJ7^qAP187aHA+Wo>W9Zq^lZGEKx6o6C-YyTji(G6X#k;!Yd z15g0cw>DD^`@hDZ`;Y~oPee`r3zM1(9DmTk&f7UP4d~23t>I(H0LAZ&tO4jcK&|65 zWPswSku?Ba8L0KaOa=&0KS0CPWPo0N0?=lFhTB6{PWMg$dU^+_JKZ}aNs=T<`a!Kf zb>DkZu2HX5Z&|NhU*qv${ja`n`Ft$`1n5IRdjb>!6ao}D_RIhZGl0U(jezC?6dnQ; zx)IREHHJ=|AE@^>kO2bp9?($%3IPfMT9!Wmvr`Lo2Yd%NizlaaZCS*{^u!R zsJ^N4Q2m_Wgj5T1-W0|`CCuZN{t@~76fnmo^V|p0#Cx7&&+wa&Zb8ybVdSc0e(lx2 zIuXg^mVf@yd*!EyIY>H=roYU8PI>qSpkt_%@=fRis(MIE-?Un^W*q2E%m!byDIddZyLoezhE}1Blwwo3NVO<)^ zmpQhCGtOWuAiUgVeWXnnilptP1wmNX&hllBZ+`+oBE~k8QThnmb`(h4TP(=D5*h51 zKpH_f!*nD{ADs9(OsL6%%uBV=K8YuyHD{dAhUp{!VkzqOHVZN?Plp?Ac9cOl!y}PE zebC{P!-U!_$hdwVZnWuP80Cx()k5_F$CnNhYP29#7o4e)<5b2O9LomlLqF~@p;il0 zXMdKEsZ|H^j59dc2Wn`Umj~6BoE2C=`?(+^6@?_^vJWo}@v=`wRnlRcL1_r7p=Dkk z)I~TPQ@6`SLkoL`#*2%%q*xaPIcHv6>{Vl6U|?WiU|?WiU|_(9^Z&c=?<%U3kgggi zf%sI8RMIcAfk;K4UOq=}{&+C%vTf`9cz?|6wk@4(LIj$NKt?g%-I;IF(giHBeADex{!E5KhT^1yUzV*p&1mo3}!UoBF*1seg7G zX6k~?|v0{{U3|LoX73Iah4MA7@7TDNMam3%9Z#eXXoL7IY@p486+1%XHG6bxwHBL-g5 zr(i(qUee&2ug5|Gt-I#IhhjMv3u$QEhay-tPRW4Qtr~COLOXZ4fQ9zUd~2bt&`M+~ zAXJH5=bM#C^$)aEn+gb(YS;N@saC+X>`*|gmc7k)YuN%W_J;yu#r|!+TkO|={qdu7 zrhGt;7yln-?k15WNs=T41@f~=Y delta 1912 zcmV-;2Z#99EZi)RBmpC_C2I$N-<8}f48_w)N(r5$w3Lt%mQGTNbF#}Hp?jqT#yAhF zd(QW;{=hT(MaUoqIsOZULZL4vT$^d^Mf9-uMJ^2pI!=|ZJVbD;#iDm-F^<3bF7-X0*8W_jZ9 zwsfJ=rnyjtUX>lO!f_$SZVM2r|0e%Dar|GpP;FB%B1BwZX@%FkOg}+XBRDP^t^JmmrZY)Y`m*NYJZ?Z)%0%LL3~0W8S)mRWa2U z?k-{@U8uErCy}97p8!yQD-0Ln+|X3Kwcgd3*=#~Bl|kb3sjN-?hav%1@%-oi%oK%z z&9tZA75}29`wc#6z86*cpSr`M)w5Q!y?^w^gvzBS86q_-h%G%T` zC4EIYkQFEh3@db-_8^XSWoEO9Xr-9nH$IqvXfGgy0MH1re6l+CaJ}-1wuOSCwZ-qxh8O_^Lkx$Zy#y74AvO-f zj}O!59Wv{sU~X_!x8kY<2%kj6khIpMUsx3H>NSI-NQjMM(J^!Lw`}UY}bpW-L zE2d=^97v>GF)ho^Jsb}~&W{Q)Y3~85U@)=2K zR+P%$=3qRCd^pe)jtpZR0GOp16&jIqj(A{!P zUTYnI0+988&E{JF&lq$LSpfP()a1Va00960?AJk(f-n$O2PY< z*n!U5IX4aH#6Yd{$B+Sv?-^MG&~<=XZ=4|m6igp{OGR6l_0Kj#-ARf3$I!Z;{}dEGKU zay~x=o@0@D?h9$;J62%Wr{2U_GBtd3VZ4@W* zM6~u9C$wSw$iG;Mdb~}7jOOVu&}LT|#AkRV637p_d~%3Tn*u?5TQm1QhC9d8o5qoK7(u7V1DSwJtEY9DnaVb5;C>wLZ0~yF7^Q%TIS_J6_T?8 z3m88aWTc{ygk1LFr6FGS$*4*?%x6#Am~&o;tmW*%vAB2lSoIZ;sae^JVCL;qp_S zy`P}z%TVh5zRGNVbGYubyw>%#^!pQ~taIA76tKUJ+5E9l)XfsMCH=_atvJMgMg3laRJjckbwBd)FFl1}!L0=82X<6gZ~@8w zU#0XbyOkh44awB5j!;owC)iDFX>ZbKFP18v+0T0B{z*S!#lu9g2Ja00006UunmM7NgfcnB|sC@Y}bzr+q?WV0q+I z+v_!AdUK|Hwd{?5z36eIMjzvaY6dB}hBkJE*UjJ97sxZ4mNQIYQD}H4-tdP(<+H7< zgAs#h8pF=X8<-cUFq~Y|Say+7gl)ksPli{o88mJ`_CLheu*vqH${P!AkNS1FOPE*K zGF(k(u#{y;&HbnHhCyQ?b4D`5s_Am9C5#LV|Np|eQldK^m^=M^gp{{`0$Rx)knJ9nDt>*cwx?`Jdp zRk={aq^o|RsJW2=3@U6H7#RL$_r0?H?y+`3gUh zNV$6HUxvyrHK{i%>gM06=dfPa&fs-pc5>?W$5EH#l|JgsiIic|5EST{dUvL)?@eX7 zb@nU)k7h3QpA%!^@Y#Kx{v%cq;kO-!CoyfZew5+MG)-he>|}@R%^R0Ayt$Qg%T=`A zo9o28MOqu&wlut1Gq0{Vko&|rowwVcA8mN^sO?*WH}46(4Z8(R!{X}B+g$u6tQ*vO z^0^X!S`NF*9se~RwHH$2=GolZA}vsCzfh?qaIHh7+`_XBb-#ZyfkW@MAy!-r==KYJm!&-9w`HeXr4{J|S@tmaMd6N}v z-C;vs1_p+P1^?FSKID{U0*CcxP*DFzk~;fzopr0BnRY5dZ)H delta 700 zcmez7`_*@X3ge!Qs;7nOZv}E4HsEP~`1}4Yb`8O48=4jOC-^g^-u@ntak_m%>qPx4 ztBMv~e!6JR{^cJtVsobSF};v!n8v=~DPu+@!=}0me?|V^>Emsf#K01eDX_qqA#2wi zKUM|F1}#;GG~qmkDJ&rN(^J-*+g~tnF<;1Gzp$TC!}@ssq2BO@s+SD)S^N&NvJU^* z7UZ1GKg8Rx$#g%bc^qR<`mYj(*fSy**cll9|9^8;k?(*3N882U_p8#@B{sJS9I5{1 zzv73>>^u&YfI8t9F)UrowYnd-{@K0Kb-@gWt%Y--IOhHPlDPl-)h?(uP7}_sY@8;| z!@>Xp2VOIRP2T)L*hR4Z%1q5a3}F7OK)%BYJkIayyJAb@YeLp$I>|JziMl1<>vG?v zdG5OV`rqXb^}SYX5c>8k;^x~O*GfE9?zI|sJ2R*r4rT5#YdsH4V%n4~l#y#TO=LpsY~S50lIlGfZ^nkOUh5E> zF!$=^e7!8jn=_aG{kBJ9LhPLjas?|HZ?cy0FFJPW^!u6Wx9S$Tt}^@?^Wi@)#RiYqw7SMT{* zK7zxDBXjcI$uf$k`h@rB#nkh0tmRgB@7VRBLH|xUa}xLWgMv#6>>sFW-)*>Y(yM@F z<{F+K7sJgJ%D#xoIJ(8#@Bi?1+x{EeFXo=#YWet}`Xm+4Nh+S{a*!ZAY{<*Nz|gSZ z-&);=oYG9-;NFfdb@l;2RLY-$Ve$qw1+_*VP_!u2zm}e}U>geqLj~j4u%!tK9;~q- O6Fgo0T-G@yGyworh&BTN diff --git a/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png b/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png index 3fa06a8fc969f3641e2f6d2a02871c11b4bf20ff..062a7da810b835f0f27d0c9b540ce22f3207e157 100644 GIT binary patch delta 4142 zcmZWqcQD+I^FF8d(>o{XiEu<0-08%NsL?z35>cWC`J5wB5>X<;X{RM2h~5+Rjb84E z1kwBHC4?y7dGr4M`#rO>yE8kxvopKTZm;03V2UtFeX2+=kW^g@h18K6R*~JUBDyG|+j=!PW%a{jVoYs@Z7N++()%Q+l!Hyc9hRNK-L_-wSZ+`mpjvv6q zqrXW7iRJaWml$ZK#3;Zxz%(owaz>4O>+*1GB=x1u_Vf4x+`m{z?l58Y4w%8-Tp%q= zu<1gEtz?`wc^o*y@wfS6->uvk`MxHv1cWNSoES2b&)aCEZBcIgG(Xv45K*u6G=ujx z99L0R*4VS-$x!TtNq;l#UwW3=HX}t7bUY#&SoOGCqpfRi%4x^Go^C~ z#$5HM+{s*jV;-xt`fFA{EiPN1IEXvzQVkY1FcoM!IipM~i4fFhLDuRkL>WBx7N49} zQ)Vsh^f_JL9@#lyw=H}KB~twjoVor2s~wkhszebx8^~pq{G5kh*>R@+h*z;EevQu% zLODH1GQqeQu*uzMR;GVf`;vlsQ_6b^x=Pz>`r?Z*(U!kUiT+M)V5M^V#LJeCq`}19 zEkD-56WL`WCFJ4HUnbdn3l7IzJc}wJzhx`Iaj(2ft~LnoRNVERn1;jrfk{`QwbR~r z4-=;jkG-Rt-PUyBm@wX;^TdfYT~`xbV@c)VYW<%VA|WB=7w-6VBWrAtR|4%QU5&|Y zlP`yzT*EFak*k9Gz*YF8%M}7jSR?a0CX&DX&_WE$)Y0aTg??MiTrkN!5B#G2>>^Vl zqn67Oi(`^Fqq`pJ4Jm)Rl9M5>AGzBwTIM^oO@_=o$PMzD4MvDI z)kaMe|0vC0F>+L*IqMfM%~Eo1NLGV=6IAA|TbDg$>PvnM^kaOgeiY!a={))+!8YMD zvjZd>gvT7Wco=rAPNL&Z*M%CL4shg=sx6m?+$TlbLZ`0||4Gwn|CWsZZSb++LkLDa zAci-|rUMT@TK`3Vs2-*VTwT4)ZB$96(I09&KVSrb zuCKRql7QePI9=~2B$pshIz5(tntp)7BgT!CGn%QT(<7#agiDa;m*YjMegRCO~p9Di%OV>J*j3T0q}Aboq>YP?IU6xLlOW8VYZpIQ8KRILSQS^=>iS zgSyURy5W$O+u|hL>(0&eX_An>{&d?O1+R@>fZR_4H7UEeun@2=V^BRN49T%!lei7f zX85(Ka0!T^8oauY^>40v3UW3AL?Pv2l9p7iLnPrz!dexX%fc>zkxOcNTP&D5DTq{sUttW!{g z!!O*r({d2;mquh%jH>SL%;-u~+@gT@KR3-~iKR#qV)p7o4zdlFLSrKx!Lcp0_hMJU z=?%{`Gmpf_MV3P=zgoeJ6EB#8EY)`+mPAU@+tk>}Xkchk-BHCxAh{R4;t z<};E~u9tm)O@~wAK2d3{XEV(vI#S*Z)96Vg8rvXRnEUyC_}D&P~kF zia{1^SHej&x=W-c#OFs*xu5%EfOmaX%f{Xf?Y8HuQ9vzY9d5#9+U-;b@0bLx`cj|J z6P{C^BSO77+u6ST@?#z5uyUa2b1IzFAmNG&9e!Bg50Rn1)^wOL<4M{4wE4;nLEpR2 zKE&s6mGF;D&+F9Y<#&c9W~CPM;;LV30(}l+JkrD7=&tqHN0XToe?AOYT)V}p5BF%B zz55O^)kHJLnlT15RjAwpV+|SsZ`?`*8)_Djz>;7bT@!G5QOo-|)SXX!ESXefS43q+ zezW$PFXd1DcJc6~;83@HbemwiI0dW8_4ybrg=$+s{9_Q*hNeyvDwVeyL}S5^dds@; z4ZR}O=!t8NAz-8F!cURt4E^XyhS*}nYIKd-TTn5$;@x1Wg(49U0R3TtgWAI_U5F=B zhlpiK-9!n^cQSt_Qj0fcoAzdkDXMP>^xHgriu+9B9lK^diYfNR`XaMvlGC^xzt9ln z#Ju-ncNmyfl03bX1mc#qxz97Yff%~OZrY%d0Uw`EV_mGwv2n$DZg_T9w>g@{*9M_H zjKFizNaY3qO9#y7U<>xOebxnSJBfa*CvJR=0Ao&84wQv_8!8I3~~zsqe-Ww4VQ&3x~ZP?d1nEmiMMha(K!wl zyS4~r&)LYY=U~=|0j9Ouhx#StE2*&@?50FdUx8;{ZL2*|;zc~ri`l@!>duV<_OYSi z>4p!VK{}o^>1Pk(N3bkA9DT+N&eK&G=G=pf7*J|6uZMaz#Rp~llj!YZdeM-ze8x*~ z;!p$ZXsV{<_0!p^dr^SWS`Ggxjd&YBc&+wQR6J;V4MOb3463xbtU_bxWc#4=^~Fp( z+WB(yFAcS5xXn^AH6`XRvL5&_7L2w736Y3s7YOvd^QkmE_g3wzA~DVULg9=|6I3P( zv>1d7N`BPw)Ont>z@%l=_U?oje25-m3K=69h$i_~1?P+g0)I)~%kO|IA`m2q<%;aV zN8g_5nF+`b*{Vc-cXFA|}?xzCazzDqvox+S=@Y$s(FJeiEJY6#2 zNI!7Vl^q$K16o)ngPiG;lvK&}8>X%bw4g|+DL?{r(4$mvV!4b^#=g-$P=5ER`kzt7 z&;>eHOFP}$Ge^riTwdn`zrT>h9SvHvN3OEX#k-MwG8W7BA+5uUV1N1jES)z$Jd*|5 z6%el_@LLnt)Y_I6YhPXJS``raMOqFt6k`u-K9u$L`jA=-TL;595l82G6s`D00*qfQ zc)+QMk95PleHwKYxXGIQN@aC3=Qmes_?55d8{-3|(lX-mJ4VWsmcfTffxsqfGE0MI z8Elrnk~QU;Z(1AyRK^FYb*ZhsEdvH8M-O#3ZLUcFJ43ChGjNC{rT;cU5=n0Fzm3>M zi@YamCUw|F@WqR4NWi5-am+!@F-Q7wmu(J+wq!M}$0x(z6>*4dJ1G)LOyfmQdzbog zX6RI$755QR&>XdyEwe+axJH^ksCH-_mG}9Mxtsd_RLISFhx4n7T)?4WXGF_{ICy{5 zX+4ymh725yaFzwpcVAbPJkv7QQ<4jg{r`rQiVPgBTwn^@uhn1s=x0%RKg@PnyY=~o zM6S2Iy2`t*;KY%r&y9j#xdulOuKqPD!lq-@xu0AKsnGJX+@#5bL$#0IMPHi<)qwKM zP2(E|X*NuxUR*f5PHT1j`;;AVa=+f1_rub%wj$npr6EC#4O$h6cQ*3$`Y>OxpdUe` zsuB;DMhmYyb^RiWcT0nROreEYYUvHoub0-(w|l{{f&(F8a>Kp>j){0XeqTy?>2cE( zMk)5KIY%zIXOz@vE%(Z_&WHs84{)Krnm1?_ku-kAC0WHgdUm4+$4t`}XV6WVOr=Ry z5qpisO^)u+i5fm``X=8H=Q^W_GxIH_?{5TdYX2~Ie(4ijuPi-y9vN#D@3b?9>f343 zE%GQF$v5oHrT53M^k;yNz}-8^epHay4R5hz&!$hpZWEBdTGOu|@v}WS0BSl5AB+U~ zFsO*ewGK#!Qq~=*OdqZjWDwN?$L~L3=&mCncJg+=`kN75d;>Rnj5(mD0SF3}5ONOP zOITZHd_;ZTLF>+2>@%zuycIWq$%A!(JiqB(9QeG5E<}DvcA_I;zH$E=r%6`skr-`r zoukp{NH2(PMI)tUiY_|=KWnrwmqFJvP$-X!bEvq#izJ(Chq|v=YR?POWo8;x$0u3P zIQUcHAaxvPCNJ1NtX3n_U@BX9aj9pdB$C~t3a`j1;WkbOAN>7ZBW0__rgt1`=9iz| zx)|80JMXM-T*2kc&mi?eK-B+^dQuZXAQDy3ovNXroMW|CydnW4?w3+>b{O#OT$XL60v#I=u^w@x-S0##0+gBOkkOq%k2^d+XFse3O+XO74ywoFrX`9>)Ia z`wR`+l_xs z*sHE}u$rc%YO(yrn-nwq=+WHdVOBo~dHNQ*&chCP_{_?{tmE%I2f!F2x$b1TAVSSO zEuI!TsWqnj#|dFTS}VWOwjZneV1F+zaiD;hVqB;%BPDN?^EgpS4W45JyX^-KpF3#6#PdWLT`NrXf+sRVDt hLNuua|5HP2l%#uH8c!Cc&>#>9WT0cLU8(7e`5&fz-^u_0 delta 4081 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 From 2a836b0ab7677ededb9efef18301b9bb3e53ab21 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 2 Jul 2025 11:02:33 -0600 Subject: [PATCH 100/114] font/coretext: fix small memory leak --- src/font/shaper/coretext.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index 1fd9719bb..1aaa029dc 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -109,7 +109,8 @@ pub const Shaper = struct { /// settings the font features of a CoreText font. fn makeFeaturesDict(feats: []const Feature) !*macos.foundation.Dictionary { const list = try macos.foundation.MutableArray.create(); - errdefer list.release(); + // The list will be retained by the dict once we add it to it. + defer list.release(); for (feats) |feat| { const value_num: c_int = @intCast(feat.value); From a91f9ed0e29797e726d278f0a81998136e7167f3 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 2 Jul 2025 11:38:26 -0600 Subject: [PATCH 101/114] font/coretext: fix small memory leak --- src/font/discovery.zig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 9284f9486..6f51379b4 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -831,6 +831,9 @@ pub const CoreText = struct { i: usize, pub fn deinit(self: *DiscoverIterator) void { + for (self.list) |desc| { + desc.release(); + } self.alloc.free(self.list); self.* = undefined; } From 1f733c9e7fbd681dd5557009819515ed614a7119 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 2 Jul 2025 11:48:30 -0600 Subject: [PATCH 102/114] renderer/metal: properly release texture descriptors Fixes memory leak. We always need to release these descriptors; the textures themselves will retain or copy them if necessary. --- src/renderer/metal/Target.zig | 2 +- src/renderer/metal/Texture.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/metal/Target.zig b/src/renderer/metal/Target.zig index fa62d3014..15780189b 100644 --- a/src/renderer/metal/Target.zig +++ b/src/renderer/metal/Target.zig @@ -68,7 +68,7 @@ pub fn init(opts: Options) !Self { const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; }; - errdefer desc.msgSend(void, objc.sel("release"), .{}); + defer desc.release(); // Set our properties desc.setProperty("width", @as(c_ulong, @intCast(opts.width))); diff --git a/src/renderer/metal/Texture.zig b/src/renderer/metal/Texture.zig index 32820f8fc..5e6ef78d0 100644 --- a/src/renderer/metal/Texture.zig +++ b/src/renderer/metal/Texture.zig @@ -50,7 +50,7 @@ pub fn init( const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; }; - errdefer desc.msgSend(void, objc.sel("release"), .{}); + defer desc.release(); // Set our properties desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format)); From 5dd1ebb5836c2155976cf6adb227e7327e6d78d2 Mon Sep 17 00:00:00 2001 From: trag1c Date: Wed, 2 Jul 2025 22:17:17 +0200 Subject: [PATCH 103/114] add newline to end of file --- po/bg_BG.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po index b371cb04d..18cadddf5 100644 --- a/po/bg_BG.UTF-8.po +++ b/po/bg_BG.UTF-8.po @@ -272,4 +272,4 @@ msgstr "Текущият процес в това разделяне ще бъд #: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" -msgstr "Копирано в клипборда" \ No newline at end of file +msgstr "Копирано в клипборда" From 8ed08aaecf19c9539a4679309fef849bee8207ba Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 2 Jul 2025 16:16:33 -0600 Subject: [PATCH 104/114] deps: update zig-objc This update also fixes a memory leak that was caused by blocks not being deallocated and just collecting every single frame, slowly accumulating memory until OOM. --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- src/renderer/metal/Frame.zig | 18 ++++++++---------- src/renderer/metal/IOSurfaceLayer.zig | 25 ++++++++++--------------- 7 files changed, 30 insertions(+), 37 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 68d65fbe9..237720f35 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -26,8 +26,8 @@ }, .zig_objc = .{ // mitchellh/zig-objc - .url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz", - .hash = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt", + .url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz", + .hash = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk", .lazy = true, }, .zig_js = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index 3099ca823..420893ef7 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -144,10 +144,10 @@ "url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz", "hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0=" }, - "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt": { + "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk": { "name": "zig_objc", - "url": "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz", - "hash": "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw=" + "url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz", + "hash": "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw=" }, "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy": { "name": "zig_wayland", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 133284201..6e4b86606 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -314,11 +314,11 @@ in }; } { - name = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt"; + name = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk"; path = fetchZigArtifact { name = "zig_objc"; - url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz"; - hash = "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw="; + url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz"; + hash = "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index bb0a27105..f05a789dd 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -29,6 +29,6 @@ https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.ta https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz -https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz +https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 08fa9568b..daf7e5cea 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -175,9 +175,9 @@ }, { "type": "archive", - "url": "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz", - "dest": "vendor/p/zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt", - "sha256": "ce7d6d47ac614a60e56b8509dedf869e2e0d8b747c75e48aded11eec31b3357c" + "url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz", + "dest": "vendor/p/zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk", + "sha256": "a37be5eea7e44a2d1b2976ba256b85f76a8c1063fc01ffec85c8a9e67468e4dc" }, { "type": "archive", diff --git a/src/renderer/metal/Frame.zig b/src/renderer/metal/Frame.zig index 81b38e7b6..c766fb8ed 100644 --- a/src/renderer/metal/Frame.zig +++ b/src/renderer/metal/Frame.zig @@ -28,7 +28,7 @@ pub const Options = struct { /// MTLCommandBuffer buffer: objc.Object, -block: CompletionBlock, +block: CompletionBlock.Context, /// Begin encoding a frame. pub fn begin( @@ -47,7 +47,7 @@ pub fn begin( // Create our block to register for completion updates. // The block is deallocated by the objC runtime on success. - const block = try CompletionBlock.init( + const block = CompletionBlock.init( .{ .renderer = renderer, .target = target, @@ -55,7 +55,6 @@ pub fn begin( }, &bufferCompleted, ); - errdefer block.deinit(); return .{ .buffer = buffer, .block = block }; } @@ -114,24 +113,23 @@ pub inline fn complete(self: *Self, sync: bool) void { // If we don't need to complete synchronously, // we add our block as a completion handler. // - // It will be deallocated by the objc runtime on success. + // It will be copied when we add the handler, and then the + // copy will be deallocated by the objc runtime on success. if (!sync) { self.buffer.msgSend( void, objc.sel("addCompletedHandler:"), - .{self.block.context}, + .{&self.block}, ); } self.buffer.msgSend(void, objc.sel("commit"), .{}); // If we need to complete synchronously, we wait until - // the buffer is completed and call the callback directly, - // deiniting the block after we're done. + // the buffer is completed and invoke the block directly. if (sync) { self.buffer.msgSend(void, "waitUntilCompleted", .{}); - self.block.context.sync = true; - bufferCompleted(self.block.context, self.buffer.value); - self.block.deinit(); + self.block.sync = true; + CompletionBlock.invoke(&self.block, .{self.buffer.value}); } } diff --git a/src/renderer/metal/IOSurfaceLayer.zig b/src/renderer/metal/IOSurfaceLayer.zig index 9212bd5e1..5a6bf7307 100644 --- a/src/renderer/metal/IOSurfaceLayer.zig +++ b/src/renderer/metal/IOSurfaceLayer.zig @@ -54,13 +54,11 @@ pub inline fn setSurface(self: *IOSurfaceLayer, surface: *IOSurface) !void { // // We release in the callback after setting the contents. surface.retain(); - // We also need to retain the layer itself to make sure it - // isn't destroyed before the callback completes, since if - // that happens it will try to interact with a deallocated - // object. - _ = self.layer.retain(); + // NOTE: Since `self.layer` is passed as an `objc.c.id`, it's + // automatically retained when the block is copied, so we + // don't need to retain it ourselves like with the surface. - var block = try SetSurfaceBlock.init(.{ + var block = SetSurfaceBlock.init(.{ .layer = self.layer.value, .surface = surface, }, &setSurfaceCallback); @@ -68,15 +66,15 @@ pub inline fn setSurface(self: *IOSurfaceLayer, surface: *IOSurface) !void { // We check if we're on the main thread and run the block directly if so. const NSThread = objc.getClass("NSThread").?; if (NSThread.msgSend(bool, "isMainThread", .{})) { - setSurfaceCallback(block.context); - block.deinit(); + setSurfaceCallback(&block); } else { - // NOTE: The block will automatically be deallocated by the objc - // runtime once it's executed, so there's no need to deinit it. + // NOTE: The block will be copied when we pass it to dispatch_async, + // and then automatically be deallocated by the objc runtime + // once it's executed. macos.dispatch.dispatch_async( @ptrCast(macos.dispatch.queue.getMain()), - @ptrCast(block.context), + @ptrCast(&block), ); } } @@ -100,10 +98,7 @@ fn setSurfaceCallback( const surface: *IOSurface = block.surface; // See explanation of why we retain and release in `setSurface`. - defer { - surface.release(); - layer.release(); - } + defer surface.release(); // We check to see if the surface is the appropriate size for // the layer, if it's not then we discard it. This is because From f1f9d5eb4b1f027aff4c7a4ed52911a0903a7e64 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 2 Jul 2025 16:21:31 -0700 Subject: [PATCH 105/114] Fix some config help that caused website errors when copied --- src/config/Config.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index ef8f48ee9..f36132ea9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2761,14 +2761,14 @@ else /// /// GTK CSS documentation can be found at the following links: /// -/// * - An overview of GTK CSS. -/// * - A comprehensive list +/// * https://docs.gtk.org/gtk4/css-overview.html - An overview of GTK CSS. +/// * https://docs.gtk.org/gtk4/css-properties.html - A comprehensive list /// of supported CSS properties. /// /// Launch Ghostty with `env GTK_DEBUG=interactive ghostty` to tweak Ghostty's /// CSS in real time using the GTK Inspector. Errors in your CSS files would /// also be reported in the terminal you started Ghostty from. See -/// for more +/// https://developer.gnome.org/documentation/tools/inspector.html for more /// information about the GTK Inspector. /// /// This configuration can be repeated multiple times to load multiple files. From 1270e04480c7925063ce2f037ff085566d2a0b45 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 2 Jul 2025 17:43:05 -0600 Subject: [PATCH 106/114] renderer/opengl: maybe fix issue with cell bg alignment By using integers for the fragcoords I may have stepped on an edge case which causes cell background positions to be shifted by 1 px under some circumstances. I couldn't reproduce that issue in a VM, so I'm making this commit for the user who was having the problem to test it. --- src/renderer/shaders/glsl/cell_bg.f.glsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/shaders/glsl/cell_bg.f.glsl b/src/renderer/shaders/glsl/cell_bg.f.glsl index 7ba6caaa6..fa48c6736 100644 --- a/src/renderer/shaders/glsl/cell_bg.f.glsl +++ b/src/renderer/shaders/glsl/cell_bg.f.glsl @@ -1,7 +1,7 @@ #include "common.glsl" // Position the origin to the upper left -layout(origin_upper_left, pixel_center_integer) in vec4 gl_FragCoord; +layout(origin_upper_left) in vec4 gl_FragCoord; // Must declare this output for some versions of OpenGL. layout(location = 0) out vec4 out_FragColor; From 182f8ddd1a00d9abcdcee5d7179ecabcdd126a0e Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Wed, 2 Jul 2025 17:37:30 -0700 Subject: [PATCH 107/114] Do not resolve the symbolic link for the initial working directory --- src/termio/Exec.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index b8f838cf9..598617a12 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -847,6 +847,15 @@ const Subprocess = struct { else null; + // Propagate the current working directory (CWD) to the shell, enabling + // the shell to display the current directory name rather than the + // resolved path for symbolic links. This is important and based + // on the same behavior in Konsole and Kitty (see the linked issues): + // https://bugs.kde.org/show_bug.cgi?id=242114 + // https://github.com/kovidgoyal/kitty/issues/1595 + // https://github.com/ghostty-org/ghostty/discussions/7769 + if (cwd) |pwd| try env.put("PWD", pwd); + // If we have a cgroup, then we copy that into our arena so the // memory remains valid when we start. const linux_cgroup: Command.LinuxCgroup = cgroup: { From 9e341a3d60212b361c45527a82e8c8f774e6cf47 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 18 Jan 2025 20:47:23 -0600 Subject: [PATCH 108/114] Created tagged union for selection colors, enabled parsing Implemented cell color for Metal Removed use of selection-invert-fg-bg Mirrored feature to OpenGL Added tests for SelectionColor Fixed selection on inverted cell behavior Implemented cell colors for cursor-text Implemented cell colors for cursor-color, removed uses of cursor-invert-fg-bg during rendering Updated docs for dynamically colored options Updated docstrings, cleaned up awkward formatting, and moved style computation to avoid unnecssary invocations Bump version in docstrings --- src/config/Config.zig | 73 +++++++++++++++++++++++++++++++++-- src/termio/Termio.zig | 11 +++--- src/termio/stream_handler.zig | 7 +++- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index f36132ea9..76f91f6a8 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -591,8 +591,11 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// the selection color is just the inverted window background and foreground /// (note: not to be confused with the cell bg/fg). /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -@"selection-foreground": ?Color = null, -@"selection-background": ?Color = null, +/// Since version 1.1.1, this can also be set to `cell-foreground` to match +/// the cell foreground color, or `cell-background` to match the cell +/// background color. +@"selection-foreground": ?DynamicColor = null, +@"selection-background": ?DynamicColor = null, /// Swap the foreground and background colors of cells for selection. This /// option overrides the `selection-foreground` and `selection-background` @@ -600,6 +603,10 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// /// If you select across cells with differing foregrounds and backgrounds, the /// selection color will vary across the selection. +/// +/// Warning: This option has been deprecated as of version 1.1.1. Instead, +/// users should set `selection-foreground` and `selection-background` to +/// `cell-background` and `cell-foreground`, respectively. @"selection-invert-fg-bg": bool = false, /// Whether to clear selected text when typing. This defaults to `true`. @@ -645,10 +652,17 @@ palette: Palette = .{}, /// The color of the cursor. If this is not set, a default will be chosen. /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -@"cursor-color": ?Color = null, +/// Since version 1.1.1, this can also be set to `cell-foreground` to match +/// the cell foreground color, or `cell-background` to match the cell +/// background color. +@"cursor-color": ?DynamicColor = null, /// Swap the foreground and background colors of the cell under the cursor. This /// option overrides the `cursor-color` and `cursor-text` options. +/// +/// Warning: This option has been deprecated as of version 1.1.1. Instead, +/// users should set `cursor-color` and `cursor-text` to `cell-foreground` and +/// `cell-background`, respectively. @"cursor-invert-fg-bg": bool = false, /// The opacity level (opposite of transparency) of the cursor. A value of 1 @@ -699,7 +713,10 @@ palette: Palette = .{}, /// The color of the text under the cursor. If this is not set, a default will /// be chosen. /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -@"cursor-text": ?Color = null, +/// Since version 1.1.1, this can also be set to `cell-foreground` to match +/// the cell foreground color, or `cell-background` to match the cell +/// background color. +@"cursor-text": ?DynamicColor = null, /// Enables the ability to move the cursor at prompts by using `alt+click` on /// Linux and `option+click` on macOS. @@ -4409,6 +4426,54 @@ pub const Color = struct { } }; +/// Represents the color values that can be set to a non-static value. +/// +/// Can either be a Color or one of the special values +/// "cell-foreground" or "cell-background". +pub const DynamicColor = union(enum) { + color: Color, + @"cell-foreground", + @"cell-background", + + pub fn parseCLI(input_: ?[]const u8) !DynamicColor { + const input = input_ orelse return error.ValueRequired; + + if (std.mem.eql(u8, input, "cell-foreground")) return .@"cell-foreground"; + if (std.mem.eql(u8, input, "cell-background")) return .@"cell-background"; + + return DynamicColor{ .color = try Color.parseCLI(input) }; + } + + /// Used by Formatter + pub fn formatEntry(self: DynamicColor, formatter: anytype) !void { + switch (self) { + .color => try self.color.formatEntry(formatter), + .@"cell-foreground", .@"cell-background" => try formatter.formatEntry([:0]const u8, @tagName(self)), + } + } + + test "parseCLI" { + const testing = std.testing; + + try testing.expectEqual(DynamicColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } }, try DynamicColor.parseCLI("#4e2a84")); + try testing.expectEqual(DynamicColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } }, try DynamicColor.parseCLI("black")); + try testing.expectEqual(DynamicColor{.@"cell-foreground"}, try DynamicColor.parseCLI("cell-foreground")); + try testing.expectEqual(DynamicColor{.@"cell-background"}, try DynamicColor.parseCLI("cell-background")); + + try testing.expectError(error.InvalidValue, DynamicColor.parseCLI("a")); + } + + test "formatConfig" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var sc: DynamicColor = .{.@"cell-foreground"}; + try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try testing.expectEqualSlices(u8, "a = cell-foreground\n", buf.items); + } +}; + pub const ColorList = struct { const Self = @This(); diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index 8aaa87011..fda52c375 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -163,8 +163,7 @@ pub const DerivedConfig = struct { image_storage_limit: usize, cursor_style: terminalpkg.CursorStyle, cursor_blink: ?bool, - cursor_color: ?configpkg.Config.Color, - cursor_invert: bool, + cursor_color: ?configpkg.Config.DynamicColor, foreground: configpkg.Config.Color, background: configpkg.Config.Color, osc_color_report_format: configpkg.Config.OSCColorReportFormat, @@ -185,7 +184,6 @@ pub const DerivedConfig = struct { .cursor_style = config.@"cursor-style", .cursor_blink = config.@"cursor-style-blink", .cursor_color = config.@"cursor-color", - .cursor_invert = config.@"cursor-invert-fg-bg", .foreground = config.foreground, .background = config.background, .osc_color_report_format = config.@"osc-color-report-format", @@ -265,8 +263,11 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { // Create our stream handler. This points to memory in self so it // isn't safe to use until self.* is set. const handler: StreamHandler = handler: { - const default_cursor_color = if (!opts.config.cursor_invert and opts.config.cursor_color != null) - opts.config.cursor_color.?.toTerminalRGB() + const default_cursor_color = if (opts.config.cursor_color) |color| + switch (color) { + .color => color.color.toTerminalRGB(), + else => null, + } else null; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 1b4fdd3aa..9946b0b8a 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -121,8 +121,11 @@ pub const StreamHandler = struct { self.default_background_color = config.background.toTerminalRGB(); self.default_cursor_style = config.cursor_style; self.default_cursor_blink = config.cursor_blink; - self.default_cursor_color = if (!config.cursor_invert and config.cursor_color != null) - config.cursor_color.?.toTerminalRGB() + self.default_cursor_color = if (config.cursor_color) |color| + switch (color) { + .color => color.color.toTerminalRGB(), + else => null, + } else null; From 95de1987615bd40f3c8afa0180bc1c0f1c184d7d Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 1 Jul 2025 23:04:58 -0400 Subject: [PATCH 109/114] Squash and rebase, updated refactored version with selection and cursor cell color changes --- src/config/Config.zig | 10 +-- src/renderer/generic.zig | 159 +++++++++++++++++---------------------- 2 files changed, 76 insertions(+), 93 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 76f91f6a8..1fc2f0f71 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -591,7 +591,7 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// the selection color is just the inverted window background and foreground /// (note: not to be confused with the cell bg/fg). /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -/// Since version 1.1.1, this can also be set to `cell-foreground` to match +/// Since version 1.2.0, this can also be set to `cell-foreground` to match /// the cell foreground color, or `cell-background` to match the cell /// background color. @"selection-foreground": ?DynamicColor = null, @@ -604,7 +604,7 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// If you select across cells with differing foregrounds and backgrounds, the /// selection color will vary across the selection. /// -/// Warning: This option has been deprecated as of version 1.1.1. Instead, +/// Warning: This option has been deprecated as of version 1.2.0. Instead, /// users should set `selection-foreground` and `selection-background` to /// `cell-background` and `cell-foreground`, respectively. @"selection-invert-fg-bg": bool = false, @@ -652,7 +652,7 @@ palette: Palette = .{}, /// The color of the cursor. If this is not set, a default will be chosen. /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -/// Since version 1.1.1, this can also be set to `cell-foreground` to match +/// Since version 1.2.0, this can also be set to `cell-foreground` to match /// the cell foreground color, or `cell-background` to match the cell /// background color. @"cursor-color": ?DynamicColor = null, @@ -660,7 +660,7 @@ palette: Palette = .{}, /// Swap the foreground and background colors of the cell under the cursor. This /// option overrides the `cursor-color` and `cursor-text` options. /// -/// Warning: This option has been deprecated as of version 1.1.1. Instead, +/// Warning: This option has been deprecated as of version 1.2.0. Instead, /// users should set `cursor-color` and `cursor-text` to `cell-foreground` and /// `cell-background`, respectively. @"cursor-invert-fg-bg": bool = false, @@ -713,7 +713,7 @@ palette: Palette = .{}, /// The color of the text under the cursor. If this is not set, a default will /// be chosen. /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -/// Since version 1.1.1, this can also be set to `cell-foreground` to match +/// Since version 1.2.0, this can also be set to `cell-foreground` to match /// the cell foreground color, or `cell-background` to match the cell /// background color. @"cursor-text": ?DynamicColor = null, diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 810e17686..617862e1c 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -133,12 +133,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// This is cursor color as set in the user's config, if any. If no cursor color /// is set in the user's config, then the cursor color is determined by the /// current foreground color. - default_cursor_color: ?terminal.color.RGB, - - /// When `cursor_color` is null, swap the foreground and background colors of - /// the cell under the cursor for the cursor color. Otherwise, use the default - /// foreground color as the cursor color. - cursor_invert: bool, + default_cursor_color: ?configpkg.Config.DynamicColor, /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of @@ -514,16 +509,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type { font_features: std.ArrayListUnmanaged([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, font_shaping_break: configpkg.FontShapingBreak, - cursor_color: ?terminal.color.RGB, - cursor_invert: bool, + cursor_color: ?configpkg.Config.DynamicColor, cursor_opacity: f64, - cursor_text: ?terminal.color.RGB, + cursor_text: ?configpkg.Config.DynamicColor, background: terminal.color.RGB, background_opacity: f64, foreground: terminal.color.RGB, - selection_background: ?terminal.color.RGB, - selection_foreground: ?terminal.color.RGB, - invert_selection_fg_bg: bool, + selection_background: ?configpkg.Config.DynamicColor, + selection_foreground: ?configpkg.Config.DynamicColor, bold_is_bright: bool, min_contrast: f32, padding_color: configpkg.WindowPaddingColor, @@ -571,8 +564,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { config.link.links.items, ); - const cursor_invert = config.@"cursor-invert-fg-bg"; - return .{ .background_opacity = @max(0, @min(1, config.@"background-opacity")), .font_thicken = config.@"font-thicken", @@ -581,36 +572,18 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .font_styles = font_styles, .font_shaping_break = config.@"font-shaping-break", - .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) - config.@"cursor-color".?.toTerminalRGB() - else - null, - - .cursor_invert = cursor_invert, - - .cursor_text = if (config.@"cursor-text") |txt| - txt.toTerminalRGB() - else - null, - + .cursor_color = config.@"cursor-color", + .cursor_text = config.@"cursor-text", .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")), .background = config.background.toTerminalRGB(), .foreground = config.foreground.toTerminalRGB(), - .invert_selection_fg_bg = config.@"selection-invert-fg-bg", .bold_is_bright = config.@"bold-is-bright", .min_contrast = @floatCast(config.@"minimum-contrast"), .padding_color = config.@"window-padding-color", - .selection_background = if (config.@"selection-background") |bg| - bg.toTerminalRGB() - else - null, - - .selection_foreground = if (config.@"selection-foreground") |bg| - bg.toTerminalRGB() - else - null, + .selection_background = config.@"selection-background", + .selection_foreground = config.@"selection-foreground", .custom_shaders = custom_shaders, .bg_image = bg_image, @@ -703,7 +676,6 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .default_background_color = options.config.background, .cursor_color = null, .default_cursor_color = options.config.cursor_color, - .cursor_invert = options.config.cursor_invert, // Render state .cells = .{}, @@ -2079,8 +2051,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Set our new colors self.default_background_color = config.background; self.default_foreground_color = config.foreground; - self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null; - self.cursor_invert = config.cursor_invert; + self.default_cursor_color = config.cursor_color; const bg_image_config_changed = self.config.bg_image_fit != config.bg_image_fit or @@ -2583,22 +2554,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // The final background color for the cell. const bg = bg: { if (selected) { - break :bg if (self.config.invert_selection_fg_bg) - if (style.flags.inverse) - // Cell is selected with invert selection fg/bg - // enabled, and the cell has the inverse style - // flag, so they cancel out and we get the normal - // bg color. - bg_style - else - // If it doesn't have the inverse style - // flag then we use the fg color instead. - fg_style + break :bg if (self.config.selection_background) |selection_color| + // Use the selection background if set, otherwise the default fg color. + switch (selection_color) { + .color => selection_color.color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, + } else - // If we don't have invert selection fg/bg set then we - // just use the selection background if set, otherwise - // the default fg color. - break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color; + self.foreground_color orelse self.default_foreground_color; } // Not selected @@ -2618,20 +2582,26 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; const fg = fg: { - if (selected and !self.config.invert_selection_fg_bg) { - // If we don't have invert selection fg/bg set - // then we just use the selection foreground if - // set, otherwise the default bg color. - break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color; - } + const final_bg = bg_style orelse self.background_color orelse self.default_background_color; // Whether we need to use the bg color as our fg color: + // - Cell is selected, inverted, and set to cell-foreground + // - Cell is selected, not inverted, and set to cell-background // - Cell is inverted and not selected - // - Cell is selected and not inverted - // Note: if selected then invert sel fg / bg must be - // false since we separately handle it if true above. - break :fg if (style.flags.inverse != selected) - bg_style orelse self.background_color orelse self.default_background_color + if (selected) { + // Use the selection foreground if set, otherwise the default bg color. + break :fg if (self.config.selection_foreground) |selection_color| + switch (selection_color) { + .color => selection_color.color.toTerminalRGB(), + .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, + .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, + } + else + self.background_color orelse self.default_background_color; + } + + break :fg if (style.flags.inverse) + final_bg else fg_style; }; @@ -2817,19 +2787,25 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Prepare the cursor cell contents. const style = cursor_style_ orelse break :cursor; - const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: { - if (self.cursor_invert) { - // Use the foreground color from the cell under the cursor, if any. - const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - break :color if (sty.flags.inverse) - // If the cell is reversed, use background color instead. - (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color) - else - (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color); - } else { - break :color self.foreground_color orelse self.default_foreground_color; + const cursor_color = self.cursor_color orelse if (self.default_cursor_color) |color| color: { + // If cursor-color is set, then compute the correct color. + // Otherwise, use the foreground color + if (color == .color) { + // Use the color set by cursor-color, if any. + break :color color.color.toTerminalRGB(); } - }; + + const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); + const fg_style = sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color; + const bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color; + + break :color switch (color) { + // If the cell is reversed, use the opposite cell color instead. + .@"cell-foreground" => if (sty.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (sty.flags.inverse) fg_style else bg_style, + else => unreachable, + }; + } else self.foreground_color orelse self.default_foreground_color; self.addCursor(screen, style, cursor_color); @@ -2853,18 +2829,25 @@ pub fn Renderer(comptime GraphicsAPI: type) type { .wide, .spacer_tail => true, }; - const uniform_color = if (self.cursor_invert) blk: { - // Use the background color from the cell under the cursor, if any. + const uniform_color = if (self.config.cursor_text) |txt| blk: { + // If cursor-text is set, then compute the correct color. + // Otherwise, use the background color. + if (txt == .color) { + // Use the color set by cursor-text, if any. + break :blk txt.color.toTerminalRGB(); + } + const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - break :blk if (sty.flags.inverse) - // If the cell is reversed, use foreground color instead. - (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color) - else - (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color); - } else if (self.config.cursor_text) |txt| - txt - else - self.background_color orelse self.default_background_color; + const fg_style = sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color; + const bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color; + + break :blk switch (txt) { + // If the cell is reversed, use the opposite cell color instead. + .@"cell-foreground" => if (sty.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (sty.flags.inverse) fg_style else bg_style, + else => unreachable, + }; + } else self.background_color orelse self.default_background_color; self.uniforms.cursor_color = .{ uniform_color.r, From e87e5e73614aa7ef68bcaaf5814ce0d26b2bb1ea Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Jul 2025 07:30:50 -0700 Subject: [PATCH 110/114] backwards compatibility handlers for removed fields --- src/config/Config.zig | 170 +++++++++++++++++++++++++++++++++--------- 1 file changed, 134 insertions(+), 36 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 1fc2f0f71..3cb808179 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -62,6 +62,13 @@ pub const compatibility = std.StaticStringMap( // Ghostty 1.2 removed the `hidden` value from `gtk-tabs-location` and // moved it to `window-show-tab-bar`. .{ "gtk-tabs-location", compatGtkTabsLocation }, + + // Ghostty 1.2 lets you set `cell-foreground` and `cell-background` + // to match the cell foreground and background colors, respectively. + // This can be used with `cursor-color` and `cursor-text` to recreate + // this behavior. This applies to selection too. + .{ "cursor-invert-fg-bg", compatCursorInvertFgBg }, + .{ "selection-invert-fg-bg", compatSelectionInvertFgBg }, }); /// The font families to use. @@ -597,18 +604,6 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, @"selection-foreground": ?DynamicColor = null, @"selection-background": ?DynamicColor = null, -/// Swap the foreground and background colors of cells for selection. This -/// option overrides the `selection-foreground` and `selection-background` -/// options. -/// -/// If you select across cells with differing foregrounds and backgrounds, the -/// selection color will vary across the selection. -/// -/// Warning: This option has been deprecated as of version 1.2.0. Instead, -/// users should set `selection-foreground` and `selection-background` to -/// `cell-background` and `cell-foreground`, respectively. -@"selection-invert-fg-bg": bool = false, - /// Whether to clear selected text when typing. This defaults to `true`. /// This is typical behavior for most terminal emulators as well as /// text input fields. If you set this to `false`, then the selected text @@ -651,19 +646,20 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, palette: Palette = .{}, /// The color of the cursor. If this is not set, a default will be chosen. -/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -/// Since version 1.2.0, this can also be set to `cell-foreground` to match -/// the cell foreground color, or `cell-background` to match the cell -/// background color. -@"cursor-color": ?DynamicColor = null, - -/// Swap the foreground and background colors of the cell under the cursor. This -/// option overrides the `cursor-color` and `cursor-text` options. /// -/// Warning: This option has been deprecated as of version 1.2.0. Instead, -/// users should set `cursor-color` and `cursor-text` to `cell-foreground` and -/// `cell-background`, respectively. -@"cursor-invert-fg-bg": bool = false, +/// Direct colors can be specified as either hex (`#RRGGBB` or `RRGGBB`) +/// or a named X11 color. +/// +/// Additionally, special values can be used to set the color to match +/// other colors at runtime: +/// +/// * `cell-foreground` - Match the cell foreground color. +/// (Available since version 1.2.0) +/// +/// * `cell-background` - Match the cell background color. +/// (Available since version 1.2.0) +/// +@"cursor-color": ?DynamicColor = null, /// The opacity level (opposite of transparency) of the cursor. A value of 1 /// is fully opaque and a value of 0 is fully transparent. A value less than 0 @@ -3843,10 +3839,6 @@ pub fn parseManuallyHook( return true; } -/// parseFieldManuallyFallback is a fallback called only when -/// parsing the field directly failed. It can be used to implement -/// backward compatibility. Since this is only called when parsing -/// fails, it doesn't impact happy-path performance. fn compatGtkTabsLocation( self: *Config, alloc: Allocator, @@ -3864,6 +3856,51 @@ fn compatGtkTabsLocation( return false; } +fn compatCursorInvertFgBg( + self: *Config, + alloc: Allocator, + key: []const u8, + value_: ?[]const u8, +) bool { + _ = alloc; + assert(std.mem.eql(u8, key, "cursor-invert-fg-bg")); + + // We don't do anything if the value is unset, which is technically + // not EXACTLY the same as prior behavior since it would fallback + // to doing whatever cursor-color/cursor-text were set to, but + // I don't want to store what that is separately so this is close + // enough. + // + // Realistically, these fields were mutually exclusive so anyone + // relying on that behavior should just upgrade to the new + // cursor-color/cursor-text fields. + const set = cli.args.parseBool(value_ orelse "t") catch return false; + if (set) { + self.@"cursor-color" = .@"cell-foreground"; + self.@"cursor-text" = .@"cell-background"; + } + + return true; +} + +fn compatSelectionInvertFgBg( + self: *Config, + alloc: Allocator, + key: []const u8, + value_: ?[]const u8, +) bool { + _ = alloc; + assert(std.mem.eql(u8, key, "selection-invert-fg-bg")); + + const set = cli.args.parseBool(value_ orelse "t") catch return false; + if (set) { + self.@"selection-foreground" = .@"cell-background"; + self.@"selection-background" = .@"cell-foreground"; + } + + return true; +} + /// Create a shallow copy of this config. This will share all the memory /// allocated with the previous config but will have a new arena for /// any changes or new allocations. The config should have `deinit` @@ -4437,28 +4474,41 @@ pub const DynamicColor = union(enum) { pub fn parseCLI(input_: ?[]const u8) !DynamicColor { const input = input_ orelse return error.ValueRequired; - if (std.mem.eql(u8, input, "cell-foreground")) return .@"cell-foreground"; if (std.mem.eql(u8, input, "cell-background")) return .@"cell-background"; - - return DynamicColor{ .color = try Color.parseCLI(input) }; + return .{ .color = try Color.parseCLI(input) }; } /// Used by Formatter pub fn formatEntry(self: DynamicColor, formatter: anytype) !void { switch (self) { .color => try self.color.formatEntry(formatter), - .@"cell-foreground", .@"cell-background" => try formatter.formatEntry([:0]const u8, @tagName(self)), + + .@"cell-foreground", + .@"cell-background", + => try formatter.formatEntry([:0]const u8, @tagName(self)), } } test "parseCLI" { const testing = std.testing; - try testing.expectEqual(DynamicColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } }, try DynamicColor.parseCLI("#4e2a84")); - try testing.expectEqual(DynamicColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } }, try DynamicColor.parseCLI("black")); - try testing.expectEqual(DynamicColor{.@"cell-foreground"}, try DynamicColor.parseCLI("cell-foreground")); - try testing.expectEqual(DynamicColor{.@"cell-background"}, try DynamicColor.parseCLI("cell-background")); + try testing.expectEqual( + DynamicColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } }, + try DynamicColor.parseCLI("#4e2a84"), + ); + try testing.expectEqual( + DynamicColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } }, + try DynamicColor.parseCLI("black"), + ); + try testing.expectEqual( + DynamicColor{.@"cell-foreground"}, + try DynamicColor.parseCLI("cell-foreground"), + ); + try testing.expectEqual( + DynamicColor{.@"cell-background"}, + try DynamicColor.parseCLI("cell-background"), + ); try testing.expectError(error.InvalidValue, DynamicColor.parseCLI("a")); } @@ -8107,3 +8157,51 @@ test "theme specifying light/dark sets theme usage in conditional state" { try testing.expect(cfg._conditional_set.contains(.theme)); } } + +test "compatibility: removed cursor-invert-fg-bg" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var cfg = try Config.default(alloc); + defer cfg.deinit(); + var it: TestIterator = .{ .data = &.{ + "--cursor-invert-fg-bg", + } }; + try cfg.loadIter(alloc, &it); + try cfg.finalize(); + + try testing.expectEqual( + DynamicColor.@"cell-foreground", + cfg.@"cursor-color", + ); + try testing.expectEqual( + DynamicColor.@"cell-background", + cfg.@"cursor-text", + ); + } +} + +test "compatibility: removed selection-invert-fg-bg" { + const testing = std.testing; + const alloc = testing.allocator; + + { + var cfg = try Config.default(alloc); + defer cfg.deinit(); + var it: TestIterator = .{ .data = &.{ + "--selection-invert-fg-bg", + } }; + try cfg.loadIter(alloc, &it); + try cfg.finalize(); + + try testing.expectEqual( + DynamicColor.@"cell-background", + cfg.@"selection-foreground", + ); + try testing.expectEqual( + DynamicColor.@"cell-foreground", + cfg.@"selection-background", + ); + } +} From 465ac5b1b7ad895951f459cfd4de578b14e0e741 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Jul 2025 09:25:55 -0700 Subject: [PATCH 111/114] clean up some of the color usage, use exhaustive switches --- src/config/Config.zig | 48 ++++++++-------- src/renderer/generic.zig | 100 +++++++++++++++++++++------------- src/termio/Termio.zig | 19 ++++--- src/termio/stream_handler.zig | 17 +++--- 4 files changed, 107 insertions(+), 77 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 3cb808179..5d9093bba 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -601,8 +601,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// Since version 1.2.0, this can also be set to `cell-foreground` to match /// the cell foreground color, or `cell-background` to match the cell /// background color. -@"selection-foreground": ?DynamicColor = null, -@"selection-background": ?DynamicColor = null, +@"selection-foreground": ?TerminalColor = null, +@"selection-background": ?TerminalColor = null, /// Whether to clear selected text when typing. This defaults to `true`. /// This is typical behavior for most terminal emulators as well as @@ -659,7 +659,7 @@ palette: Palette = .{}, /// * `cell-background` - Match the cell background color. /// (Available since version 1.2.0) /// -@"cursor-color": ?DynamicColor = null, +@"cursor-color": ?TerminalColor = null, /// The opacity level (opposite of transparency) of the cursor. A value of 1 /// is fully opaque and a value of 0 is fully transparent. A value less than 0 @@ -712,7 +712,7 @@ palette: Palette = .{}, /// Since version 1.2.0, this can also be set to `cell-foreground` to match /// the cell foreground color, or `cell-background` to match the cell /// background color. -@"cursor-text": ?DynamicColor = null, +@"cursor-text": ?TerminalColor = null, /// Enables the ability to move the cursor at prompts by using `alt+click` on /// Linux and `option+click` on macOS. @@ -4463,16 +4463,14 @@ pub const Color = struct { } }; -/// Represents the color values that can be set to a non-static value. -/// -/// Can either be a Color or one of the special values -/// "cell-foreground" or "cell-background". -pub const DynamicColor = union(enum) { +/// Represents color values that can also reference special color +/// values such as "cell-foreground" or "cell-background". +pub const TerminalColor = union(enum) { color: Color, @"cell-foreground", @"cell-background", - pub fn parseCLI(input_: ?[]const u8) !DynamicColor { + pub fn parseCLI(input_: ?[]const u8) !TerminalColor { const input = input_ orelse return error.ValueRequired; if (std.mem.eql(u8, input, "cell-foreground")) return .@"cell-foreground"; if (std.mem.eql(u8, input, "cell-background")) return .@"cell-background"; @@ -4480,7 +4478,7 @@ pub const DynamicColor = union(enum) { } /// Used by Formatter - pub fn formatEntry(self: DynamicColor, formatter: anytype) !void { + pub fn formatEntry(self: TerminalColor, formatter: anytype) !void { switch (self) { .color => try self.color.formatEntry(formatter), @@ -4494,23 +4492,23 @@ pub const DynamicColor = union(enum) { const testing = std.testing; try testing.expectEqual( - DynamicColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } }, - try DynamicColor.parseCLI("#4e2a84"), + TerminalColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } }, + try TerminalColor.parseCLI("#4e2a84"), ); try testing.expectEqual( - DynamicColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } }, - try DynamicColor.parseCLI("black"), + TerminalColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } }, + try TerminalColor.parseCLI("black"), ); try testing.expectEqual( - DynamicColor{.@"cell-foreground"}, - try DynamicColor.parseCLI("cell-foreground"), + TerminalColor{.@"cell-foreground"}, + try TerminalColor.parseCLI("cell-foreground"), ); try testing.expectEqual( - DynamicColor{.@"cell-background"}, - try DynamicColor.parseCLI("cell-background"), + TerminalColor{.@"cell-background"}, + try TerminalColor.parseCLI("cell-background"), ); - try testing.expectError(error.InvalidValue, DynamicColor.parseCLI("a")); + try testing.expectError(error.InvalidValue, TerminalColor.parseCLI("a")); } test "formatConfig" { @@ -4518,7 +4516,7 @@ pub const DynamicColor = union(enum) { var buf = std.ArrayList(u8).init(testing.allocator); defer buf.deinit(); - var sc: DynamicColor = .{.@"cell-foreground"}; + var sc: TerminalColor = .{.@"cell-foreground"}; try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); try testing.expectEqualSlices(u8, "a = cell-foreground\n", buf.items); } @@ -8172,11 +8170,11 @@ test "compatibility: removed cursor-invert-fg-bg" { try cfg.finalize(); try testing.expectEqual( - DynamicColor.@"cell-foreground", + TerminalColor.@"cell-foreground", cfg.@"cursor-color", ); try testing.expectEqual( - DynamicColor.@"cell-background", + TerminalColor.@"cell-background", cfg.@"cursor-text", ); } @@ -8196,11 +8194,11 @@ test "compatibility: removed selection-invert-fg-bg" { try cfg.finalize(); try testing.expectEqual( - DynamicColor.@"cell-background", + TerminalColor.@"cell-background", cfg.@"selection-foreground", ); try testing.expectEqual( - DynamicColor.@"cell-foreground", + TerminalColor.@"cell-foreground", cfg.@"selection-background", ); } diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 617862e1c..e7faf633f 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -133,7 +133,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// This is cursor color as set in the user's config, if any. If no cursor color /// is set in the user's config, then the cursor color is determined by the /// current foreground color. - default_cursor_color: ?configpkg.Config.DynamicColor, + default_cursor_color: ?configpkg.Config.TerminalColor, /// The current set of cells to render. This is rebuilt on every frame /// but we keep this around so that we don't reallocate. Each set of @@ -509,14 +509,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type { font_features: std.ArrayListUnmanaged([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, font_shaping_break: configpkg.FontShapingBreak, - cursor_color: ?configpkg.Config.DynamicColor, + cursor_color: ?configpkg.Config.TerminalColor, cursor_opacity: f64, - cursor_text: ?configpkg.Config.DynamicColor, + cursor_text: ?configpkg.Config.TerminalColor, background: terminal.color.RGB, background_opacity: f64, foreground: terminal.color.RGB, - selection_background: ?configpkg.Config.DynamicColor, - selection_foreground: ?configpkg.Config.DynamicColor, + selection_background: ?configpkg.Config.TerminalColor, + selection_foreground: ?configpkg.Config.TerminalColor, bold_is_bright: bool, min_contrast: f32, padding_color: configpkg.WindowPaddingColor, @@ -2548,21 +2548,31 @@ pub fn Renderer(comptime GraphicsAPI: type) type { else false; + // The `_style` suffixed values are the colors based on + // the cell style (SGR), before applying any additional + // configuration, inversions, selections, etc. const bg_style = style.bg(cell, color_palette); - const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color; + const fg_style = style.fg( + color_palette, + self.config.bold_is_bright, + ) orelse self.foreground_color orelse self.default_foreground_color; // The final background color for the cell. const bg = bg: { if (selected) { - break :bg if (self.config.selection_background) |selection_color| - // Use the selection background if set, otherwise the default fg color. - switch (selection_color) { - .color => selection_color.color.toTerminalRGB(), + // If we have an explicit selection background color + // specified int he config, use that + if (self.config.selection_background) |v| { + break :bg switch (v) { + .color => |color| color.toTerminalRGB(), .@"cell-foreground" => if (style.flags.inverse) bg_style else fg_style, .@"cell-background" => if (style.flags.inverse) fg_style else bg_style, - } - else - self.foreground_color orelse self.default_foreground_color; + }; + } + + // If no configuration, then our selection background + // is our foreground color. + break :bg self.foreground_color orelse self.default_foreground_color; } // Not selected @@ -2582,22 +2592,27 @@ pub fn Renderer(comptime GraphicsAPI: type) type { }; const fg = fg: { - const final_bg = bg_style orelse self.background_color orelse self.default_background_color; + // Our happy-path non-selection background color + // is our style or our configured defaults. + const final_bg = bg_style orelse + self.background_color orelse + self.default_background_color; // Whether we need to use the bg color as our fg color: // - Cell is selected, inverted, and set to cell-foreground // - Cell is selected, not inverted, and set to cell-background // - Cell is inverted and not selected if (selected) { - // Use the selection foreground if set, otherwise the default bg color. - break :fg if (self.config.selection_foreground) |selection_color| - switch (selection_color) { - .color => selection_color.color.toTerminalRGB(), + // Use the selection foreground if set + if (self.config.selection_foreground) |v| { + break :fg switch (v) { + .color => |color| color.toTerminalRGB(), .@"cell-foreground" => if (style.flags.inverse) final_bg else fg_style, .@"cell-background" => if (style.flags.inverse) fg_style else final_bg, - } - else - self.background_color orelse self.default_background_color; + }; + } + + break :fg self.background_color orelse self.default_background_color; } break :fg if (style.flags.inverse) @@ -2787,25 +2802,36 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Prepare the cursor cell contents. const style = cursor_style_ orelse break :cursor; - const cursor_color = self.cursor_color orelse if (self.default_cursor_color) |color| color: { - // If cursor-color is set, then compute the correct color. - // Otherwise, use the foreground color - if (color == .color) { - // Use the color set by cursor-color, if any. - break :color color.color.toTerminalRGB(); - } + const cursor_color = cursor_color: { + // If an explicit cursor color was set by OSC 12, use that. + if (self.cursor_color) |v| break :cursor_color v; - const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - const fg_style = sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color; - const bg_style = sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color; + // Use our configured color if specified + if (self.default_cursor_color) |v| switch (v) { + .color => |color| break :cursor_color color.toTerminalRGB(), + inline .@"cell-foreground", + .@"cell-background", + => |_, tag| { + const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); + const fg_style = sty.fg( + color_palette, + self.config.bold_is_bright, + ) orelse self.foreground_color orelse self.default_foreground_color; + const bg_style = sty.bg( + screen.cursor.page_cell, + color_palette, + ) orelse self.background_color orelse self.default_background_color; - break :color switch (color) { - // If the cell is reversed, use the opposite cell color instead. - .@"cell-foreground" => if (sty.flags.inverse) bg_style else fg_style, - .@"cell-background" => if (sty.flags.inverse) fg_style else bg_style, - else => unreachable, + break :cursor_color switch (tag) { + .color => unreachable, + .@"cell-foreground" => if (sty.flags.inverse) bg_style else fg_style, + .@"cell-background" => if (sty.flags.inverse) fg_style else bg_style, + }; + }, }; - } else self.foreground_color orelse self.default_foreground_color; + + break :cursor_color self.foreground_color orelse self.default_foreground_color; + }; self.addCursor(screen, style, cursor_color); diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index fda52c375..4b5b93641 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -163,7 +163,7 @@ pub const DerivedConfig = struct { image_storage_limit: usize, cursor_style: terminalpkg.CursorStyle, cursor_blink: ?bool, - cursor_color: ?configpkg.Config.DynamicColor, + cursor_color: ?configpkg.Config.TerminalColor, foreground: configpkg.Config.Color, background: configpkg.Config.Color, osc_color_report_format: configpkg.Config.OSCColorReportFormat, @@ -263,13 +263,16 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void { // Create our stream handler. This points to memory in self so it // isn't safe to use until self.* is set. const handler: StreamHandler = handler: { - const default_cursor_color = if (opts.config.cursor_color) |color| - switch (color) { - .color => color.color.toTerminalRGB(), - else => null, - } - else - null; + const default_cursor_color: ?terminalpkg.color.RGB = color: { + if (opts.config.cursor_color) |color| switch (color) { + .color => break :color color.color.toTerminalRGB(), + .@"cell-foreground", + .@"cell-background", + => {}, + }; + + break :color null; + }; break :handler .{ .alloc = alloc, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 9946b0b8a..040132f03 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -121,13 +121,16 @@ pub const StreamHandler = struct { self.default_background_color = config.background.toTerminalRGB(); self.default_cursor_style = config.cursor_style; self.default_cursor_blink = config.cursor_blink; - self.default_cursor_color = if (config.cursor_color) |color| - switch (color) { - .color => color.color.toTerminalRGB(), - else => null, - } - else - null; + self.default_cursor_color = color: { + if (config.cursor_color) |color| switch (color) { + .color => break :color color.color.toTerminalRGB(), + .@"cell-foreground", + .@"cell-background", + => {}, + }; + + break :color null; + }; // If our cursor is the default, then we update it immediately. if (self.default_cursor) self.setCursorStyle(.default) catch |err| { From 32764f3a1d8aab3043c11170e2b4c691c3316ca4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Jul 2025 09:29:36 -0700 Subject: [PATCH 112/114] fix test syntax --- src/config/Config.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 5d9093bba..e140785bb 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4500,11 +4500,11 @@ pub const TerminalColor = union(enum) { try TerminalColor.parseCLI("black"), ); try testing.expectEqual( - TerminalColor{.@"cell-foreground"}, + TerminalColor.@"cell-foreground", try TerminalColor.parseCLI("cell-foreground"), ); try testing.expectEqual( - TerminalColor{.@"cell-background"}, + TerminalColor.@"cell-background", try TerminalColor.parseCLI("cell-background"), ); @@ -4516,7 +4516,7 @@ pub const TerminalColor = union(enum) { var buf = std.ArrayList(u8).init(testing.allocator); defer buf.deinit(); - var sc: TerminalColor = .{.@"cell-foreground"}; + var sc: TerminalColor = .@"cell-foreground"; try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); try testing.expectEqualSlices(u8, "a = cell-foreground\n", buf.items); } From e1be836283e0824f7f37dc9d94404ec4723c9050 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Jul 2025 13:45:38 -0700 Subject: [PATCH 113/114] config: for now, make goto_tab NOT performable on macOS Fixes #7786 Fixes regression from #7683 This is a band-aid fix. The issue is that performable keybinds don't show up in the reverse mapping that GUI toolkits use to find their key equivalents. The full explanation of why is already in Binding.zig. For macOS, we have a way to validate menu items before they're triggered so we ideally do want a way to get reverse mappings even with performable keybinds. But I think this wants to be optional and that's all a bigger change. For now, this is a simple fix that will work. --- src/config/Config.zig | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index e140785bb..fec5b41fc 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5455,7 +5455,14 @@ pub const Keybinds = struct { .mods = mods, }, .{ .goto_tab = (i - start) + 1 }, - .{ .performable = true }, + .{ + // On macOS we keep this not performable so that the + // keyboard shortcuts in tabs work. In the future the + // correct fix is to fix the reverse mapping lookup + // to allow us to lookup performable keybinds + // conditionally. + .performable = !builtin.target.os.tag.isDarwin(), + }, ); } try self.set.putFlags( @@ -5465,7 +5472,10 @@ pub const Keybinds = struct { .mods = mods, }, .{ .last_tab = {} }, - .{ .performable = true }, + .{ + // See comment above with the numeric goto_tab + .performable = !builtin.target.os.tag.isDarwin(), + }, ); } From e494d94fb326c043e062ab5f60704e891f927371 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Jul 2025 21:14:14 -0700 Subject: [PATCH 114/114] Handle `exec` failures more gracefully Fixes #7792 Our error handling for `exec` failing within the forked process never actually worked! It triggered all sorts of issues. We didn't catch this before because it used to be exceptionally hard to fail an exec because we used to wrap ALL commands in a `/bin/sh -c`. However, we now support direction execution, most notably when you do `ghostty -e ` but also via the `direct:` prefix on configured commands. This fixes up our exec failure handling by printing a useful error message and avoiding any errdefers in the child which was causing the double-close. --- src/Command.zig | 27 +++++++++++++++++--- src/termio/Exec.zig | 60 ++++++++++++++++++++++----------------------- 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/src/Command.zig b/src/Command.zig index 7ed026efe..1bddf8b82 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -188,10 +188,31 @@ fn startPosix(self: *Command, arena: Allocator) !void { // Finally, replace our process. // Note: we must use the "p"-variant of exec here because we // do not guarantee our command is looked up already in the path. - _ = posix.execvpeZ(self.path, argsZ, envp) catch null; + const err = posix.execvpeZ(self.path, argsZ, envp); - // If we are executing this code, the exec failed. In that scenario, - // we return a very specific error that can be detected to determine + // If we are executing this code, the exec failed. We're in the + // child process so there isn't much we can do. We try to output + // something reasonable. Its important to note we MUST NOT return + // any other error condition from here on out. + const stderr = std.io.getStdErr().writer(); + switch (err) { + error.FileNotFound => stderr.print( + \\Requested executable not found. Please verify the command is on + \\the PATH and try again. + \\ + , + .{}, + ) catch {}, + + else => stderr.print( + \\exec syscall failed with unexpected error: {} + \\ + , + .{err}, + ) catch {}, + } + + // We return a very specific error that can be detected to determine // we're in the child. return error.ExecFailedInChild; } diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 598617a12..15b6b8cd4 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -90,15 +90,13 @@ pub fn threadEnter( // Start our subprocess const pty_fds = self.subprocess.start(alloc) catch |err| { // If we specifically got this error then we are in the forked - // process and our child failed to execute. In that case - if (err != error.Termio) return err; + // process and our child failed to execute. If we DIDN'T + // get this specific error then we're in the parent and + // we need to bubble it up. + if (err != error.ExecFailedInChild) return err; - // Output an error message about the exec faililng and exit. - // This generally should NOT happen because we always wrap - // our command execution either in login (macOS) or /bin/sh - // (Linux) which are usually guaranteed to exist. Still, we - // want to handle this scenario. - execFailedInChild() catch {}; + // We're in the child. Nothing more we can do but abnormal exit. + // The Command will output some additional information. posix.exit(1); }; errdefer self.subprocess.stop(); @@ -272,25 +270,6 @@ pub fn resize( return try self.subprocess.resize(grid_size, screen_size); } -/// This outputs an error message when exec failed and we are the -/// child process. This returns so the caller should probably exit -/// after calling this. -/// -/// Note that this usually is only called under very very rare -/// circumstances because we wrap our command execution in login -/// (macOS) or /bin/sh (Linux). So this output can be pretty crude -/// because it should never happen. Notably, this is not the error -/// users see when `command` is invalid. -fn execFailedInChild() !void { - const stderr = std.io.getStdErr().writer(); - try stderr.writeAll("exec failed\n"); - try stderr.writeAll("press any key to exit\n"); - - var buf: [1]u8 = undefined; - var reader = std.io.getStdIn().reader(); - _ = try reader.read(&buf); -} - fn processExitCommon(td: *termio.Termio.ThreadData, exit_code: u32) void { assert(td.backend == .exec); const execdata = &td.backend.exec; @@ -895,6 +874,12 @@ const Subprocess = struct { } { assert(self.pty == null and self.command == null); + // This function is funny because on POSIX systems it can + // fail in the forked process. This is flipped to true if + // we're in an error state in the forked process (child + // process). + var in_child: bool = false; + // Create our pty var pty = try Pty.open(.{ .ws_row = @intCast(self.grid_size.rows), @@ -903,14 +888,14 @@ const Subprocess = struct { .ws_ypixel = @intCast(self.screen_size.height), }); self.pty = pty; - errdefer { + errdefer if (!in_child) { if (comptime builtin.os.tag != .windows) { _ = posix.close(pty.slave); } pty.deinit(); self.pty = null; - } + }; log.debug("starting command command={s}", .{self.args}); @@ -1013,7 +998,22 @@ const Subprocess = struct { .data = self, .linux_cgroup = self.linux_cgroup, }; - try cmd.start(alloc); + + cmd.start(alloc) catch |err| { + // We have to do this because start on Windows can't + // ever return ExecFailedInChild + const StartError = error{ExecFailedInChild} || @TypeOf(err); + switch (@as(StartError, err)) { + // If we fail in our child we need to flag it so our + // errdefers don't run. + error.ExecFailedInChild => { + in_child = true; + return err; + }, + + else => return err, + } + }; errdefer killCommand(&cmd) catch |err| { log.warn("error killing command during cleanup err={}", .{err}); };