diff --git a/CODEOWNERS b/CODEOWNERS index 3d8a4da3d..56768d5ae 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. # @@ -161,6 +165,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 @@ -173,6 +178,8 @@ /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 +/po/ko_KR.UTF-8.po @ghostty-org/ko_KR # Packaging - Snap /snap/ @ghostty-org/snap 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 diff --git a/build.zig.zon b/build.zig.zon index e85958aaf..237720f35 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 = .{ @@ -20,14 +20,14 @@ }, .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 = .{ // 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 = .{ @@ -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..420893ef7 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,20 +54,20 @@ "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", "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", @@ -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", @@ -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 46345871b..6e4b86606 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="; }; } { @@ -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="; }; } { @@ -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="; }; } { @@ -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 b7cb2772f..f05a789dd 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -27,8 +27,8 @@ 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/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz -https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz +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/c9e917a4e15a983b672ca779c7985d738a2d517c.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/dist/linux/app.desktop b/dist/linux/app.desktop.in similarity index 72% rename from dist/linux/app.desktop rename to dist/linux/app.desktop.in index 6e464ea87..c39164158 100644 --- a/dist/linux/app.desktop +++ b/dist/linux/app.desktop.in @@ -1,13 +1,15 @@ [Desktop Entry] -Name=Ghostty +Version=1.0 +Name=@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=@APPID@ 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/com.mitchellh.ghostty.metainfo.xml b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in similarity index 94% rename from dist/linux/com.mitchellh.ghostty.metainfo.xml rename to dist/linux/com.mitchellh.ghostty.metainfo.xml.in index 0424d3a09..42ccc2754 100644 --- a/dist/linux/com.mitchellh.ghostty.metainfo.xml +++ b/dist/linux/com.mitchellh.ghostty.metainfo.xml.in @@ -1,8 +1,8 @@ - com.mitchellh.ghostty - com.mitchellh.ghostty.desktop - Ghostty + @APPID@ + @APPID@.desktop + @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 new file mode 100644 index 000000000..2f782a7ed --- /dev/null +++ b/dist/linux/dbus.service.in @@ -0,0 +1,4 @@ +[D-BUS 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 new file mode 100644 index 000000000..3ff848ddd --- /dev/null +++ b/dist/linux/systemd.service.in @@ -0,0 +1,11 @@ +[Unit] +Description=@NAME@ +After=graphical-session.target + +[Service] +Type=dbus +BusName=@APPID@ +ExecStart=@GHOSTTY@ --launched-from=systemd + +[Install] +WantedBy=graphical-session.target 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/flatpak/zig-packages.json b/flatpak/zig-packages.json index 32bd8bd54..daf7e5cea 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", @@ -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", @@ -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", @@ -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/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/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 { diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift index 5f4d6b177..996506f0b 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 static 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 = Self.hiddenStyleMask.union([.fullScreen]) + } else { + styleMask = Self.hiddenStyleMask + } // Hide the title titleVisibility = .hidden 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/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po new file mode 100644 index 000000000..18cadddf5 --- /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 "Копирано в клипборда" 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 "" diff --git a/po/es_AR.UTF-8.po b/po/es_AR.UTF-8.po new file mode 100644 index 000000000..3cd0625c8 --- /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 "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 +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 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" +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 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 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 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 a 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ás 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 "¿Cerrar división?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +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 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 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/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po new file mode 100644 index 000000000..686d22d76 --- /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-29 21:15+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.4\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 "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 "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 táb" + +#: 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 ordaithe" + +#: 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 " +"teirminéal, toisc go d'fhéadfadh 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 táib 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 "Tá an chumraíocht athlódáilte" + +#: 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 táb?" + +#: 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 teirminéil sa táb 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" diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po new file mode 100644 index 000000000..42cb2682f --- /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 "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ Ghostty 디버그 빌드로 실행 중입니다! 성능이 저하됩니다." + +#: src/apprt/gtk/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "설정값을 다시 불러왔습니다" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Ghostty 개발자들" 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 "已复制至剪贴板" diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index d7fc63712..df8d6ae53 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -76,13 +76,11 @@ 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 - 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: diff --git a/src/App.zig b/src/App.zig index 3bbeff2c8..02089ae5b 100644 --- a/src/App.zig +++ b/src/App.zig @@ -76,34 +76,38 @@ 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. /// /// 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,7 +118,13 @@ pub fn destroy(self: *App) void { // should gracefully close all surfaces. assert(self.font_grid_set.count() == 0); self.font_grid_set.deinit(); +} +pub fn destroy(self: *App) void { + // Deinitialize the app + self.deinit(); + + // Free the app memory self.alloc.destroy(self); } 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/Surface.zig b/src/Surface.zig index 6005635d9..dc7b0e3bf 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 @@ -237,6 +240,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 +259,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 +306,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 +325,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 +918,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) { @@ -945,9 +948,182 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { log.warn("apprt failed to ring bell={}", .{err}); }; }, + + .selection_scroll_tick => |active| { + self.selection_scroll_active = active; + 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; + + // 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()) { + // If the exit code is 0 then 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 +2129,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). @@ -3094,15 +3278,42 @@ 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) { + // 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. + 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 +3449,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 +3467,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 +3479,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 +3764,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(); } } @@ -3626,6 +3841,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. @@ -3708,13 +3932,16 @@ 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. + // 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 @@ -3768,13 +3995,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 +4033,7 @@ fn dragLeftClickTriple( } else { sel.endPtr().* = line.end(); } - try self.setSelection(sel); + try self.io.terminal.screen.select(sel); } fn dragLeftClickSingle( @@ -3815,7 +4042,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)), @@ -4685,6 +4912,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/apprt/embedded.zig b/src/apprt/embedded.zig index 31dd2f46b..dec1e4135 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,13 @@ pub const CAPI = struct { opts: *const apprt.runtime.App.Options, config: *const Config, ) !*App { - var core_app = try CoreApp.create(global.alloc); + const core_app = try CoreApp.create(global.alloc); errdefer core_app.destroy(); // 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; 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 7c9c15191..c61254fbd 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 @@ -373,6 +373,13 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { .{}, ); + // 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 @@ -405,11 +412,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. @@ -420,7 +431,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, @@ -1504,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 { @@ -1683,6 +1710,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 +1740,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/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/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/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index f911ccbc1..555edb1e4 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -55,6 +55,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, @@ -86,6 +89,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, @@ -106,6 +110,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", @@ -141,6 +146,7 @@ pub fn init(self: *Window, app: *App) !void { .config = .init(&app.config), .window = undefined, .headerbar = undefined, + .tab_bar = undefined, .tab_overview = null, .notebook = undefined, .titlebar_menu = undefined, @@ -225,8 +231,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"); @@ -238,8 +245,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"); @@ -385,21 +391,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)); @@ -414,23 +415,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. @@ -555,6 +551,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/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"); } }; } 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; } - }, - } + } + }, } } diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index dce6a3a56..9254b2fd5 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 { @@ -78,6 +79,13 @@ pub const Message = union(enum) { color: terminal.color.RGB, }, + /// 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, @@ -89,6 +97,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/build/Config.zig b/src/build/Config.zig index 8974e1f0c..5f8780af9 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -87,7 +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: bool = 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. @@ -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/GhosttyResources.zig b/src/build/GhosttyResources.zig index 640491fd6..34b5e35f8 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,83 +221,178 @@ 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, + ); + + return .{ .steps = steps.items }; +} + +/// 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(&b.addInstallFile( - b.path("dist/linux/app.desktop"), - "share/applications/com.mitchellh.ghostty.desktop", - ).step); + try ts.append(.{ + b.path("dist/linux/app.desktop.in"), + b.fmt("share/applications/{s}.desktop", .{app_id}), + }); - // 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); + // Service for DBus activation. + 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}), + }); - // 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); + // 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, + }, + ), + }); - // 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); + // 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}), + }); - // 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); - } + 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, + }); + + // 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( + tail.captureStdOut(), + template[1], + ); + + try steps.append(©.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); + + // 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 { diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index acd3ed1d8..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, @@ -652,14 +651,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); } } 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..1af74df69 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. @@ -147,7 +132,23 @@ 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)) { + log.info( + "compatibility handler for {s} handled error, you may be using a deprecated field: {}", + .{ key, err }, + ); + break :err; + } + } + } + if (comptime !canTrackDiags(T)) return err; // The error set is dependent on comptime T, so we always add @@ -177,6 +178,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 +454,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 +795,77 @@ 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, + b: 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 --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" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); @@ -1176,24 +1290,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/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, } 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 dee2fe10a..be0230da0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -46,14 +46,29 @@ 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 }, + + // 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. @@ -262,6 +277,32 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{ /// 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. @@ -557,16 +598,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, - -/// 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. -@"selection-invert-fg-bg": bool = false, +/// 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": ?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 @@ -610,12 +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. -@"cursor-color": ?Color = null, - -/// Swap the foreground and background colors of the cell under the cursor. This -/// option overrides the `cursor-color` and `cursor-text` options. -@"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": ?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 @@ -665,7 +709,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.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": ?TerminalColor = null, /// Enables the ability to move the cursor at prompts by using `alt+click` on /// Linux and `option+click` on macOS. @@ -1029,12 +1076,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). /// @@ -1581,6 +1633,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. @@ -2720,14 +2793,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. @@ -3785,6 +3858,68 @@ pub fn parseManuallyHook( return true; } +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; +} + +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` @@ -4347,6 +4482,65 @@ pub const Color = struct { } }; +/// 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) !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"; + return .{ .color = try Color.parseCLI(input) }; + } + + /// Used by Formatter + pub fn formatEntry(self: TerminalColor, 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( + TerminalColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } }, + try TerminalColor.parseCLI("#4e2a84"), + ); + try testing.expectEqual( + TerminalColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } }, + try TerminalColor.parseCLI("black"), + ); + try testing.expectEqual( + TerminalColor.@"cell-foreground", + try TerminalColor.parseCLI("cell-foreground"), + ); + try testing.expectEqual( + TerminalColor.@"cell-background", + try TerminalColor.parseCLI("cell-background"), + ); + + try testing.expectError(error.InvalidValue, TerminalColor.parseCLI("a")); + } + + test "formatConfig" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var sc: TerminalColor = .@"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(); @@ -4996,6 +5190,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 }) }, @@ -5274,7 +5474,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( @@ -5284,7 +5491,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(), + }, ); } @@ -6172,6 +6382,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(); @@ -6492,7 +6707,6 @@ pub const GtkSingleInstance = enum { pub const GtkTabsLocation = enum { top, bottom, - hidden, }; /// See gtk-toolbar-style @@ -6543,6 +6757,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, @@ -7965,3 +8186,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( + TerminalColor.@"cell-foreground", + cfg.@"cursor-color", + ); + try testing.expectEqual( + TerminalColor.@"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( + TerminalColor.@"cell-background", + cfg.@"selection-foreground", + ); + try testing.expectEqual( + TerminalColor.@"cell-foreground", + cfg.@"selection-background", + ); + } +} 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. diff --git a/src/font/Atlas.zig b/src/font/Atlas.zig index 969318943..7b31e2794 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); @@ -556,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 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/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; } 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 8e2c45c69..1aaa029dc 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; @@ -108,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); @@ -288,19 +290,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, + opts: font.shape.RunOptions, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, - .grid = grid, - .screen = screen, - .row = row, - .selection = selection, - .cursor_x = cursor_x, + .opts = opts, }; } @@ -594,13 +588,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); @@ -613,13 +605,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); @@ -633,13 +623,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); @@ -654,13 +642,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); @@ -701,13 +687,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); @@ -737,13 +721,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; @@ -772,13 +754,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; @@ -800,13 +780,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; @@ -825,13 +803,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; @@ -858,13 +834,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; @@ -892,13 +866,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; @@ -926,13 +898,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)).?; @@ -957,13 +927,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; @@ -990,13 +958,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; @@ -1040,13 +1006,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; @@ -1076,13 +1040,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; @@ -1111,13 +1073,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; @@ -1143,13 +1103,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; @@ -1181,13 +1139,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; @@ -1221,13 +1177,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; @@ -1257,17 +1211,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; @@ -1280,17 +1233,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; @@ -1303,17 +1255,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; @@ -1326,17 +1277,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; @@ -1349,17 +1299,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; @@ -1385,13 +1334,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; @@ -1400,61 +1347,111 @@ 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(.{ + .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; + _ = 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(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); + 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(.{ + .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; + _ = 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(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); + 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(.{ + .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; + _ = 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(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); + 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); } } @@ -1474,13 +1471,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; @@ -1493,13 +1488,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; @@ -1510,13 +1504,42 @@ 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 } }).?, + }); + 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(.{ + .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; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); + } + { + // Get our run iterator + var shaper = &testdata.shaper; + 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; @@ -1540,13 +1563,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; @@ -1564,13 +1585,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; @@ -1589,13 +1608,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; @@ -1614,13 +1631,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; @@ -1638,13 +1653,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; @@ -1678,13 +1691,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 361cbbe93..4209f795c 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; @@ -89,19 +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, + opts: font.shape.RunOptions, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, - .grid = grid, - .screen = screen, - .row = row, - .selection = selection, - .cursor_x = cursor_x, + .opts = opts, }; } @@ -225,13 +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); @@ -244,13 +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); @@ -264,13 +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; @@ -316,13 +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()); @@ -353,13 +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; @@ -382,13 +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; @@ -407,13 +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; @@ -440,13 +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; @@ -476,13 +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; @@ -513,13 +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; @@ -565,13 +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; @@ -603,13 +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; @@ -640,13 +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; @@ -674,13 +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; @@ -712,13 +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; @@ -752,13 +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; @@ -789,17 +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; @@ -812,17 +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; @@ -835,17 +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; @@ -858,17 +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; @@ -881,17 +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; @@ -917,13 +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; @@ -932,61 +886,111 @@ 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(.{ + .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; + _ = 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(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); + 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(.{ + .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; + _ = 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(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); + 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(.{ + .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; + _ = 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(.{ + .grid = testdata.grid, + .screen = &screen, + .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, + }); + 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); } } @@ -1006,13 +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; @@ -1025,13 +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, - ); + 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; @@ -1042,13 +1043,42 @@ 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 } }).?, + }); + 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(.{ + .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; + _ = try shaper.shape(run); + } + try testing.expectEqual(@as(usize, 1), count); + } + { + // Get our run iterator + var shaper = &testdata.shaper; + 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; @@ -1072,13 +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; @@ -1096,13 +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; @@ -1121,13 +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; @@ -1146,13 +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; @@ -1170,13 +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 f8988f4ee..8723071d7 100644 --- a/src/font/shaper/noop.zig +++ b/src/font/shaper/noop.zig @@ -70,19 +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, + opts: font.shape.RunOptions, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, - .grid = grid, - .screen = screen, - .row = row, - .selection = selection, - .cursor_x = cursor_x, + .opts = opts, }; } diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 18ddd4b56..92e629e19 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -35,15 +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, + 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: { @@ -58,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; } @@ -76,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; @@ -86,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; @@ -142,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; } @@ -162,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; @@ -186,7 +182,7 @@ pub const RunIterator = struct { // joiners will show the joiners allowing you to modify the // emoji. if (!cell.hasGrapheme()) { - if (self.cursor_x) |cursor_x| { + 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 @@ -227,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, @@ -235,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, @@ -273,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; @@ -298,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, }; } @@ -326,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, @@ -336,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, @@ -349,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); @@ -365,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, @@ -376,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 f38ab885a..4ed4b7db6 100644 --- a/src/font/shaper/web_canvas.zig +++ b/src/font/shaper/web_canvas.zig @@ -61,17 +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, + opts: font.shape.RunOptions, ) font.shape.RunIterator { return .{ .hooks = .{ .shaper = self }, - .group = group, - .row = row, - .selection = selection, - .cursor_x = cursor_x, + .opts = opts, }; } diff --git a/src/font/sprite.zig b/src/font/sprite.zig index 6485d6008..cf86fa6dd 100644 --- a/src/font/sprite.zig +++ b/src/font/sprite.zig @@ -32,12 +32,7 @@ pub const Sprite = enum(u32) { cursor_rect, 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. + cursor_underline, test { const testing = std.testing; 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..1463fb38b 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -16,25 +16,158 @@ 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: []const Range = 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; + } + + // 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 { + // 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 +185,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 +198,350 @@ 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 .{ + .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), + .sprite = true, }; } -/// 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..d16035996 --- /dev/null +++ b/src/font/sprite/draw/README.md @@ -0,0 +1,55 @@ +# 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..571f25a79 --- /dev/null +++ b/src/font/sprite/draw/block.zig @@ -0,0 +1,181 @@ +//! 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 fill = common.fill; + +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 { + 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 new file mode 100644 index 000000000..f14e5a3f9 --- /dev/null +++ b/src/font/sprite/draw/box.zig @@ -0,0 +1,932 @@ +//! 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 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); +} + +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..67b9dc778 --- /dev/null +++ b/src/font/sprite/draw/common.zig @@ -0,0 +1,378 @@ +//! 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; +}; + +/// 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 + eighth, + one_eighth, + two_eighths, + three_eighths, + four_eighths, + five_eighths, + six_eighths, + 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 + half, + one_half, + + // Alternative names for 1/2 + center, + middle, + + // Names for the max edge + end, + right, + bottom, + 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. + /// + /// `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)); + } + + /// 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, + .left, + .top, + .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, + .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, + }; + } +}; + +/// 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, + x0: Fraction, + x1: Fraction, + y0: Fraction, + y1: Fraction, +) void { + canvas.box( + x0.min(metrics.cell_width), + y0.min(metrics.cell_height), + x1.max(metrics.cell_width), + y1.max(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); +} 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..e41cac487 --- /dev/null +++ b/src/font/sprite/draw/special.zig @@ -0,0 +1,346 @@ +//! 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); +} + +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/font/sprite/draw/symbols_for_legacy_computing.zig b/src/font/sprite/draw/symbols_for_legacy_computing.zig new file mode 100644 index 000000000..164aa1ac3 --- /dev/null +++ b/src/font/sprite/draw/symbols_for_legacy_computing.zig @@ -0,0 +1,1415 @@ +//! 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 Fraction = common.Fraction; +const Corner = common.Corner; +const Quads = common.Quads; +const Edge = common.Edge; +const Shade = common.Shade; +const fill = common.fill; + +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, + ))); + 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 +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 top: f64 = 0.0; + 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 = Fraction.half.float(metrics.cell_width); + 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; + + fill( + metrics, + canvas, + Fraction.eighths[n], + Fraction.eighths[n + 1], + .top, + .bottom, + ); +} + +/// 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; + + fill( + metrics, + canvas, + .left, + .right, + Fraction.eighths[n], + Fraction.eighths[n + 1], + ); +} + +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); + } + } + } +} + +pub 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..f43949eb9 --- /dev/null +++ b/src/font/sprite/draw/symbols_for_legacy_computing_supplement.zig @@ -0,0 +1,628 @@ +//! 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 Fraction = common.Fraction; +const Corner = common.Corner; +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"); + +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 oct = octants[cp - octant_min]; + 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 +// 𜰡 𜰢 𜰣 𜰤 𜰥 𜰦 𜰧 𜰨 𜰩 𜰪 𜰫 𜰬 𜰭 𜰮 𜰯 +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, + ); +} + +/// 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, .tl), + // 𜰱 UPPER CENTRE LEFT TWELFTH CIRCLE + 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, .tr), + // 𜰳 UPPER RIGHT TWELFTH CIRCLE + 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, .tl), + // 𜰵 UPPER LEFT QUARTER CIRCLE + 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, .tr), + // 𜰷 UPPER MIDDLE RIGHT TWELFTH CIRCLE + 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, .bl), + // 𜰹 LOWER LEFT QUARTER CIRCLE + 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, .br), + // 𜰻 LOWER MIDDLE RIGHT TWELFTH CIRCLE + 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, .bl), + // 𜰽 LOWER CENTRE LEFT TWELFTH CIRCLE + 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, .br), + // 𜰿 LOWER RIGHT TWELFTH CIRCLE + 0x1CC3F => try circlePiece(canvas, width, height, metrics, 3, 3, 2, 2, .br), + else => unreachable, + } +} + +/// 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, + } +} + +/// 𜸀 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, + 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, + 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, + ); +} + +/// 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, + height: u32, + metrics: font.Metrics, + x: f64, + 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; + 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); + + switch (corner) { + .tl => { + path.moveTo(wdth - xp, ht - yp); + path.curveTo( + wdth - cw - xp, + ht - yp, + ht - xp, + hght - ch - yp, + ht - xp, + hght - yp, + ); + }, + .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, + hght + ch - yp, + wdth - cw - xp, + hght * 2 - ht - yp, + wdth - xp, + hght * 2 - ht - yp, + ); + }, + .br => { + 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); +} diff --git a/src/font/sprite/testdata/Box.ppm b/src/font/sprite/testdata/Box.ppm deleted file mode 100644 index 6082475af..000000000 Binary files a/src/font/sprite/testdata/Box.ppm and /dev/null differ 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 new file mode 100644 index 000000000..e04e7726b Binary files /dev/null and b/src/font/sprite/testdata/U+1CC00...U+1CCFF-11x21+2.png differ 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 000000000..ce1b1c422 Binary files /dev/null and b/src/font/sprite/testdata/U+1CC00...U+1CCFF-12x24+3.png differ 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 000000000..1c21f688a Binary files /dev/null and b/src/font/sprite/testdata/U+1CC00...U+1CCFF-18x36+4.png differ 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 new file mode 100644 index 000000000..459822e63 Binary files /dev/null and b/src/font/sprite/testdata/U+1CC00...U+1CCFF-9x17+1.png differ 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 000000000..8d7de36ac Binary files /dev/null and b/src/font/sprite/testdata/U+1CD00...U+1CDFF-11x21+2.png differ 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 000000000..ab6bec96d Binary files /dev/null and b/src/font/sprite/testdata/U+1CD00...U+1CDFF-12x24+3.png differ 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 000000000..43035aefb Binary files /dev/null and b/src/font/sprite/testdata/U+1CD00...U+1CDFF-18x36+4.png differ 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 000000000..fc111e2d7 Binary files /dev/null and b/src/font/sprite/testdata/U+1CD00...U+1CDFF-9x17+1.png differ 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 000000000..b3cda82d1 Binary files /dev/null and b/src/font/sprite/testdata/U+1CE00...U+1CEFF-11x21+2.png differ 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 000000000..e076da7c5 Binary files /dev/null and b/src/font/sprite/testdata/U+1CE00...U+1CEFF-12x24+3.png differ 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 000000000..366be3867 Binary files /dev/null and b/src/font/sprite/testdata/U+1CE00...U+1CEFF-18x36+4.png differ 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 000000000..5cd7a4efe Binary files /dev/null and b/src/font/sprite/testdata/U+1CE00...U+1CEFF-9x17+1.png differ 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 000000000..023396feb Binary files /dev/null and b/src/font/sprite/testdata/U+1FB00...U+1FBFF-11x21+2.png differ 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 000000000..2eff59c76 Binary files /dev/null and b/src/font/sprite/testdata/U+1FB00...U+1FBFF-12x24+3.png differ 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 000000000..b77f7dfae Binary files /dev/null and b/src/font/sprite/testdata/U+1FB00...U+1FBFF-18x36+4.png differ 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 000000000..062a7da81 Binary files /dev/null and b/src/font/sprite/testdata/U+1FB00...U+1FBFF-9x17+1.png differ 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 000000000..9ae5b45ff Binary files /dev/null and b/src/font/sprite/testdata/U+2500...U+25FF-11x21+2.png differ 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 new file mode 100644 index 000000000..89011df49 Binary files /dev/null and b/src/font/sprite/testdata/U+2500...U+25FF-12x24+3.png differ diff --git a/src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png b/src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png new file mode 100644 index 000000000..05ff2a5f2 Binary files /dev/null and b/src/font/sprite/testdata/U+2500...U+25FF-18x36+4.png differ 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 new file mode 100644 index 000000000..cf96b7d15 Binary files /dev/null and b/src/font/sprite/testdata/U+2500...U+25FF-9x17+1.png differ diff --git a/src/font/sprite/testdata/U+2800...U+28FF-11x21+2.png b/src/font/sprite/testdata/U+2800...U+28FF-11x21+2.png new file mode 100644 index 000000000..804e3017c Binary files /dev/null and b/src/font/sprite/testdata/U+2800...U+28FF-11x21+2.png differ 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 000000000..aa3e2f99b Binary files /dev/null and b/src/font/sprite/testdata/U+2800...U+28FF-12x24+3.png differ 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 000000000..49f431a43 Binary files /dev/null and b/src/font/sprite/testdata/U+2800...U+28FF-18x36+4.png differ 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 000000000..4308c5361 Binary files /dev/null and b/src/font/sprite/testdata/U+2800...U+28FF-9x17+1.png differ 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 000000000..addd1f496 Binary files /dev/null and b/src/font/sprite/testdata/U+E000...U+E0FF-11x21+2.png differ 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 000000000..ddfa79e85 Binary files /dev/null and b/src/font/sprite/testdata/U+E000...U+E0FF-12x24+3.png differ 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 new file mode 100644 index 000000000..787cbcb6c Binary files /dev/null and b/src/font/sprite/testdata/U+E000...U+E0FF-18x36+4.png differ diff --git a/src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png b/src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png new file mode 100644 index 000000000..461516971 Binary files /dev/null and b/src/font/sprite/testdata/U+E000...U+E0FF-9x17+1.png differ 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 000000000..103853790 Binary files /dev/null and b/src/font/sprite/testdata/U+F500...U+F5FF-11x21+2.png differ 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 000000000..b1bb803ab Binary files /dev/null and b/src/font/sprite/testdata/U+F500...U+F5FF-12x24+3.png differ 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 new file mode 100644 index 000000000..e7415b7ae Binary files /dev/null and b/src/font/sprite/testdata/U+F500...U+F5FF-18x36+4.png differ diff --git a/src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png b/src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png new file mode 100644 index 000000000..730a4c42c Binary files /dev/null and b/src/font/sprite/testdata/U+F500...U+F5FF-9x17+1.png differ 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 000000000..b216174e4 Binary files /dev/null and b/src/font/sprite/testdata/U+F600...U+F6FF-11x21+2.png differ 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 000000000..2243248b0 Binary files /dev/null and b/src/font/sprite/testdata/U+F600...U+F6FF-12x24+3.png differ 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 000000000..f3a887d20 Binary files /dev/null and b/src/font/sprite/testdata/U+F600...U+F6FF-18x36+4.png differ 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 new file mode 100644 index 000000000..82ce82468 Binary files /dev/null and b/src/font/sprite/testdata/U+F600...U+F6FF-9x17+1.png differ diff --git a/src/font/sprite/underline.zig b/src/font/sprite/underline.zig deleted file mode 100644 index d2e439e6a..000000000 --- a/src/font/sprite/underline.zig +++ /dev/null @@ -1,312 +0,0 @@ -//! This file renders underline sprites. To draw underlines, we render the -//! full cell-width as a sprite and then draw it as a separate pass to the -//! text. -//! -//! We used to render the underlines directly in the GPU shaders but its -//! annoying to support multiple types of underlines and its also annoying -//! to maintain and debug another set of shaders for each renderer instead of -//! just relying on the glyph system we already need to support for text -//! anyways. -//! -//! This also renders strikethrough, so its really more generally a -//! "horizontal line" renderer. -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 an underline. -pub fn renderGlyph( - alloc: Allocator, - atlas: *font.Atlas, - sprite: Sprite, - width: u32, - height: u32, - line_pos: u32, - line_thickness: u32, -) !font.Glyph { - // Draw the appropriate sprite - var canvas: font.sprite.Canvas, const offset_y: i32 = switch (sprite) { - .underline => 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/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", diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 985c6c9bd..567eec5f9 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -98,11 +98,12 @@ pub fn main() !MainReturn { } // Create our app state - var app = try App.create(alloc); + const app: *App = try App.create(alloc); defer app.destroy(); // 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 diff --git a/src/os/i18n.zig b/src/os/i18n.zig index c5fca6a78..a4d6c1577 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -39,12 +39,16 @@ pub const locales = [_][:0]const u8{ "ru_RU.UTF-8", "uk_UA.UTF-8", "pl_PL.UTF-8", + "ko_KR.UTF-8", "mk_MK.UTF-8", "tr_TR.UTF-8", "id_ID.UTF-8", "es_BO.UTF-8", + "es_AR.UTF-8", "pt_BR.UTF-8", "ca_ES.UTF-8", + "bg_BG.UTF-8", + "ga_IE.UTF-8", }; /// Set for faster membership lookup of locales. diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index e112c0df7..cf195361e 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", }; } @@ -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/generic.zig b/src/renderer/generic.zig index bf189fc4c..e7faf633f 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.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 @@ -513,16 +508,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type { font_thicken_strength: u8, font_features: std.ArrayListUnmanaged([:0]const u8), font_styles: font.CodepointResolver.StyleStatus, - cursor_color: ?terminal.color.RGB, - cursor_invert: bool, + font_shaping_break: configpkg.FontShapingBreak, + cursor_color: ?configpkg.Config.TerminalColor, cursor_opacity: f64, - cursor_text: ?terminal.color.RGB, + cursor_text: ?configpkg.Config.TerminalColor, 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.TerminalColor, + selection_foreground: ?configpkg.Config.TerminalColor, bold_is_bright: bool, min_contrast: f32, padding_color: configpkg.WindowPaddingColor, @@ -570,45 +564,26 @@ 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", .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() - 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, @@ -701,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 = .{}, @@ -2077,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 @@ -2467,13 +2440,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; @@ -2573,28 +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.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 - 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; + // 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, + }; + } + + // If no configuration, then our selection background + // is our foreground color. + break :bg self.foreground_color orelse self.default_foreground_color; } // Not selected @@ -2614,20 +2592,31 @@ 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; - } + // 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 - // - 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 + 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, + }; + } + + break :fg self.background_color orelse self.default_background_color; + } + + break :fg if (style.flags.inverse) + final_bg else fg_style; }; @@ -2813,18 +2802,35 @@ 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 = cursor_color: { + // If an explicit cursor color was set by OSC 12, use that. + if (self.cursor_color) |v| break :cursor_color v; + + // 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 :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, + }; + }, + }; + + break :cursor_color self.foreground_color orelse self.default_foreground_color; }; self.addCursor(screen, style, cursor_color); @@ -2849,18 +2855,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, @@ -3035,15 +3048,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, @@ -3094,7 +3114,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, }; 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 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)); 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/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; 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; diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 2688b03a7..079df37db 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -402,32 +402,47 @@ 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. // 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; } - break :start try pages.trackPin(.{ .node = pages.pages.first.? }); + // 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 change it - // to the bottom-right. - break :end try pages.trackPin(pages.pin(.{ .active = .{ - .x = 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 = .{ @@ -3053,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; @@ -3671,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(); @@ -3697,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(); @@ -3719,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(); @@ -5287,6 +5309,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; @@ -6003,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. @@ -7453,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 = .{} }); @@ -7461,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 = .{ @@ -7851,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("prompt3$ 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 = .{ @@ -7945,26 +7968,17 @@ 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("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, @@ -7981,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("prompt3$ 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 = .{ @@ -8081,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 = .{ @@ -8141,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 = .{ @@ -8196,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("prompt3$ 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( 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/Exec.zig b/src/termio/Exec.zig index aed7cefb6..15b6b8cd4 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"); @@ -89,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(); @@ -153,8 +152,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,102 +270,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. -/// -/// 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; @@ -386,63 +287,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 +414,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 +503,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, @@ -992,6 +826,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: { @@ -1031,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), @@ -1039,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}); @@ -1149,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}); }; diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index c474d55bb..4b5b93641 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -163,13 +163,11 @@ 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.TerminalColor, foreground: configpkg.Config.Color, background: configpkg.Config.Color, osc_color_report_format: configpkg.Config.OSCColorReportFormat, - abnormal_runtime_threshold_ms: u32, - wait_after_command: bool, + clipboard_write: configpkg.ClipboardAccess, enquiry_response: []const u8, pub fn init( @@ -186,12 +184,10 @@ 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", - .abnormal_runtime_threshold_ms = config.@"abnormal-command-exit-runtime", - .wait_after_command = config.@"wait-after-command", + .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 @@ -267,10 +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_invert and opts.config.cursor_color != null) - opts.config.cursor_color.?.toTerminalRGB() - 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, @@ -282,6 +284,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(), @@ -660,15 +663,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..7773ea7cd 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,10 +322,16 @@ 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, - .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, @@ -447,3 +467,57 @@ 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 self = cb.self; + + // Send the tick to the main surface + _ = cb.io.surface_mailbox.push( + .{ .selection_scroll_tick = self.scroll_active }, + .{ .instant = {} }, + ); + + 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/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..ee6dbcc0f 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"); @@ -47,6 +48,12 @@ pub const Message = union(enum) { /// Scroll the viewport scroll_viewport: terminal.Terminal.ScrollViewport, + /// 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. jump_to_prompt: isize, @@ -58,15 +65,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, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 90add84ae..040132f03 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,15 +115,22 @@ 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(); 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() - 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| { @@ -723,7 +733,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(.{ diff --git a/typos.toml b/typos.toml index fafc38858..1fb54ecc6 100644 --- a/typos.toml +++ b/typos.toml @@ -32,13 +32,13 @@ 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] 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"