Merge branch 'main' into ssh-integration

This commit is contained in:
Jason Rayne
2025-07-03 22:41:29 -07:00
137 changed files with 9860 additions and 6351 deletions

View File

@ -81,6 +81,10 @@
# - @ghostty-org/localization/* - Anything related to localization # - @ghostty-org/localization/* - Anything related to localization
# for a specific locale. # 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 # - @ghostty-org/macos - The Ghostty macOS app and any macOS-specific
# features, configurations, etc. # features, configurations, etc.
# #
@ -161,6 +165,7 @@
/po/ca_ES.UTF-8.po @ghostty-org/ca_ES /po/ca_ES.UTF-8.po @ghostty-org/ca_ES
/po/de_DE.UTF-8.po @ghostty-org/de_DE /po/de_DE.UTF-8.po @ghostty-org/de_DE
/po/es_BO.UTF-8.po @ghostty-org/es_BO /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/fr_FR.UTF-8.po @ghostty-org/fr_FR
/po/id_ID.UTF-8.po @ghostty-org/id_ID /po/id_ID.UTF-8.po @ghostty-org/id_ID
/po/ja_JP.UTF-8.po @ghostty-org/ja_JP /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/tr_TR.UTF-8.po @ghostty-org/tr_TR
/po/uk_UA.UTF-8.po @ghostty-org/uk_UA /po/uk_UA.UTF-8.po @ghostty-org/uk_UA
/po/zh_CN.UTF-8.po @ghostty-org/zh_CN /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 # Packaging - Snap
/snap/ @ghostty-org/snap /snap/ @ghostty-org/snap

21
TODO.md
View File

@ -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

View File

@ -8,8 +8,8 @@
.libxev = .{ .libxev = .{
// mitchellh/libxev // mitchellh/libxev
.url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz", .url = "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz",
.hash = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3", .hash = "libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw",
.lazy = true, .lazy = true,
}, },
.vaxis = .{ .vaxis = .{
@ -20,14 +20,14 @@
}, },
.z2d = .{ .z2d = .{
// vancluever/z2d // vancluever/z2d
.url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", .url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
.hash = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW", .hash = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg",
.lazy = true, .lazy = true,
}, },
.zig_objc = .{ .zig_objc = .{
// mitchellh/zig-objc // mitchellh/zig-objc
.url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz", .url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
.hash = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt", .hash = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
.lazy = true, .lazy = true,
}, },
.zig_js = .{ .zig_js = .{
@ -103,8 +103,8 @@
// Other // Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" }, .apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{ .iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz", .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz",
.hash = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX", .hash = "N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS",
.lazy = true, .lazy = true,
}, },
}, },

24
build.zig.zon.json generated
View File

@ -54,20 +54,20 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
}, },
"N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX": { "N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS": {
"name": "iterm2_themes", "name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz", "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz",
"hash": "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U=" "hash": "sha256-g9o2CIc/TfWYoUS/l/HP5KZECD7qNsdQUlFruaKkVz4="
}, },
"N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": {
"name": "libpng", "name": "libpng",
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz", "url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
"hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo=" "hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="
}, },
"libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3": { "libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw": {
"name": "libxev", "name": "libxev",
"url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz", "url": "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz",
"hash": "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU=" "hash": "sha256-/CSKSuZZfn0aIQlVZ0O8ch5O4gCajYBTTuoetRdo0n4="
}, },
"N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": { "N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": {
"name": "libxml2", "name": "libxml2",
@ -124,10 +124,10 @@
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=" "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
}, },
"z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW": { "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg": {
"name": "z2d", "name": "z2d",
"url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", "url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
"hash": "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U=" "hash": "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4="
}, },
"zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": { "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": {
"name": "zf", "name": "zf",
@ -144,10 +144,10 @@
"url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz", "url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
"hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0=" "hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="
}, },
"zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt": { "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk": {
"name": "zig_objc", "name": "zig_objc",
"url": "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz", "url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
"hash": "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw=" "hash": "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw="
}, },
"wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy": { "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy": {
"name": "zig_wayland", "name": "zig_wayland",

24
build.zig.zon.nix generated
View File

@ -170,11 +170,11 @@ in
}; };
} }
{ {
name = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX"; name = "N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS";
path = fetchZigArtifact { path = fetchZigArtifact {
name = "iterm2_themes"; name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz"; url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz";
hash = "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U="; 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 { path = fetchZigArtifact {
name = "libxev"; name = "libxev";
url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz"; url = "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz";
hash = "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU="; 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 { path = fetchZigArtifact {
name = "z2d"; name = "z2d";
url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz"; url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz";
hash = "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U="; 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 { path = fetchZigArtifact {
name = "zig_objc"; name = "zig_objc";
url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz"; url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz";
hash = "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw="; hash = "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw=";
}; };
} }
{ {

8
build.zig.zon.txt generated
View File

@ -27,8 +27,8 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.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/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz
https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz
https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz

View File

@ -1,13 +1,15 @@
[Desktop Entry] [Desktop Entry]
Name=Ghostty Version=1.0
Name=@NAME@
Type=Application Type=Application
Comment=A terminal emulator Comment=A terminal emulator
Exec=ghostty TryExec=@GHOSTTY@
Exec=@GHOSTTY@ --launched-from=desktop
Icon=com.mitchellh.ghostty Icon=com.mitchellh.ghostty
Categories=System;TerminalEmulator; Categories=System;TerminalEmulator;
Keywords=terminal;tty;pty; Keywords=terminal;tty;pty;
StartupNotify=true StartupNotify=true
StartupWMClass=com.mitchellh.ghostty StartupWMClass=@APPID@
Terminal=false Terminal=false
Actions=new-window; Actions=new-window;
X-GNOME-UsesNotifications=true X-GNOME-UsesNotifications=true
@ -16,7 +18,8 @@ X-TerminalArgTitle=--title=
X-TerminalArgAppId=--class= X-TerminalArgAppId=--class=
X-TerminalArgDir=--working-directory= X-TerminalArgDir=--working-directory=
X-TerminalArgHold=--wait-after-command X-TerminalArgHold=--wait-after-command
DBusActivatable=true
[Desktop Action new-window] [Desktop Action new-window]
Name=New Window Name=New Window
Exec=ghostty Exec=@GHOSTTY@ --launched-from=desktop

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application"> <component type="desktop-application">
<id>com.mitchellh.ghostty</id> <id>@APPID@</id>
<launchable type="desktop-id">com.mitchellh.ghostty.desktop</launchable> <launchable type="desktop-id">@APPID@.desktop</launchable>
<name>Ghostty</name> <name>@NAME@</name>
<url type="homepage">https://ghostty.org</url> <url type="homepage">https://ghostty.org</url>
<url type="help">https://ghostty.org/docs</url> <url type="help">https://ghostty.org/docs</url>
<url type="bugtracker">https://github.com/ghostty-org/ghostty/discussions</url> <url type="bugtracker">https://github.com/ghostty-org/ghostty/discussions</url>

3
dist/linux/dbus.service.flatpak.in vendored Normal file
View File

@ -0,0 +1,3 @@
[D-BUS Service]
Name=@APPID@
Exec=@GHOSTTY@ --launched-from=dbus

4
dist/linux/dbus.service.in vendored Normal file
View File

@ -0,0 +1,4 @@
[D-BUS Service]
Name=@APPID@
SystemdService=@APPID@.service
Exec=@GHOSTTY@ --launched-from=dbus

11
dist/linux/systemd.service.in vendored Normal file
View File

@ -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

View File

@ -6,11 +6,7 @@ sdk-extensions:
- org.freedesktop.Sdk.Extension.ziglang - org.freedesktop.Sdk.Extension.ziglang
default-branch: tip default-branch: tip
command: ghostty 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 rename-icon: com.mitchellh.ghostty
desktop-file-name-suffix: " (Debug)"
finish-args: finish-args:
# 3D rendering # 3D rendering
- --device=dri - --device=dri

View File

@ -67,9 +67,9 @@
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz", "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz",
"dest": "vendor/p/N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX", "dest": "vendor/p/N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS",
"sha256": "0bddcc4b2360c81fae86fccc4044c35ebefcdfd845c97ecd7d3329e0750ab375" "sha256": "83da3608873f4df598a144bf97f1cfe4a644083eea36c75052516bb9a2a4573e"
}, },
{ {
"type": "archive", "type": "archive",
@ -79,9 +79,9 @@
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz", "url": "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz",
"dest": "vendor/p/libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3", "dest": "vendor/p/libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw",
"sha256": "570141c83a29b6a88de5492894543b4db461c0c11145e0fd12bda98f14c26085" "sha256": "fc248a4ae6597e7d1a2109556743bc721e4ee2009a8d80534eea1eb51768d27e"
}, },
{ {
"type": "archive", "type": "archive",
@ -151,9 +151,9 @@
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", "url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
"dest": "vendor/p/z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW", "dest": "vendor/p/z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg",
"sha256": "c2226cebf2d48b2f80a42e6ced53f2a5b06e92306be2f8f1deffe5f4ead3ef45" "sha256": "c1f69d7a07a2c5c6e0c51cd1b5fe8cd87df8093baf0ccdb65d5221bae7c5046e"
}, },
{ {
"type": "archive", "type": "archive",
@ -175,9 +175,9 @@
}, },
{ {
"type": "archive", "type": "archive",
"url": "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz", "url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
"dest": "vendor/p/zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt", "dest": "vendor/p/zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
"sha256": "ce7d6d47ac614a60e56b8509dedf869e2e0d8b747c75e48aded11eec31b3357c" "sha256": "a37be5eea7e44a2d1b2976ba256b85f76a8c1063fc01ffec85c8a9e67468e4dc"
}, },
{ {
"type": "archive", "type": "archive",

View File

@ -112,6 +112,9 @@ class AppDelegate: NSObject,
/// The observer for the app appearance. /// The observer for the app appearance.
private var appearanceObserver: NSKeyValueObservation? = nil private var appearanceObserver: NSKeyValueObservation? = nil
/// Signals
private var signals: [DispatchSourceSignal] = []
/// The custom app icon image that is currently in use. /// The custom app icon image that is currently in use.
@Published private(set) var appIcon: NSImage? = nil { @Published private(set) var appIcon: NSImage? = nil {
didSet { didSet {
@ -249,6 +252,9 @@ class AppDelegate: NSObject,
// Setup our menu // Setup our menu
setupMenuImages() setupMenuImages()
// Setup signal handlers
setupSignals()
} }
func applicationDidBecomeActive(_ notification: Notification) { func applicationDidBecomeActive(_ notification: Notification) {
@ -406,6 +412,34 @@ class AppDelegate: NSObject,
return dockMenu 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. /// Setup all the images for our menu items.
private func setupMenuImages() { private func setupMenuImages() {
// Note: This COULD Be done all in the xib file, but I find it easier to // Note: This COULD Be done all in the xib file, but I find it easier to

View File

@ -610,14 +610,18 @@ extension SplitTree.Node {
return (self, 1) return (self, 1)
case .split(let split): case .split(let split):
// Recursively equalize children // Calculate weights based on split direction
let (leftNode, leftWeight) = split.left.equalizeWithWeight() let leftWeight = split.left.weightForDirection(split.direction)
let (rightNode, rightWeight) = split.right.equalizeWithWeight() let rightWeight = split.right.weightForDirection(split.direction)
// Calculate new ratio based on relative weights // Calculate new ratio based on relative weights
let totalWeight = leftWeight + rightWeight let totalWeight = leftWeight + rightWeight
let newRatio = Double(leftWeight) / Double(totalWeight) 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 // Create new split with equalized ratio
let newSplit = Split( let newSplit = Split(
direction: split.direction, 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 /// Calculate the bounds of all views in this subtree based on split ratios
func calculateViewBounds(in bounds: CGRect) -> [(view: ViewType, bounds: CGRect)] { func calculateViewBounds(in bounds: CGRect) -> [(view: ViewType, bounds: CGRect)] {
switch self { switch self {

View File

@ -19,20 +19,27 @@ class HiddenTitlebarTerminalWindow: TerminalWindow {
NotificationCenter.default.removeObserver(self) 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. /// Apply the hidden titlebar style.
private func reapplyHiddenStyle() { private func reapplyHiddenStyle() {
styleMask = [ // Apply our style mask while preserving the .fullScreen option
// We need `titled` in the mask to get the normal window frame if styleMask.contains(.fullScreen) {
.titled, styleMask = Self.hiddenStyleMask.union([.fullScreen])
} else {
// Full size content view so we can extend styleMask = Self.hiddenStyleMask
// content in to the hidden titlebar's area }
.fullSizeContentView,
.resizable,
.closable,
.miniaturizable,
]
// Hide the title // Hide the title
titleVisibility = .hidden titleVisibility = .hidden

View File

@ -1,3 +1,5 @@
const std = @import("std");
const c = @cImport({ const c = @cImport({
@cInclude("gtk4-layer-shell.h"); @cInclude("gtk4-layer-shell.h");
}); });
@ -31,6 +33,14 @@ pub fn getProtocolVersion() c_uint {
return c.gtk_layer_get_protocol_version(); 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 { pub fn initForWindow(window: *gtk.Window) void {
c.gtk_layer_init_for_window(@ptrCast(window)); c.gtk_layer_init_for_window(@ptrCast(window));
} }

275
po/bg_BG.UTF-8.po Normal file
View File

@ -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 <damyan.bogoev@gmail.com>, 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 <damyan.bogoev@gmail.com>\n"
"Language-Team: Bulgarian <dict@ludost.net>\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 "Копирано в клипборда"

View File

@ -1,5 +1,5 @@
# SOME DESCRIPTIVE TITLE. # 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. # This file is distributed under the same license as the com.mitchellh.ghostty package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
# #
@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n" "Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title."
msgstr "" msgstr ""
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 #: src/apprt/gtk/ui/1.5/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" msgid "Cancel"
msgstr "" msgstr ""
@ -35,22 +36,26 @@ msgid "OK"
msgstr "" msgstr ""
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 #: 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" msgid "Configuration Errors"
msgstr "" msgstr ""
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
msgid "" msgid ""
"One or more configuration errors were found. Please review the errors below, " "One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors." "and either reload your configuration or ignore these errors."
msgstr "" msgstr ""
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9
msgid "Ignore" msgid "Ignore"
msgstr "" msgstr ""
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: 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-surface-context_menu.blp:97
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 #: 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" msgid "Reload Configuration"
msgstr "" 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-surface-context_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_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" msgid "Paste"
msgstr "" 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-surface-context_menu.blp:62
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 #: 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" msgid "New Tab"
msgstr "" msgstr ""
@ -160,7 +165,7 @@ msgid "Terminal Inspector"
msgstr "" msgstr ""
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 #: 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" msgid "About Ghostty"
msgstr "" 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-read.blp:6
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.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" msgid "Authorize Clipboard Access"
msgstr "" msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 #: 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 "" msgid ""
"An application is attempting to read from the clipboard. The current " "An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below." "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-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.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" msgid "Deny"
msgstr "" msgstr ""
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: 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.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" msgid "Allow"
msgstr "" 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.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
msgid "" msgid ""
"An application is attempting to write to the clipboard. The current " "An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below." "clipboard contents are shown below."
msgstr "" 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" msgid "Warning: Potentially Unsafe Paste"
msgstr "" 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 "" msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some " "Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed." "commands may be executed."
msgstr "" msgstr ""
#: src/apprt/gtk/Window.zig:208 #: src/apprt/gtk/Window.zig:216
msgid "Main Menu" msgid "Main Menu"
msgstr "" msgstr ""
#: src/apprt/gtk/Window.zig:229 #: src/apprt/gtk/Window.zig:238
msgid "View Open Tabs" msgid "View Open Tabs"
msgstr "" msgstr ""
#: src/apprt/gtk/Window.zig:256 #: src/apprt/gtk/Window.zig:264
msgid "New Split" msgid "New Split"
msgstr "" msgstr ""
#: src/apprt/gtk/Window.zig:319 #: src/apprt/gtk/Window.zig:327
msgid "" msgid ""
"⚠️ You're running a debug build of Ghostty! Performance will be degraded." "⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr "" msgstr ""
#: src/apprt/gtk/Window.zig:765 #: src/apprt/gtk/Window.zig:773
msgid "Reloaded the configuration" msgid "Reloaded the configuration"
msgstr "" msgstr ""
#: src/apprt/gtk/Window.zig:1005 #: src/apprt/gtk/Window.zig:1017
msgid "Ghostty Developers" msgid "Ghostty Developers"
msgstr "" msgstr ""
@ -270,6 +293,6 @@ msgstr ""
msgid "The currently running process in this split will be terminated." msgid "The currently running process in this split will be terminated."
msgstr "" msgstr ""
#: src/apprt/gtk/Surface.zig:1243 #: src/apprt/gtk/Surface.zig:1257
msgid "Copied to clipboard" msgid "Copied to clipboard"
msgstr "" msgstr ""

285
po/es_AR.UTF-8.po Normal file
View File

@ -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 <alanmoyano203@gmail.com>, 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 <alanmoyano203@gmail.com>\n"
"Language-Team: Argentinian <es@tp.org.es>\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"

286
po/ga_IE.UTF-8.po Normal file
View File

@ -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 <aindriu80@gmail.com>, 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 <aindriu80@gmail.com>\n"
"Language-Team: Irish <gaeilge-gnulinux@lists.sourceforge.net>\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 ""
"Dfhé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"

259
po/ko_KR.UTF-8.po Normal file
View File

@ -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 <hey@rme.gg>, 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 <hey@rme.gg>\n"
"Language-Team: Korean <translation-team-ko@googlegroups.com>\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 개발자들"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n" "Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\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" "PO-Revision-Date: 2025-02-27 09:16+0100\n"
"Last-Translator: Leah <hi@pluie.me>\n" "Last-Translator: Leah <hi@pluie.me>\n"
"Language-Team: Chinese (simplified) <i18n-zh@googlegroups.com>\n" "Language-Team: Chinese (simplified) <i18n-zh@googlegroups.com>\n"
@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title."
msgstr "留空以重置至默认标题。" msgstr "留空以重置至默认标题。"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 #: src/apprt/gtk/ui/1.5/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" msgid "Cancel"
msgstr "取消" msgstr "取消"
@ -35,10 +36,12 @@ msgid "OK"
msgstr "确认" msgstr "确认"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 #: 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" msgid "Configuration Errors"
msgstr "配置错误" msgstr "配置错误"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
msgid "" msgid ""
"One or more configuration errors were found. Please review the errors below, " "One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors." "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.5/config-errors-dialog.blp:9
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9
msgid "Ignore" msgid "Ignore"
msgstr "忽略" msgstr "忽略"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: 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-surface-context_menu.blp:97
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 #: 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" msgid "Reload Configuration"
msgstr "重新加载配置" 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-surface-context_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_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" msgid "Paste"
msgstr "粘贴" 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-surface-context_menu.blp:62
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 #: 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" msgid "New Tab"
msgstr "新建标签页" msgstr "新建标签页"
@ -161,7 +166,7 @@ msgid "Terminal Inspector"
msgstr "终端调试器" msgstr "终端调试器"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 #: 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" msgid "About Ghostty"
msgstr "关于 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-read.blp:6
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.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" msgid "Authorize Clipboard Access"
msgstr "剪贴板访问授权" msgstr "剪贴板访问授权"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 #: 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 "" msgid ""
"An application is attempting to read from the clipboard. The current " "An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below." "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-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.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" msgid "Deny"
msgstr "拒绝" msgstr "拒绝"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: 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.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" msgid "Allow"
msgstr "允许" 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.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
msgid "" msgid ""
"An application is attempting to write to the clipboard. The current " "An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below." "clipboard contents are shown below."
msgstr "一个应用正在试图向剪贴板写入内容。剪贴板目前的内容如下:" 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" msgid "Warning: Potentially Unsafe Paste"
msgstr "警告:粘贴内容可能不安全" 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 "" msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some " "Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed." "commands may be executed."
msgstr "将以下内容粘贴至终端内将可能执行有害命令。" msgstr "将以下内容粘贴至终端内将可能执行有害命令。"
#: src/apprt/gtk/Window.zig:208 #: src/apprt/gtk/Window.zig:216
msgid "Main Menu" msgid "Main Menu"
msgstr "主菜单" msgstr "主菜单"
#: src/apprt/gtk/Window.zig:229 #: src/apprt/gtk/Window.zig:238
msgid "View Open Tabs" msgid "View Open Tabs"
msgstr "浏览标签页" msgstr "浏览标签页"
#: src/apprt/gtk/Window.zig:256 #: src/apprt/gtk/Window.zig:264
msgid "New Split" msgid "New Split"
msgstr "新建分屏" msgstr "新建分屏"
#: src/apprt/gtk/Window.zig:319 #: src/apprt/gtk/Window.zig:327
msgid "" msgid ""
"⚠️ You're running a debug build of Ghostty! Performance will be degraded." "⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。" msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。"
#: src/apprt/gtk/Window.zig:765 #: src/apprt/gtk/Window.zig:773
msgid "Reloaded the configuration" msgid "Reloaded the configuration"
msgstr "已重新加载配置" msgstr "已重新加载配置"
#: src/apprt/gtk/Window.zig:1005 #: src/apprt/gtk/Window.zig:1017
msgid "Ghostty Developers" msgid "Ghostty Developers"
msgstr "Ghostty 开发团队" msgstr "Ghostty 开发团队"
@ -271,6 +294,6 @@ msgstr "标签页内所有运行中的进程将被终止。"
msgid "The currently running process in this split will be terminated." msgid "The currently running process in this split will be terminated."
msgstr "分屏内正在运行中的进程将被终止。" msgstr "分屏内正在运行中的进程将被终止。"
#: src/apprt/gtk/Surface.zig:1243 #: src/apprt/gtk/Surface.zig:1257
msgid "Copied to clipboard" msgid "Copied to clipboard"
msgstr "已复制至剪贴板" msgstr "已复制至剪贴板"

View File

@ -76,13 +76,11 @@ parts:
- git - git
- patchelf - patchelf
- gettext - gettext
# TODO: Remove -fno-sys=gtk4-layer-shell when we upgrade to a version that packages it Ubuntu 24.10+
override-build: | override-build: |
craftctl set version=$(cat VERSION) 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/ 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 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: libs:

View File

@ -76,34 +76,38 @@ first: bool = true,
pub const CreateError = Allocator.Error || font.SharedGridSet.InitError; 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 /// Initialize the main app instance. This creates the main window, sets
/// up the renderer state, compiles the shaders, etc. This is the primary /// up the renderer state, compiles the shaders, etc. This is the primary
/// "startup" logic. /// "startup" logic.
/// ///
/// After calling this function, well behaved apprts should then call /// After calling this function, well behaved apprts should then call
/// `focusEvent` to set the initial focus state of the app. /// `focusEvent` to set the initial focus state of the app.
pub fn create( pub fn init(
self: *App,
alloc: Allocator, alloc: Allocator,
) CreateError!*App { ) CreateError!void {
var app = try alloc.create(App);
errdefer alloc.destroy(app);
var font_grid_set = try font.SharedGridSet.init(alloc); var font_grid_set = try font.SharedGridSet.init(alloc);
errdefer font_grid_set.deinit(); errdefer font_grid_set.deinit();
app.* = .{ self.* = .{
.alloc = alloc, .alloc = alloc,
.surfaces = .{}, .surfaces = .{},
.mailbox = .{}, .mailbox = .{},
.font_grid_set = font_grid_set, .font_grid_set = font_grid_set,
.config_conditional_state = .{}, .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 // Clean up all our surfaces
for (self.surfaces.items) |surface| surface.deinit(); for (self.surfaces.items) |surface| surface.deinit();
self.surfaces.deinit(self.alloc); self.surfaces.deinit(self.alloc);
@ -114,7 +118,13 @@ pub fn destroy(self: *App) void {
// should gracefully close all surfaces. // should gracefully close all surfaces.
assert(self.font_grid_set.count() == 0); assert(self.font_grid_set.count() == 0);
self.font_grid_set.deinit(); self.font_grid_set.deinit();
}
pub fn destroy(self: *App) void {
// Deinitialize the app
self.deinit();
// Free the app memory
self.alloc.destroy(self); self.alloc.destroy(self);
} }

View File

@ -188,10 +188,31 @@ fn startPosix(self: *Command, arena: Allocator) !void {
// Finally, replace our process. // Finally, replace our process.
// Note: we must use the "p"-variant of exec here because we // Note: we must use the "p"-variant of exec here because we
// do not guarantee our command is looked up already in the path. // 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, // If we are executing this code, the exec failed. We're in the
// we return a very specific error that can be detected to determine // 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. // we're in the child.
return error.ExecFailedInChild; return error.ExecFailedInChild;
} }

View File

@ -138,6 +138,9 @@ child_exited: bool = false,
/// to let us know. /// to let us know.
focused: bool = true, 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 effect of an input event. This can be used by callers to take
/// the appropriate action after an input event. For example, key /// the appropriate action after an input event. For example, key
/// input can be forwarded to the OS for further processing if it /// 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. /// For docs for these, see the associated config they are derived from.
original_font_size: f32, original_font_size: f32,
keybind: configpkg.Keybinds, keybind: configpkg.Keybinds,
abnormal_command_exit_runtime_ms: u32,
clipboard_read: configpkg.ClipboardAccess, clipboard_read: configpkg.ClipboardAccess,
clipboard_write: configpkg.ClipboardAccess, clipboard_write: configpkg.ClipboardAccess,
clipboard_trim_trailing_spaces: bool, clipboard_trim_trailing_spaces: bool,
@ -255,6 +259,7 @@ const DerivedConfig = struct {
macos_option_as_alt: ?configpkg.OptionAsAlt, macos_option_as_alt: ?configpkg.OptionAsAlt,
selection_clear_on_typing: bool, selection_clear_on_typing: bool,
vt_kam_allowed: bool, vt_kam_allowed: bool,
wait_after_command: bool,
window_padding_top: u32, window_padding_top: u32,
window_padding_bottom: u32, window_padding_bottom: u32,
window_padding_left: u32, window_padding_left: u32,
@ -301,6 +306,7 @@ const DerivedConfig = struct {
return .{ return .{
.original_font_size = config.@"font-size", .original_font_size = config.@"font-size",
.keybind = try config.keybind.clone(alloc), .keybind = try config.keybind.clone(alloc),
.abnormal_command_exit_runtime_ms = config.@"abnormal-command-exit-runtime",
.clipboard_read = config.@"clipboard-read", .clipboard_read = config.@"clipboard-read",
.clipboard_write = config.@"clipboard-write", .clipboard_write = config.@"clipboard-write",
.clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces", .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", .macos_option_as_alt = config.@"macos-option-as-alt",
.selection_clear_on_typing = config.@"selection-clear-on-typing", .selection_clear_on_typing = config.@"selection-clear-on-typing",
.vt_kam_allowed = config.@"vt-kam-allowed", .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_top = config.@"window-padding-y".top_left,
.window_padding_bottom = config.@"window-padding-y".bottom_right, .window_padding_bottom = config.@"window-padding-y".bottom_right,
.window_padding_left = config.@"window-padding-x".top_left, .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 => self.close(),
// Close without confirmation. .child_exited => |v| self.childExited(v),
.child_exited => {
self.child_exited = true;
self.close();
},
.desktop_notification => |notification| { .desktop_notification => |notification| {
if (!self.config.desktop_notifications) { 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}); 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. /// Called when the terminal detects there is a password input prompt.
fn passwordInput(self: *Surface, v: bool) !void { 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 (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. // 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 // We only do this on pressed events to avoid hiding the mouse when we
// change focus due to a keybinding (i.e. switching tabs). // 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 if (button == .left and action == .release) {
// reporting or any other mouse handling because a successfully // Stop selection scrolling when releasing the left mouse button
// clicked link will swallow the event. // but only when selection scrolling is active.
if (button == .left and action == .release and self.mouse.over_link) { if (self.selection_scroll_active) {
const pos = try self.rt_surface.getCursorPos(); self.io.queueMessage(
if (self.processLinks(pos)) |processed| { .{ .selection_scroll = false },
if (processed) return true; .unlocked,
} else |err| { );
log.warn("error processing links err={}", .{err}); }
// 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}); 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) { switch (self.mouse.left_click_count) {
// Single click // Single click
1 => { 1 => {
// If we have a selection, clear it. This always happens. // If we have a selection, clear it. This always happens.
if (self.io.terminal.screen.selection != null) { if (self.io.terminal.screen.selection != null) {
try self.setSelection(null); try self.io.terminal.screen.select(null);
try self.queueRender(); try self.queueRender();
} }
}, },
@ -3252,7 +3467,7 @@ pub fn mouseButtonCallback(
2 => { 2 => {
const sel_ = self.io.terminal.screen.selectWord(pin.*); const sel_ = self.io.terminal.screen.selectWord(pin.*);
if (sel_) |sel| { if (sel_) |sel| {
try self.setSelection(sel); try self.io.terminal.screen.select(sel);
try self.queueRender(); try self.queueRender();
} }
}, },
@ -3264,7 +3479,7 @@ pub fn mouseButtonCallback(
else else
self.io.terminal.screen.selectLine(.{ .pin = pin.* }); self.io.terminal.screen.selectLine(.{ .pin = pin.* });
if (sel_) |sel| { if (sel_) |sel| {
try self.setSelection(sel); try self.io.terminal.screen.select(sel);
try self.queueRender(); try self.queueRender();
} }
}, },
@ -3549,7 +3764,7 @@ pub fn mousePressureCallback(
// to handle state inconsistency here. // to handle state inconsistency here.
const pin = self.mouse.left_click_pin orelse break :select; const pin = self.mouse.left_click_pin orelse break :select;
const sel = self.io.terminal.screen.selectWord(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(); try self.queueRender();
} }
} }
@ -3626,6 +3841,15 @@ pub fn cursorPosCallback(
self.renderer_state.mutex.lock(); self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock(); 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 // 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 // want to set it when we're not selecting or doing any other mouse
// event. // event.
@ -3708,13 +3932,16 @@ pub fn cursorPosCallback(
// Note: one day, we can change this from distance to time based if we want. // Note: one day, we can change this from distance to time based if we want.
//log.warn("CURSOR POS: {} {}", .{ pos, self.size.screen }); //log.warn("CURSOR POS: {} {}", .{ pos, self.size.screen });
const max_y: f32 = @floatFromInt(self.size.screen.height); 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 // If the mouse is outside the viewport and we have the left
// at this cursor position. Right now, the user has to jiggle their // mouse button pressed then we need to start the scroll timer.
// mouse in order to scroll. 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 // Convert to points
@ -3768,13 +3995,13 @@ fn dragLeftClickDouble(
// If our current mouse position is before the starting position, // If our current mouse position is before the starting position,
// then the selection start is the word nearest our current position. // then the selection start is the word nearest our current position.
if (drag_pin.before(click_pin)) { if (drag_pin.before(click_pin)) {
try self.setSelection(terminal.Selection.init( try self.io.terminal.screen.select(.init(
word_current.start(), word_current.start(),
word_start.end(), word_start.end(),
false, false,
)); ));
} else { } else {
try self.setSelection(terminal.Selection.init( try self.io.terminal.screen.select(.init(
word_start.start(), word_start.start(),
word_current.end(), word_current.end(),
false, false,
@ -3806,7 +4033,7 @@ fn dragLeftClickTriple(
} else { } else {
sel.endPtr().* = line.end(); sel.endPtr().* = line.end();
} }
try self.setSelection(sel); try self.io.terminal.screen.select(sel);
} }
fn dragLeftClickSingle( fn dragLeftClickSingle(
@ -3815,7 +4042,7 @@ fn dragLeftClickSingle(
drag_x: f64, drag_x: f64,
) !void { ) !void {
// This logic is in a separate function so that it can be unit tested. // 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.?.*, self.mouse.left_click_pin.?.*,
drag_pin, drag_pin,
@intFromFloat(@max(0.0, self.mouse.left_click_xpos)), @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); const path = try tmp_dir.dir.realpath(filename, &path_buf);
switch (write_action) { 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), .open => try internal_os.open(self.alloc, .text, path),
.paste => self.io.queueMessage(try termio.Message.writeReq( .paste => self.io.queueMessage(try termio.Message.writeReq(
self.alloc, self.alloc,

View File

@ -117,10 +117,11 @@ pub const App = struct {
config: Config, config: Config,
pub fn init( pub fn init(
self: *App,
core_app: *CoreApp, core_app: *CoreApp,
config: *const Config, config: *const Config,
opts: Options, opts: Options,
) !App { ) !void {
// We have to clone the config. // We have to clone the config.
const alloc = core_app.alloc; const alloc = core_app.alloc;
var config_clone = try config.clone(alloc); var config_clone = try config.clone(alloc);
@ -129,7 +130,7 @@ pub const App = struct {
var keymap = try input.Keymap.init(); var keymap = try input.Keymap.init();
errdefer keymap.deinit(); errdefer keymap.deinit();
return .{ self.* = .{
.core_app = core_app, .core_app = core_app,
.config = config_clone, .config = config_clone,
.opts = opts, .opts = opts,
@ -1316,13 +1317,13 @@ pub const CAPI = struct {
opts: *const apprt.runtime.App.Options, opts: *const apprt.runtime.App.Options,
config: *const Config, config: *const Config,
) !*App { ) !*App {
var core_app = try CoreApp.create(global.alloc); const core_app = try CoreApp.create(global.alloc);
errdefer core_app.destroy(); errdefer core_app.destroy();
// Create our runtime app // Create our runtime app
var app = try global.alloc.create(App); var app = try global.alloc.create(App);
errdefer global.alloc.destroy(app); errdefer global.alloc.destroy(app);
app.* = try .init(core_app, config, opts.*); try app.init(core_app, config, opts.*);
errdefer app.terminate(); errdefer app.terminate();
return app; return app;

View File

@ -50,7 +50,7 @@ pub const App = struct {
pub const Options = 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()) { if (comptime builtin.target.os.tag.isDarwin()) {
log.warn("WARNING WARNING WARNING: GLFW ON MAC HAS BUGS.", .{}); log.warn("WARNING WARNING WARNING: GLFW ON MAC HAS BUGS.", .{});
log.warn("You should use the AppKit-based app instead. The official download", .{}); 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. // We want the event loop to wake up instantly so we can process our tick.
glfw.postEmptyEvent(); glfw.postEmptyEvent();
return .{ self.* = .{
.app = core_app, .app = core_app,
.config = config, .config = config,
.darwin = darwin, .darwin = darwin,

View File

@ -110,7 +110,7 @@ quit_timer: union(enum) {
expired: void, expired: void,
} = .{ .off = {} }, } = .{ .off = {} },
pub fn init(core_app: *CoreApp, opts: Options) !App { pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void {
_ = opts; _ = opts;
// Log our GTK version // 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 // 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: // loop so we have to do the same things the run function does:
// https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 // 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 // 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 // routine so we just call it, but only if the config allows it (this allows
// for launching Ghostty in the "background" without immediately opening // 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 // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
if (config.@"initial-window") if (config.@"initial-window") switch (config.@"launched-from".?) {
gio_app.activate(); .desktop, .cli => gio_app.activate(),
.dbus, .systemd => {},
};
// Internally, GTK ensures that only one instance of this provider exists in the provider list // Internally, GTK ensures that only one instance of this provider exists in the provider list
// for the display. // for the display.
@ -420,7 +431,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3,
); );
return .{ self.* = .{
.core_app = core_app, .core_app = core_app,
.app = adw_app, .app = adw_app,
.config = config, .config = config,
@ -1504,6 +1515,22 @@ pub fn quitNow(self: *App) void {
self.running = false; 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 /// 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. /// also when a secondary instance launches and requests a new window.
fn gtkActivate(_: *adw.Application, core_app: *CoreApp) callconv(.c) void { 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 is called to setup the action map that this application supports.
/// This should be called only once on startup. /// This should be called only once on startup.
fn initActions(self: *App) void { fn initActions(self: *App) void {
@ -1702,7 +1740,9 @@ fn initActions(self: *App) void {
.{ "reload-config", gtkActionReloadConfig, null }, .{ "reload-config", gtkActionReloadConfig, null },
.{ "present-surface", gtkActionPresentSurface, t }, .{ "present-surface", gtkActionPresentSurface, t },
.{ "show-gtk-inspector", gtkActionShowGTKInspector, null }, .{ "show-gtk-inspector", gtkActionShowGTKInspector, null },
.{ "new-window", gtkActionNewWindow, null },
}; };
inline for (actions) |entry| { inline for (actions) |entry| {
const action = gio.SimpleAction.new(entry[0], entry[2]); const action = gio.SimpleAction.new(entry[0], entry[2]);
defer action.unref(); defer action.unref();

View File

@ -17,7 +17,7 @@ const adw_version = @import("adw_version.zig");
const log = std.log.scoped(.gtk); 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, app: *App,
dialog: *DialogType, dialog: *DialogType,
@ -28,6 +28,7 @@ text_view: *gtk.TextView,
text_view_scroll: *gtk.ScrolledWindow, text_view_scroll: *gtk.ScrolledWindow,
reveal_button: *gtk.Button, reveal_button: *gtk.Button,
hide_button: *gtk.Button, hide_button: *gtk.Button,
remember_choice: if (adw_version.supportsSwitchRow()) ?*adw.SwitchRow else ?*anyopaque,
pub fn create( pub fn create(
app: *App, app: *App,
@ -89,6 +90,10 @@ fn init(
const reveal_button = builder.getObject(gtk.Button, "reveal_button").?; const reveal_button = builder.getObject(gtk.Button, "reveal_button").?;
const hide_button = builder.getObject(gtk.Button, "hide_button").?; const hide_button = builder.getObject(gtk.Button, "hide_button").?;
const text_view_scroll = builder.getObject(gtk.ScrolledWindow, "text_view_scroll").?; 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); const copy = try app.core_app.alloc.dupeZ(u8, data);
errdefer app.core_app.alloc.free(copy); errdefer app.core_app.alloc.free(copy);
@ -102,6 +107,7 @@ fn init(
.text_view_scroll = text_view_scroll, .text_view_scroll = text_view_scroll,
.reveal_button = reveal_button, .reveal_button = reveal_button,
.hide_button = hide_button, .hide_button = hide_button,
.remember_choice = remember_choice,
}; };
const buffer = gtk.TextBuffer.new(null); const buffer = gtk.TextBuffer.new(null);
@ -152,8 +158,10 @@ fn init(
} }
} }
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void { fn handleResponse(self: *ClipboardConfirmation, response: [*:0]const u8) void {
if (std.mem.orderZ(u8, response, "ok") == .eq) { const is_ok = std.mem.orderZ(u8, response, "ok") == .eq;
if (is_ok) {
self.core_surface.completeClipboardRequest( self.core_surface.completeClipboardRequest(
self.pending_req, self.pending_req,
self.data, self.data,
@ -162,8 +170,30 @@ fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation)
log.err("Failed to requeue clipboard request: {}", .{err}); 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(); 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 { fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true)); self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));

View File

@ -2325,6 +2325,15 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
env.remove("GDK_DISABLE"); env.remove("GDK_DISABLE");
env.remove("GSK_RENDERER"); 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. // Unset environment varies set by snaps if we're running in a snap.
// This allows Ghostty to further launch additional snaps. // This allows Ghostty to further launch additional snaps.
if (env.get("SNAP")) |_| { if (env.get("SNAP")) |_| {

View File

@ -55,6 +55,9 @@ window: *adw.ApplicationWindow,
/// The header bar for the window. /// The header bar for the window.
headerbar: HeaderBar, 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 /// The tab overview for the window. This is possibly null since there is no
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0). /// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
tab_overview: ?*adw.TabOverview, tab_overview: ?*adw.TabOverview,
@ -86,6 +89,7 @@ pub const DerivedConfig = struct {
gtk_tabs_location: configpkg.Config.GtkTabsLocation, gtk_tabs_location: configpkg.Config.GtkTabsLocation,
gtk_wide_tabs: bool, gtk_wide_tabs: bool,
gtk_toolbar_style: configpkg.Config.GtkToolbarStyle, gtk_toolbar_style: configpkg.Config.GtkToolbarStyle,
window_show_tab_bar: configpkg.Config.WindowShowTabBar,
quick_terminal_position: configpkg.Config.QuickTerminalPosition, quick_terminal_position: configpkg.Config.QuickTerminalPosition,
quick_terminal_size: configpkg.Config.QuickTerminalSize, quick_terminal_size: configpkg.Config.QuickTerminalSize,
@ -106,6 +110,7 @@ pub const DerivedConfig = struct {
.gtk_tabs_location = config.@"gtk-tabs-location", .gtk_tabs_location = config.@"gtk-tabs-location",
.gtk_wide_tabs = config.@"gtk-wide-tabs", .gtk_wide_tabs = config.@"gtk-wide-tabs",
.gtk_toolbar_style = config.@"gtk-toolbar-style", .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_position = config.@"quick-terminal-position",
.quick_terminal_size = config.@"quick-terminal-size", .quick_terminal_size = config.@"quick-terminal-size",
@ -141,6 +146,7 @@ pub fn init(self: *Window, app: *App) !void {
.config = .init(&app.config), .config = .init(&app.config),
.window = undefined, .window = undefined,
.headerbar = undefined, .headerbar = undefined,
.tab_bar = undefined,
.tab_overview = null, .tab_overview = null,
.notebook = undefined, .notebook = undefined,
.titlebar_menu = 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 we're using an AdwWindow then we can support the tab overview.
if (self.tab_overview) |tab_overview| { if (self.tab_overview) |tab_overview| {
if (!adw_version.supportsTabOverview()) unreachable; 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(); const btn = gtk.ToggleButton.new();
btn.as(gtk.Widget).setTooltipText(i18n._("View Open Tabs")); btn.as(gtk.Widget).setTooltipText(i18n._("View Open Tabs"));
btn.as(gtk.Button).setIconName("view-grid-symbolic"); 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); break :btn btn.as(gtk.Widget);
}, },
.never => btn: {
.hidden => btn: {
const btn = adw.TabButton.new(); const btn = adw.TabButton.new();
btn.setView(self.notebook.tab_view); btn.setView(self.notebook.tab_view);
btn.as(gtk.Actionable).setActionName("overview.open"); btn.as(gtk.Actionable).setActionName("overview.open");
@ -385,21 +391,16 @@ pub fn init(self: *Window, app: *App) !void {
// Our actions for the menu // Our actions for the menu
initActions(self); initActions(self);
self.tab_bar = adw.TabBar.new();
self.tab_bar.setView(self.notebook.tab_view);
if (adw_version.supportsToolbarView()) { if (adw_version.supportsToolbarView()) {
const toolbar_view = adw.ToolbarView.new(); const toolbar_view = adw.ToolbarView.new();
toolbar_view.addTopBar(self.headerbar.asWidget()); toolbar_view.addTopBar(self.headerbar.asWidget());
if (self.config.gtk_tabs_location != .hidden) { switch (self.config.gtk_tabs_location) {
const tab_bar = adw.TabBar.new(); .top => toolbar_view.addTopBar(self.tab_bar.as(gtk.Widget)),
tab_bar.setView(self.notebook.tab_view); .bottom => toolbar_view.addBottomBar(self.tab_bar.as(gtk.Widget)),
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,
}
} }
toolbar_view.setContent(box.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. // Set our application window content.
self.tab_overview.?.setChild(toolbar_view.as(gtk.Widget)); self.tab_overview.?.setChild(toolbar_view.as(gtk.Widget));
self.window.setContent(self.tab_overview.?.as(gtk.Widget)); self.window.setContent(self.tab_overview.?.as(gtk.Widget));
} else tab_bar: { } else {
if (self.config.gtk_tabs_location == .hidden) break :tab_bar;
// In earlier adwaita versions, we need to add the tabbar manually since we do not use // In earlier adwaita versions, we need to add the tabbar manually since we do not use
// an AdwToolbarView. // an AdwToolbarView.
const tab_bar = adw.TabBar.new(); self.tab_bar.as(gtk.Widget).addCssClass("inline");
tab_bar.as(gtk.Widget).addCssClass("inline");
switch (self.config.gtk_tabs_location) { switch (self.config.gtk_tabs_location) {
.top => box.insertChildAfter( .top => box.insertChildAfter(
tab_bar.as(gtk.Widget), self.tab_bar.as(gtk.Widget),
self.headerbar.asWidget(), self.headerbar.asWidget(),
), ),
.bottom => box.append(tab_bar.as(gtk.Widget)), .bottom => box.append(self.tab_bar.as(gtk.Widget)),
.hidden => unreachable,
} }
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. // 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| { self.winproto.syncAppearance() catch |err| {
log.warn("failed to sync winproto appearance error={}", .{err}); log.warn("failed to sync winproto appearance error={}", .{err});
}; };

View File

@ -109,6 +109,10 @@ pub inline fn supportsTabOverview() bool {
return atLeast(1, 4, 0); return atLeast(1, 4, 0);
} }
pub inline fn supportsSwitchRow() bool {
return atLeast(1, 4, 0);
}
pub inline fn supportsToolbarView() bool { pub inline fn supportsToolbarView() bool {
return atLeast(1, 4, 0); return atLeast(1, 4, 0);
} }

View File

@ -64,14 +64,18 @@ window.ssd.no-border-radius {
padding: 0; padding: 0;
} }
.clipboard-overlay {
border-radius: 10px;
}
.clipboard-content-view { .clipboard-content-view {
filter: blur(0px); filter: blur(0px);
transition: filter 0.3s ease; transition: filter 0.3s ease;
border-radius: 10px;
} }
.clipboard-content-view.blurred { .clipboard-content-view.blurred {
filter: blur(5px); filter: blur(5px);
transition: filter 0.3s ease;
} }
.command-palette-search { .command-palette-search {

View File

@ -14,58 +14,72 @@ Adw.AlertDialog clipboard_confirmation_window {
default-response: "cancel"; default-response: "cancel";
close-response: "cancel"; close-response: "cancel";
extra-child: Overlay { extra-child: ListBox {
selection-mode: none;
styles [ styles [
"osd", "boxed-list-separate",
] ]
ScrolledWindow text_view_scroll { Overlay {
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;
styles [ styles [
"opaque", "osd",
"clipboard-overlay",
] ]
Image { ScrolledWindow text_view_scroll {
icon-name: "view-conceal-symbolic"; 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");
} }
}; };
} }

View File

@ -14,58 +14,68 @@ Adw.AlertDialog clipboard_confirmation_window {
default-response: "cancel"; default-response: "cancel";
close-response: "cancel"; close-response: "cancel";
extra-child: Overlay { extra-child: ListBox {
selection-mode: none;
styles [ styles [
"osd", "boxed-list-separate",
] ]
ScrolledWindow text_view_scroll { Overlay {
width-request: 500; styles [
height-request: 250; "osd",
"clipboard-overlay",
]
TextView text_view { ScrolledWindow text_view_scroll {
cursor-visible: false; width-request: 500;
editable: false; height-request: 200;
monospace: true;
top-margin: 8; TextView text_view {
left-margin: 8; cursor-visible: false;
bottom-margin: 8; editable: false;
right-margin: 8; 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 [ styles [
"clipboard-content-view", "opaque",
] ]
} }
} }
[overlay] Adw.SwitchRow remember_choice {
Button reveal_button { title: _("Remember choice for this split");
visible: false; subtitle: _("Reload configuration to show this prompt again");
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";
}
} }
}; };
} }

View File

@ -37,6 +37,19 @@ pub const App = struct {
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null, default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
xdg_activation: ?*xdg.ActivationV1 = 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( pub fn init(
@ -95,11 +108,21 @@ pub const App = struct {
return null; return null;
} }
pub fn supportsQuickTerminal(_: App) bool { pub fn supportsQuickTerminal(self: App) bool {
if (!layer_shell.isSupported()) { if (!layer_shell.isSupported()) {
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{}); log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
return false; 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; return true;
} }
@ -111,26 +134,38 @@ pub const App = struct {
layer_shell.setNamespace(window, "ghostty-quick-terminal"); 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( fn registryListener(
registry: *wl.Registry, registry: *wl.Registry,
event: wl.Registry.Event, event: wl.Registry.Event,
context: *Context, context: *Context,
) void { ) void {
inline for (@typeInfo(Context).@"struct".fields) |field| { const ctx_fields = @typeInfo(Context).@"struct".fields;
// 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,
};
// Only process Wayland interfaces switch (event) {
if (!@hasDecl(T, "interface")) continue; .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( if (std.mem.orderZ(
u8, u8,
v.interface, v.interface,
@ -148,19 +183,22 @@ pub const App = struct {
); );
return; return;
}; };
}, }
},
// This should be a rare occurrence, but in case a global // This should be a rare occurrence, but in case a global
// is suddenly no longer available, we destroy and unset it // is suddenly no longer available, we destroy and unset it
// as the protocol mandates. // as the protocol mandates.
.global_remove => |v| remove: { .global_remove => |v| remove: {
inline for (ctx_fields) |field| {
if (getInterfaceType(field) == null) continue;
const global = @field(context, field.name) orelse break :remove; const global = @field(context, field.name) orelse break :remove;
if (global.getId() == v.name) { if (global.getId() == v.name) {
global.destroy(); global.destroy();
@field(context, field.name) = null; @field(context, field.name) = null;
} }
}, }
} },
} }
} }

View File

@ -43,8 +43,9 @@ pub const Message = union(enum) {
close: void, close: void,
/// The child process running in the surface has exited. This may trigger /// The child process running in the surface has exited. This may trigger
/// a surface close, it may not. /// a surface close, it may not. Additional details about the child
child_exited: void, /// command are given in the `ChildExited` struct.
child_exited: ChildExited,
/// Show a desktop notification. /// Show a desktop notification.
desktop_notification: struct { desktop_notification: struct {
@ -78,6 +79,13 @@ pub const Message = union(enum) {
color: terminal.color.RGB, 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. /// The terminal has reported a change in the working directory.
pwd_change: WriteReq, pwd_change: WriteReq,
@ -89,6 +97,11 @@ pub const Message = union(enum) {
// This enum is a placeholder for future title styles. // This enum is a placeholder for future title styles.
}; };
pub const ChildExited = struct {
exit_code: u32,
runtime_ms: u64,
};
}; };
/// A surface mailbox. /// A surface mailbox.

View File

@ -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 set to true when we're building a system package. For now
// this is trivially detected using the "system_package_mode" bool // this is trivially detected using the "system_package_mode" bool
// but we may want to make this more sophisticated in the future. // 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 // This specifies our target wasm runtime. For now only one semi-usable
// one exists so this is hardcoded. // one exists so this is hardcoded.
@ -361,7 +361,6 @@ pub fn init(b: *std.Build) !Config {
"libpng", "libpng",
"zlib", "zlib",
"oniguruma", "oniguruma",
"gtk4-layer-shell",
}) |dep| { }) |dep| {
_ = b.systemIntegrationOption( _ = b.systemIntegrationOption(
dep, dep,
@ -387,6 +386,15 @@ pub fn init(b: *std.Build) !Config {
}) |dep| { }) |dep| {
_ = b.systemIntegrationOption(dep, .{ .default = false }); _ = 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; return config;

View File

@ -2,6 +2,7 @@ const GhosttyResources = @This();
const std = @import("std"); const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const assert = std.debug.assert;
const buildpkg = @import("main.zig"); const buildpkg = @import("main.zig");
const Config = @import("Config.zig"); const Config = @import("Config.zig");
const config_vim = @import("../config/vim.zig"); const config_vim = @import("../config/vim.zig");
@ -220,83 +221,178 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
} }
// App (Linux) // App (Linux)
if (cfg.target.result.os.tag == .linux) { if (cfg.target.result.os.tag == .linux) try addLinuxAppResources(
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html 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 // Desktop file so that we have an icon and other metadata
try steps.append(&b.addInstallFile( try ts.append(.{
b.path("dist/linux/app.desktop"), b.path("dist/linux/app.desktop.in"),
"share/applications/com.mitchellh.ghostty.desktop", b.fmt("share/applications/{s}.desktop", .{app_id}),
).step); });
// AppStream metainfo so that application has rich metadata within app stores // Service for DBus activation.
try steps.append(&b.addInstallFile( try ts.append(.{
b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml"), if (cfg.flatpak)
"share/metainfo/com.mitchellh.ghostty.metainfo.xml", b.path("dist/linux/dbus.service.flatpak.in")
).step); 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 // systemd user service. This is kind of nasty but systemd
try steps.append(&b.addInstallFile( // looks for user services in different paths depending on
b.path("dist/linux/ghostty_dolphin.desktop"), // if we are installed as a system package or not (lib vs.
"share/kio/servicemenus/com.mitchellh.ghostty.desktop", // share) so we have to handle that here. We might be able
).step); // 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 // AppStream metainfo so that application has rich metadata
// `ghostty.py`. Using the full app id causes problems (see #5468). // within app stores
try steps.append(&b.addInstallFile( try ts.append(.{
b.path("dist/linux/ghostty_nautilus.py"), b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"),
"share/nautilus-python/extensions/ghostty.py", b.fmt("share/metainfo/{s}.metainfo.xml", .{app_id}),
).step); });
// Various icons that our application can use, including the icon break :templates ts.items;
// 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);
}
// 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(&copy.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( try steps.append(&b.addInstallFile(
b.path("images/icons/icon_16@2x.png"), b.path("images/icons/icon_1024.png"),
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png", "share/icons/hicolor/1024x1024/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); ).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 { pub fn install(self: *const GhosttyResources) void {

View File

@ -405,12 +405,11 @@ pub fn add(
})) |dep| { })) |dep| {
step.root_module.addImport("xev", dep.module("xev")); step.root_module.addImport("xev", dep.module("xev"));
} }
if (b.lazyDependency("z2d", .{})) |dep| { if (b.lazyDependency("z2d", .{
step.root_module.addImport("z2d", b.addModule("z2d", .{ .target = target,
.root_source_file = dep.path("src/z2d.zig"), .optimize = optimize,
.target = target, })) |dep| {
.optimize = optimize, step.root_module.addImport("z2d", dep.module("z2d"));
}));
} }
if (b.lazyDependency("ziglyph", .{ if (b.lazyDependency("ziglyph", .{
.target = target, .target = target,
@ -652,14 +651,13 @@ fn addGTK(
// IMPORTANT: gtk4-layer-shell must be linked BEFORE // IMPORTANT: gtk4-layer-shell must be linked BEFORE
// wayland-client, as it relies on shimming libwayland's APIs. // wayland-client, as it relies on shimming libwayland's APIs.
if (b.systemIntegrationOption("gtk4-layer-shell", .{})) { if (b.systemIntegrationOption("gtk4-layer-shell", .{})) {
step.linkSystemLibrary2( step.linkSystemLibrary2("gtk4-layer-shell-0", dynamic_link_opts);
"gtk4-layer-shell-0",
dynamic_link_opts,
);
} else { } else {
// gtk4-layer-shell *must* be dynamically linked, // gtk4-layer-shell *must* be dynamically linked,
// so we don't add it as a static library // 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);
} }
} }

View File

@ -2,6 +2,8 @@ const diags = @import("cli/diagnostics.zig");
pub const args = @import("cli/args.zig"); pub const args = @import("cli/args.zig");
pub const Action = @import("cli/action.zig").Action; 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 DiagnosticList = diags.DiagnosticList;
pub const Diagnostic = diags.Diagnostic; pub const Diagnostic = diags.Diagnostic;
pub const Location = diags.Location; pub const Location = diags.Location;

View File

@ -40,11 +40,14 @@ pub const Error = error{
/// "DiagnosticList" and any diagnostic messages will be added to that list. /// "DiagnosticList" and any diagnostic messages will be added to that list.
/// When diagnostics are present, only allocation errors will be returned. /// When diagnostics are present, only allocation errors will be returned.
/// ///
/// If the destination type has a decl "renamed", it must be of type /// If the destination type has a decl "compatibility", it must be of type
/// std.StaticStringMap([]const u8) and contains a mapping from the old /// std.StaticStringMap(CompatibilityHandler(T)), and it will be used to
/// field name to the new field name. This is used to allow renaming fields /// handle backwards compatibility for fields with the given name. The
/// while still supporting the old name. If a renamed field is set, parsing /// field name doesn't need to exist (so you can setup compatibility for
/// will automatically set the new field name. /// 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 /// 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. /// 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); const info = @typeInfo(T);
assert(info == .@"struct"); 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, // 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 // 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. // the config, then we reuse that. See memory note in parse docs.
@ -147,7 +132,23 @@ pub fn parse(
break :value null; 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; if (comptime !canTrackDiags(T)) return err;
// The error set is dependent on comptime T, so we always add // 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( fn formatValueRequired(
comptime T: type, comptime T: type,
arena_alloc: std.mem.Allocator, 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; 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" { test "parseIntoField: ignore underscore-prefixed fields" {
const testing = std.testing; const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator); 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 /// An iterator that considers its location to be CLI args. It
/// iterates through an underlying iterator and increments a counter /// iterates through an underlying iterator and increments a counter
/// to track the current CLI arg index. /// to track the current CLI arg index.

View File

@ -176,7 +176,7 @@ const Boo = struct {
pub fn run(gpa: Allocator) !u8 { pub fn run(gpa: Allocator) !u8 {
// Disable on non-desktop systems. // Disable on non-desktop systems.
switch (builtin.os.tag) { switch (builtin.os.tag) {
.windows, .macos, .linux => {}, .windows, .macos, .linux, .freebsd => {},
else => return 1, else => return 1,
} }

View File

@ -20,6 +20,7 @@ pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
pub const CopyOnSelect = Config.CopyOnSelect; pub const CopyOnSelect = Config.CopyOnSelect;
pub const CustomShaderAnimation = Config.CustomShaderAnimation; pub const CustomShaderAnimation = Config.CustomShaderAnimation;
pub const FontSyntheticStyle = Config.FontSyntheticStyle; pub const FontSyntheticStyle = Config.FontSyntheticStyle;
pub const FontShapingBreak = Config.FontShapingBreak;
pub const FontStyle = Config.FontStyle; pub const FontStyle = Config.FontStyle;
pub const FreetypeLoadFlags = Config.FreetypeLoadFlags; pub const FreetypeLoadFlags = Config.FreetypeLoadFlags;
pub const Keybinds = Config.Keybinds; pub const Keybinds = Config.Keybinds;

View File

@ -46,14 +46,29 @@ const c = @cImport({
@cInclude("unistd.h"); @cInclude("unistd.h");
}); });
/// Renamed fields, used by cli.parse pub const compatibility = std.StaticStringMap(
pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{ cli.CompatibilityHandler(Config),
).initComptime(&.{
// Ghostty 1.1 introduced background-blur support for Linux which // Ghostty 1.1 introduced background-blur support for Linux which
// doesn't support a specific radius value. The renaming is to let // doesn't support a specific radius value. The renaming is to let
// one field be used for both platforms (macOS retained the ability // one field be used for both platforms (macOS retained the ability
// to set a radius). // to set a radius).
.{ "background-blur-radius", "background-blur" }, .{ "background-blur-radius", cli.compatibilityRenamed(Config, "background-blur") },
.{ "adw-toolbar-style", "gtk-toolbar-style" },
// 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. /// The font families to use.
@ -262,6 +277,32 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
/// This is currently only supported on macOS. /// This is currently only supported on macOS.
@"font-thicken-strength": u8 = 255, @"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. /// What color space to use when performing alpha blending.
/// ///
/// This affects the appearance of text and of any images with transparency. /// 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 /// the selection color is just the inverted window background and foreground
/// (note: not to be confused with the cell bg/fg). /// (note: not to be confused with the cell bg/fg).
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
@"selection-foreground": ?Color = null, /// Since version 1.2.0, this can also be set to `cell-foreground` to match
@"selection-background": ?Color = null, /// the cell foreground color, or `cell-background` to match the cell
/// background color.
/// Swap the foreground and background colors of cells for selection. This @"selection-foreground": ?TerminalColor = null,
/// option overrides the `selection-foreground` and `selection-background` @"selection-background": ?TerminalColor = null,
/// 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,
/// Whether to clear selected text when typing. This defaults to `true`. /// Whether to clear selected text when typing. This defaults to `true`.
/// This is typical behavior for most terminal emulators as well as /// 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 = .{}, palette: Palette = .{},
/// The color of the cursor. If this is not set, a default will be chosen. /// 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, /// Direct colors can be specified as either hex (`#RRGGBB` or `RRGGBB`)
/// or a named X11 color.
/// Swap the foreground and background colors of the cell under the cursor. This ///
/// option overrides the `cursor-color` and `cursor-text` options. /// Additionally, special values can be used to set the color to match
@"cursor-invert-fg-bg": bool = false, /// 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 /// 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 /// 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 /// The color of the text under the cursor. If this is not set, a default will
/// be chosen. /// be chosen.
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. /// 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 /// Enables the ability to move the cursor at prompts by using `alt+click` on
/// Linux and `option+click` on macOS. /// Linux and `option+click` on macOS.
@ -1029,12 +1076,17 @@ title: ?[:0]const u8 = null,
/// The setting that will change the application class value. /// The setting that will change the application class value.
/// ///
/// This controls the class field of the `WM_CLASS` X11 property (when running /// 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 /// Note that changing this value between invocations will create new, separate
/// instances, of Ghostty when running with `gtk-single-instance=true`. See that /// instances, of Ghostty when running with `gtk-single-instance=true`. See that
/// option for more details. /// 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 /// The class name must follow the requirements defined [in the GTK
/// documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html). /// 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. /// * `end` - Insert the new tab at the end of the tab list.
@"window-new-tab-position": WindowNewTabPosition = .current, @"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 /// Background color for the window titlebar. This only takes effect if
/// window-theme is set to ghostty. Currently only supported in the GTK app /// window-theme is set to ghostty. Currently only supported in the GTK app
/// runtime. /// runtime.
@ -2720,14 +2793,14 @@ else
/// ///
/// GTK CSS documentation can be found at the following links: /// GTK CSS documentation can be found at the following links:
/// ///
/// * <https://docs.gtk.org/gtk4/css-overview.html> - An overview of GTK CSS. /// * https://docs.gtk.org/gtk4/css-overview.html - An overview of GTK CSS.
/// * <https://docs.gtk.org/gtk4/css-properties.html> - A comprehensive list /// * https://docs.gtk.org/gtk4/css-properties.html - A comprehensive list
/// of supported CSS properties. /// of supported CSS properties.
/// ///
/// Launch Ghostty with `env GTK_DEBUG=interactive ghostty` to tweak Ghostty's /// 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 /// 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 /// also be reported in the terminal you started Ghostty from. See
/// <https://developer.gnome.org/documentation/tools/inspector.html> for more /// https://developer.gnome.org/documentation/tools/inspector.html for more
/// information about the GTK Inspector. /// information about the GTK Inspector.
/// ///
/// This configuration can be repeated multiple times to load multiple files. /// This configuration can be repeated multiple times to load multiple files.
@ -3785,6 +3858,68 @@ pub fn parseManuallyHook(
return true; 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 /// 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 /// allocated with the previous config but will have a new arena for
/// any changes or new allocations. The config should have `deinit` /// 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 { pub const ColorList = struct {
const Self = @This(); const Self = @This();
@ -4996,6 +5190,12 @@ pub const Keybinds = struct {
.{ .reset_font_size = {} }, .{ .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( try self.set.put(
alloc, alloc,
.{ .key = .{ .unicode = 'j' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .{ .key = .{ .unicode = 'j' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
@ -5274,7 +5474,14 @@ pub const Keybinds = struct {
.mods = mods, .mods = mods,
}, },
.{ .goto_tab = (i - start) + 1 }, .{ .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( try self.set.putFlags(
@ -5284,7 +5491,10 @@ pub const Keybinds = struct {
.mods = mods, .mods = mods,
}, },
.{ .last_tab = {} }, .{ .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, @"bold-italic": bool = true,
}; };
/// See "font-shaping-break" for documentation
pub const FontShapingBreak = packed struct {
cursor: bool = true,
};
/// See "link" for documentation. /// See "link" for documentation.
pub const RepeatableLink = struct { pub const RepeatableLink = struct {
const Self = @This(); const Self = @This();
@ -6492,7 +6707,6 @@ pub const GtkSingleInstance = enum {
pub const GtkTabsLocation = enum { pub const GtkTabsLocation = enum {
top, top,
bottom, bottom,
hidden,
}; };
/// See gtk-toolbar-style /// See gtk-toolbar-style
@ -6543,6 +6757,13 @@ pub const WindowNewTabPosition = enum {
end, end,
}; };
/// See window-show-tab-bar
pub const WindowShowTabBar = enum {
always,
auto,
never,
};
/// See resize-overlay /// See resize-overlay
pub const ResizeOverlay = enum { pub const ResizeOverlay = enum {
always, always,
@ -7965,3 +8186,51 @@ test "theme specifying light/dark sets theme usage in conditional state" {
try testing.expect(cfg._conditional_set.contains(.theme)); 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",
);
}
}

View File

@ -20,10 +20,10 @@ pub fn open(alloc_gpa: Allocator) !void {
// Use an arena to make memory management easier in here. // Use an arena to make memory management easier in here.
var arena = ArenaAllocator.init(alloc_gpa); var arena = ArenaAllocator.init(alloc_gpa);
defer arena.deinit(); defer arena.deinit();
const alloc = arena.allocator(); const alloc_arena = arena.allocator();
// Get the path we should open // Get the path we should open
const config_path = try configPath(alloc); const config_path = try configPath(alloc_arena);
// Create config directory recursively. // Create config directory recursively.
if (std.fs.path.dirname(config_path)) |config_dir| { 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. /// Returns the config path to use for open for the current OS.

View File

@ -251,6 +251,37 @@ pub fn set(self: *Atlas, reg: Region, data: []const u8) void {
_ = self.modified.fetchAdd(1, .monotonic); _ = 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. // 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 { pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void {
assert(size_new >= self.size); assert(size_new >= self.size);
@ -556,6 +587,35 @@ test "writing data" {
try testing.expectEqual(@as(u8, 4), atlas.data[66]); 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" { test "grow" {
const alloc = testing.allocator; const alloc = testing.allocator;
var atlas = try init(alloc, 4, .grayscale); // +2 for 1px border var atlas = try init(alloc, 4, .grayscale); // +2 for 1px border

View File

@ -20,3 +20,6 @@ atlas_y: u32,
/// horizontal position to increase drawing position for strings /// horizontal position to increase drawing position for strings
advance_x: f32, advance_x: f32,
/// Whether we drew this glyph ourselves with the sprite font.
sprite: bool = false,

View File

@ -831,6 +831,9 @@ pub const CoreText = struct {
i: usize, i: usize,
pub fn deinit(self: *DiscoverIterator) void { pub fn deinit(self: *DiscoverIterator) void {
for (self.list) |desc| {
desc.release();
}
self.alloc.free(self.list); self.alloc.free(self.list);
self.* = undefined; self.* = undefined;
} }

View File

@ -2,6 +2,9 @@ const builtin = @import("builtin");
const options = @import("main.zig").options; const options = @import("main.zig").options;
const run = @import("shaper/run.zig"); const run = @import("shaper/run.zig");
const feature = @import("shaper/feature.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 noop = @import("shaper/noop.zig");
pub const harfbuzz = @import("shaper/harfbuzz.zig"); pub const harfbuzz = @import("shaper/harfbuzz.zig");
pub const coretext = @import("shaper/coretext.zig"); pub const coretext = @import("shaper/coretext.zig");
@ -61,6 +64,38 @@ pub const Options = struct {
features: []const []const u8 = &.{}, 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 { test {
_ = Cache; _ = Cache;
_ = Shaper; _ = Shaper;

View File

@ -7,6 +7,7 @@ const trace = @import("tracy").trace;
const font = @import("../main.zig"); const font = @import("../main.zig");
const os = @import("../../os/main.zig"); const os = @import("../../os/main.zig");
const terminal = @import("../../terminal/main.zig"); const terminal = @import("../../terminal/main.zig");
const config = @import("../../config.zig");
const Feature = font.shape.Feature; const Feature = font.shape.Feature;
const FeatureList = font.shape.FeatureList; const FeatureList = font.shape.FeatureList;
const default_features = font.shape.default_features; const default_features = font.shape.default_features;
@ -108,7 +109,8 @@ pub const Shaper = struct {
/// settings the font features of a CoreText font. /// settings the font features of a CoreText font.
fn makeFeaturesDict(feats: []const Feature) !*macos.foundation.Dictionary { fn makeFeaturesDict(feats: []const Feature) !*macos.foundation.Dictionary {
const list = try macos.foundation.MutableArray.create(); 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| { for (feats) |feat| {
const value_num: c_int = @intCast(feat.value); const value_num: c_int = @intCast(feat.value);
@ -288,19 +290,11 @@ pub const Shaper = struct {
pub fn runIterator( pub fn runIterator(
self: *Shaper, self: *Shaper,
grid: *SharedGrid, opts: font.shape.RunOptions,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection,
cursor_x: ?usize,
) font.shape.RunIterator { ) font.shape.RunIterator {
return .{ return .{
.hooks = .{ .shaper = self }, .hooks = .{ .shaper = self },
.grid = grid, .opts = opts,
.screen = screen,
.row = row,
.selection = selection,
.cursor_x = cursor_x,
}; };
} }
@ -594,13 +588,11 @@ test "run iterator" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
@ -613,13 +605,11 @@ test "run iterator" {
try screen.testWriteString("ABCD EFG"); try screen.testWriteString("ABCD EFG");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
@ -633,13 +623,11 @@ test "run iterator" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 3), count); try testing.expectEqual(@as(usize, 3), count);
@ -654,13 +642,11 @@ test "run iterator" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 2), count); try testing.expectEqual(@as(usize, 2), count);
@ -701,13 +687,11 @@ test "run iterator: empty cells with background set" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
{ {
const run = (try it.next(alloc)).?; const run = (try it.next(alloc)).?;
const cells = try shaper.shape(run); const cells = try shaper.shape(run);
@ -737,13 +721,11 @@ test "shape" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -772,13 +754,11 @@ test "shape nerd fonts" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -800,13 +780,11 @@ test "shape inconsolata ligs" {
try screen.testWriteString(">="); try screen.testWriteString(">=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -825,13 +803,11 @@ test "shape inconsolata ligs" {
try screen.testWriteString("==="); try screen.testWriteString("===");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -858,13 +834,11 @@ test "shape monaspace ligs" {
try screen.testWriteString("==="); try screen.testWriteString("===");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -892,13 +866,11 @@ test "shape left-replaced lig in last run" {
try screen.testWriteString("!=="); try screen.testWriteString("!==");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -926,13 +898,11 @@ test "shape left-replaced lig in early run" {
try screen.testWriteString("!==X"); try screen.testWriteString("!==X");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
const run = (try it.next(alloc)).?; const run = (try it.next(alloc)).?;
@ -957,13 +927,11 @@ test "shape U+3C9 with JB Mono" {
try screen.testWriteString("\u{03C9} foo"); try screen.testWriteString("\u{03C9} foo");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var run_count: usize = 0; var run_count: usize = 0;
var cell_count: usize = 0; var cell_count: usize = 0;
@ -990,13 +958,11 @@ test "shape emoji width" {
try screen.testWriteString("👍"); try screen.testWriteString("👍");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1040,13 +1006,11 @@ test "shape emoji width long" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1076,13 +1040,11 @@ test "shape variation selector VS15" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1111,13 +1073,11 @@ test "shape variation selector VS16" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1143,13 +1103,11 @@ test "shape with empty cells in between" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1181,13 +1139,11 @@ test "shape Chinese characters" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1221,13 +1177,11 @@ test "shape box glyphs" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1257,17 +1211,16 @@ test "shape selection boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init( .selection = terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
false, false,
), ),
null, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1280,17 +1233,16 @@ test "shape selection boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init( .selection = terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
false, false,
), ),
null, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1303,17 +1255,16 @@ test "shape selection boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init( .selection = terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
false, false,
), ),
null, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1326,17 +1277,16 @@ test "shape selection boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init( .selection = terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
false, false,
), ),
null, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1349,17 +1299,16 @@ test "shape selection boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init( .selection = terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
false, false,
), ),
null, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1385,13 +1334,11 @@ test "shape cursor boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1400,61 +1347,111 @@ test "shape cursor boundary" {
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
// Cursor at index 0 is two runs
{ {
// Get our run iterator // Cursor at index 0 is two runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(.{
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .grid = testdata.grid,
null, .screen = &screen,
0, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); .cursor_x = 0,
var count: usize = 0; });
while (try it.next(alloc)) |run| { var count: usize = 0;
count += 1; while (try it.next(alloc)) |run| {
_ = try shaper.shape(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 // Cursor at index 1 is three runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(.{
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .grid = testdata.grid,
null, .screen = &screen,
1, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); .cursor_x = 1,
var count: usize = 0; });
while (try it.next(alloc)) |run| { var count: usize = 0;
count += 1; while (try it.next(alloc)) |run| {
_ = try shaper.shape(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 // Cursor at last col is two runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(.{
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .grid = testdata.grid,
null, .screen = &screen,
9, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); .cursor_x = 9,
var count: usize = 0; });
while (try it.next(alloc)) |run| { var count: usize = 0;
count += 1; while (try it.next(alloc)) |run| {
_ = try shaper.shape(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 // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1493,13 +1488,12 @@ test "shape cursor boundary and colored emoji" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, .cursor_x = 0,
0, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1510,13 +1504,42 @@ test "shape cursor boundary and colored emoji" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
1, var count: usize = 0;
); while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.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; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1540,13 +1563,11 @@ test "shape cell attribute change" {
try screen.testWriteString(">="); try screen.testWriteString(">=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1564,13 +1585,11 @@ test "shape cell attribute change" {
try screen.testWriteString("="); try screen.testWriteString("=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1589,13 +1608,11 @@ test "shape cell attribute change" {
try screen.testWriteString("="); try screen.testWriteString("=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1614,13 +1631,11 @@ test "shape cell attribute change" {
try screen.testWriteString("="); try screen.testWriteString("=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1638,13 +1653,11 @@ test "shape cell attribute change" {
try screen.testWriteString("="); try screen.testWriteString("=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1678,13 +1691,11 @@ test "shape high plane sprite font codepoint" {
try screen.testWriteString("\u{1FB70}"); try screen.testWriteString("\u{1FB70}");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
// We should get one run // We should get one run
const run = (try it.next(alloc)).?; const run = (try it.next(alloc)).?;
// The run state should have the UTF-16 encoding of the character. // The run state should have the UTF-16 encoding of the character.

View File

@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
const harfbuzz = @import("harfbuzz"); const harfbuzz = @import("harfbuzz");
const font = @import("../main.zig"); const font = @import("../main.zig");
const terminal = @import("../../terminal/main.zig"); const terminal = @import("../../terminal/main.zig");
const config = @import("../../config.zig");
const Feature = font.shape.Feature; const Feature = font.shape.Feature;
const FeatureList = font.shape.FeatureList; const FeatureList = font.shape.FeatureList;
const default_features = font.shape.default_features; const default_features = font.shape.default_features;
@ -89,19 +90,11 @@ pub const Shaper = struct {
/// and assume the y value matches. /// and assume the y value matches.
pub fn runIterator( pub fn runIterator(
self: *Shaper, self: *Shaper,
grid: *SharedGrid, opts: font.shape.RunOptions,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection,
cursor_x: ?usize,
) font.shape.RunIterator { ) font.shape.RunIterator {
return .{ return .{
.hooks = .{ .shaper = self }, .hooks = .{ .shaper = self },
.grid = grid, .opts = opts,
.screen = screen,
.row = row,
.selection = selection,
.cursor_x = cursor_x,
}; };
} }
@ -225,13 +218,11 @@ test "run iterator" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
@ -244,13 +235,11 @@ test "run iterator" {
try screen.testWriteString("ABCD EFG"); try screen.testWriteString("ABCD EFG");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| count += 1; while (try it.next(alloc)) |_| count += 1;
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
@ -264,13 +253,11 @@ test "run iterator" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |_| { while (try it.next(alloc)) |_| {
count += 1; count += 1;
@ -316,13 +303,11 @@ test "run iterator: empty cells with background set" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
{ {
const run = (try it.next(alloc)).?; const run = (try it.next(alloc)).?;
try testing.expectEqual(@as(u32, 3), shaper.hb_buf.getLength()); try testing.expectEqual(@as(u32, 3), shaper.hb_buf.getLength());
@ -353,13 +338,11 @@ test "shape" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -382,13 +365,11 @@ test "shape inconsolata ligs" {
try screen.testWriteString(">="); try screen.testWriteString(">=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -407,13 +388,11 @@ test "shape inconsolata ligs" {
try screen.testWriteString("==="); try screen.testWriteString("===");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -440,13 +419,11 @@ test "shape monaspace ligs" {
try screen.testWriteString("==="); try screen.testWriteString("===");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -476,13 +453,11 @@ test "shape arabic forced LTR" {
try screen.testWriteString(@embedFile("testdata/arabic.txt")); try screen.testWriteString(@embedFile("testdata/arabic.txt"));
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -513,13 +488,11 @@ test "shape emoji width" {
try screen.testWriteString("👍"); try screen.testWriteString("👍");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -565,13 +538,11 @@ test "shape emoji width long" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 1 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -603,13 +574,11 @@ test "shape variation selector VS15" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -640,13 +609,11 @@ test "shape variation selector VS16" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -674,13 +641,11 @@ test "shape with empty cells in between" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -712,13 +677,11 @@ test "shape Chinese characters" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -752,13 +715,11 @@ test "shape box glyphs" {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -789,17 +750,16 @@ test "shape selection boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init( .selection = terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
false, false,
), ),
null, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -812,17 +772,16 @@ test "shape selection boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init( .selection = terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
false, false,
), ),
null, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -835,17 +794,16 @@ test "shape selection boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init( .selection = terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
false, false,
), ),
null, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -858,17 +816,16 @@ test "shape selection boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init( .selection = terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
false, false,
), ),
null, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -881,17 +838,16 @@ test "shape selection boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
terminal.Selection.init( .selection = terminal.Selection.init(
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?, screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
false, false,
), ),
null, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -917,13 +873,11 @@ test "shape cursor boundary" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -932,61 +886,111 @@ test "shape cursor boundary" {
try testing.expectEqual(@as(usize, 1), count); try testing.expectEqual(@as(usize, 1), count);
} }
// Cursor at index 0 is two runs
{ {
// Get our run iterator // Cursor at index 0 is two runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(.{
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .grid = testdata.grid,
null, .screen = &screen,
0, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); .cursor_x = 0,
var count: usize = 0; });
while (try it.next(alloc)) |run| { var count: usize = 0;
count += 1; while (try it.next(alloc)) |run| {
_ = try shaper.shape(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 // Cursor at index 1 is three runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(.{
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .grid = testdata.grid,
null, .screen = &screen,
1, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); .cursor_x = 1,
var count: usize = 0; });
while (try it.next(alloc)) |run| { var count: usize = 0;
count += 1; while (try it.next(alloc)) |run| {
_ = try shaper.shape(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 // Cursor at last col is two runs
var shaper = &testdata.shaper; {
var it = shaper.runIterator( // Get our run iterator
testdata.grid, var shaper = &testdata.shaper;
&screen, var it = shaper.runIterator(.{
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .grid = testdata.grid,
null, .screen = &screen,
9, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
); .cursor_x = 9,
var count: usize = 0; });
while (try it.next(alloc)) |run| { var count: usize = 0;
count += 1; while (try it.next(alloc)) |run| {
_ = try shaper.shape(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 // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1025,13 +1027,12 @@ test "shape cursor boundary and colored emoji" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, .cursor_x = 0,
0, });
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1042,13 +1043,42 @@ test "shape cursor boundary and colored emoji" {
{ {
// Get our run iterator // Get our run iterator
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
1, var count: usize = 0;
); while (try it.next(alloc)) |run| {
count += 1;
_ = try shaper.shape(run);
}
try testing.expectEqual(@as(usize, 1), count);
}
{
// Get our run iterator
var shaper = &testdata.shaper;
var it = shaper.runIterator(.{
.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; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1072,13 +1102,11 @@ test "shape cell attribute change" {
try screen.testWriteString(">="); try screen.testWriteString(">=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1096,13 +1124,11 @@ test "shape cell attribute change" {
try screen.testWriteString("="); try screen.testWriteString("=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1121,13 +1147,11 @@ test "shape cell attribute change" {
try screen.testWriteString("="); try screen.testWriteString("=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1146,13 +1170,11 @@ test "shape cell attribute change" {
try screen.testWriteString("="); try screen.testWriteString("=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;
@ -1170,13 +1192,11 @@ test "shape cell attribute change" {
try screen.testWriteString("="); try screen.testWriteString("=");
var shaper = &testdata.shaper; var shaper = &testdata.shaper;
var it = shaper.runIterator( var it = shaper.runIterator(.{
testdata.grid, .grid = testdata.grid,
&screen, .screen = &screen,
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?, .row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
null, });
null,
);
var count: usize = 0; var count: usize = 0;
while (try it.next(alloc)) |run| { while (try it.next(alloc)) |run| {
count += 1; count += 1;

View File

@ -70,19 +70,11 @@ pub const Shaper = struct {
pub fn runIterator( pub fn runIterator(
self: *Shaper, self: *Shaper,
grid: *SharedGrid, opts: font.shape.RunOptions,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection,
cursor_x: ?usize,
) font.shape.RunIterator { ) font.shape.RunIterator {
return .{ return .{
.hooks = .{ .shaper = self }, .hooks = .{ .shaper = self },
.grid = grid, .opts = opts,
.screen = screen,
.row = row,
.selection = selection,
.cursor_x = cursor_x,
}; };
} }

View File

@ -35,15 +35,11 @@ pub const TextRun = struct {
/// RunIterator is an iterator that yields text runs. /// RunIterator is an iterator that yields text runs.
pub const RunIterator = struct { pub const RunIterator = struct {
hooks: font.Shaper.RunIteratorHook, hooks: font.Shaper.RunIteratorHook,
grid: *font.SharedGrid, opts: shape.RunOptions,
screen: *const terminal.Screen,
row: terminal.Pin,
selection: ?terminal.Selection = null,
cursor_x: ?usize = null,
i: usize = 0, i: usize = 0,
pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun { 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 // Trim the right side of a row that might be empty
const max: usize = max: { const max: usize = max: {
@ -58,7 +54,7 @@ pub const RunIterator = struct {
// Invisible cells don't have any glyphs rendered, // Invisible cells don't have any glyphs rendered,
// so we explicitly skip them in the shaping process. // so we explicitly skip them in the shaping process.
while (self.i < max and 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; self.i += 1;
} }
@ -76,7 +72,7 @@ pub const RunIterator = struct {
var hasher = Hasher.init(0); var hasher = Hasher.init(0);
// Let's get our style that we'll expect for the run. // 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. // Go through cell by cell and accumulate while we build our run.
var j: usize = self.i; 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 // If we have a selection and we're at a boundary point, then
// we break the run here. // we break the run here.
if (self.selection) |unordered_sel| { if (self.opts.selection) |unordered_sel| {
if (j > self.i) { 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 start_x = sel.start().x;
const end_x = sel.end().x; const end_x = sel.end().x;
@ -142,7 +138,7 @@ pub const RunIterator = struct {
// The style is different. We allow differing background // The style is different. We allow differing background
// styles but any other change results in a new run. // styles but any other change results in a new run.
const c1 = comparableStyle(style); 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; if (!c1.eql(c2)) break;
} }
@ -162,7 +158,7 @@ pub const RunIterator = struct {
const presentation: ?font.Presentation = if (cell.hasGrapheme()) p: { const presentation: ?font.Presentation = if (cell.hasGrapheme()) p: {
// We only check the FIRST codepoint because I believe the // We only check the FIRST codepoint because I believe the
// presentation format must be directly adjacent to the codepoint. // 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); assert(cps.len > 0);
if (cps[0] == 0xFE0E) break :p .text; if (cps[0] == 0xFE0E) break :p .text;
if (cps[0] == 0xFE0F) break :p .emoji; 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 // joiners will show the joiners allowing you to modify the
// emoji. // emoji.
if (!cell.hasGrapheme()) { 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 // Exactly: self.i is the cursor and we iterated once. This
// means that we started exactly at the cursor and did at // means that we started exactly at the cursor and did at
// exactly one iteration. Why exactly one? Because we may // 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 // Otherwise we need a fallback character. Prefer the
// official replacement character. // official replacement character.
if (try self.grid.getIndex( if (try self.opts.grid.getIndex(
alloc, alloc,
0xFFFD, // replacement char 0xFFFD, // replacement char
font_style, font_style,
@ -235,7 +231,7 @@ pub const RunIterator = struct {
)) |idx| break :font_info .{ .idx = idx, .fallback = 0xFFFD }; )) |idx| break :font_info .{ .idx = idx, .fallback = 0xFFFD };
// Fallback to space // Fallback to space
if (try self.grid.getIndex( if (try self.opts.grid.getIndex(
alloc, alloc,
' ', ' ',
font_style, font_style,
@ -273,7 +269,7 @@ pub const RunIterator = struct {
@intCast(cluster), @intCast(cluster),
); );
if (cell.hasGrapheme()) { if (cell.hasGrapheme()) {
const cps = self.row.grapheme(cell).?; const cps = self.opts.row.grapheme(cell).?;
for (cps) |cp| { for (cps) |cp| {
// Do not send presentation modifiers // Do not send presentation modifiers
if (cp == 0xFE0E or cp == 0xFE0F) continue; if (cp == 0xFE0E or cp == 0xFE0F) continue;
@ -298,7 +294,7 @@ pub const RunIterator = struct {
.hash = hasher.final(), .hash = hasher.final(),
.offset = @intCast(self.i), .offset = @intCast(self.i),
.cells = @intCast(j - self.i), .cells = @intCast(j - self.i),
.grid = self.grid, .grid = self.opts.grid,
.font_index = current_font, .font_index = current_font,
}; };
} }
@ -326,7 +322,7 @@ pub const RunIterator = struct {
cell.codepoint() == 0 or cell.codepoint() == 0 or
cell.codepoint() == terminal.kitty.graphics.unicode.placeholder) cell.codepoint() == terminal.kitty.graphics.unicode.placeholder)
{ {
return try self.grid.getIndex( return try self.opts.grid.getIndex(
alloc, alloc,
' ', ' ',
style, style,
@ -336,7 +332,7 @@ pub const RunIterator = struct {
// Get the font index for the primary codepoint. // Get the font index for the primary codepoint.
const primary_cp: u32 = cell.codepoint(); const primary_cp: u32 = cell.codepoint();
const primary = try self.grid.getIndex( const primary = try self.opts.grid.getIndex(
alloc, alloc,
primary_cp, primary_cp,
style, style,
@ -349,7 +345,7 @@ pub const RunIterator = struct {
// If this is a grapheme, we need to find a font that supports // If this is a grapheme, we need to find a font that supports
// all of the codepoints in the grapheme. // 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); var candidates = try std.ArrayList(font.Collection.Index).initCapacity(alloc, cps.len + 1);
defer candidates.deinit(); defer candidates.deinit();
candidates.appendAssumeCapacity(primary); candidates.appendAssumeCapacity(primary);
@ -365,7 +361,7 @@ pub const RunIterator = struct {
// to support the base presentation, since it is common for emoji // to support the base presentation, since it is common for emoji
// fonts to support the base emoji with emoji presentation but not // fonts to support the base emoji with emoji presentation but not
// certain ZWJ-combined characters like the male and female signs. // certain ZWJ-combined characters like the male and female signs.
const idx = try self.grid.getIndex( const idx = try self.opts.grid.getIndex(
alloc, alloc,
cp, cp,
style, style,
@ -376,11 +372,11 @@ pub const RunIterator = struct {
// We need to find a candidate that has ALL of our codepoints // We need to find a candidate that has ALL of our codepoints
for (candidates.items) |idx| { 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| { for (cps) |cp| {
// Ignore Emoji ZWJs // Ignore Emoji ZWJs
if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue; 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 { } else {
// If the while completed, then we have a candidate that // If the while completed, then we have a candidate that
// supports all of our codepoints. // supports all of our codepoints.

View File

@ -61,17 +61,11 @@ pub const Shaper = struct {
/// for a Shaper struct since they share state. /// for a Shaper struct since they share state.
pub fn runIterator( pub fn runIterator(
self: *Shaper, self: *Shaper,
group: *font.GroupCache, opts: font.shape.RunOptions,
row: terminal.Screen.Row,
selection: ?terminal.Selection,
cursor_x: ?usize,
) font.shape.RunIterator { ) font.shape.RunIterator {
return .{ return .{
.hooks = .{ .shaper = self }, .hooks = .{ .shaper = self },
.group = group, .opts = opts,
.row = row,
.selection = selection,
.cursor_x = cursor_x,
}; };
} }

View File

@ -32,12 +32,7 @@ pub const Sprite = enum(u32) {
cursor_rect, cursor_rect,
cursor_hollow_rect, cursor_hollow_rect,
cursor_bar, cursor_bar,
cursor_underline,
// Note: we don't currently put the box drawing glyphs in here because
// there are a LOT and I'm lazy. What I want to do is spend more time
// studying the patterns to see if we can programmatically build our
// enum perhaps and comptime generate the drawing code at the same time.
// I'm not sure if that's advisable yet though.
test { test {
const testing = std.testing; const testing = std.testing;

File diff suppressed because it is too large Load Diff

View File

@ -16,25 +16,158 @@ const std = @import("std");
const builtin = @import("builtin"); const builtin = @import("builtin");
const assert = std.debug.assert; const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const wuffs = @import("wuffs");
const z2d = @import("z2d");
const font = @import("../main.zig"); const font = @import("../main.zig");
const Sprite = font.sprite.Sprite; const Sprite = font.sprite.Sprite;
const Box = @import("Box.zig");
const Powerline = @import("Powerline.zig"); const special = @import("draw/special.zig");
const underline = @import("underline.zig");
const cursor = @import("cursor.zig");
const log = std.log.scoped(.font_sprite); const log = std.log.scoped(.font_sprite);
/// Grid metrics for rendering sprites. /// Grid metrics for rendering sprites.
metrics: font.Metrics, 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<CP>` or `draw<MIN>_<MAX>`.
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. /// Returns true if the codepoint exists in our sprite font.
pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool { pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool {
// We ignore presentation. No matter what presentation is requested // We ignore presentation. No matter what presentation is
// we always provide glyphs for our codepoints. // requested we always provide glyphs for our codepoints.
_ = p; _ = p;
_ = self; _ = self;
return Kind.init(cp) != null; return getDrawFn(cp) != null;
} }
/// Render the glyph. /// 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 // 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 // in runtime safety modes but in case it is its not worth memory
// corruption so we return a valid, blank glyph. // corruption so we return a valid, blank glyph.
const kind = Kind.init(cp) orelse return .{ const draw = getDrawFn(cp) orelse return .{
.width = 0, .width = 0,
.height = 0, .height = 0,
.offset_x = 0, .offset_x = 0,
@ -73,217 +198,350 @@ pub fn renderGlyph(
.advance_x = 0, .advance_x = 0,
}; };
// Safe to ".?" because of the above assertion. const metrics = self.metrics;
return switch (kind) {
.box => (Box{ .metrics = metrics }).renderGlyph(alloc, atlas, cp),
.underline => try underline.renderGlyph( // We adjust our sprite width based on the cell width.
alloc, const width = switch (opts.cell_width orelse 1) {
atlas, 0, 1 => metrics.cell_width,
@enumFromInt(cp), else => |width| metrics.cell_width * width,
width, };
metrics.cell_height,
metrics.underline_position,
metrics.underline_thickness,
),
.strikethrough => try underline.renderGlyph( const height = metrics.cell_height;
alloc,
atlas,
@enumFromInt(cp),
width,
metrics.cell_height,
metrics.strikethrough_position,
metrics.strikethrough_thickness,
),
.overline => overline: { const padding_x = width / 4;
var g = try underline.renderGlyph( const padding_y = height / 4;
alloc,
atlas,
@enumFromInt(cp),
width,
metrics.cell_height,
0,
metrics.overline_thickness,
);
// We have to manually subtract the overline position // Make a canvas of the desired size
// on the rendered glyph since it can be negative. var canvas = try font.sprite.Canvas.init(alloc, width, height, padding_x, padding_y);
g.offset_y -= metrics.overline_position; defer canvas.deinit();
break :overline g; try draw(cp, &canvas, width, height, metrics);
},
.powerline => powerline: { // Write the drawing to the atlas
const f: Powerline = .{ const region = try canvas.writeAtlas(alloc, atlas);
.width = metrics.cell_width,
.height = metrics.cell_height,
.thickness = metrics.box_thickness,
};
break :powerline try f.renderGlyph(alloc, atlas, cp); return .{
}, .width = region.width,
.height = region.height,
.cursor => cursor: { .offset_x = @as(i32, @intCast(canvas.clip_left)) - @as(i32, @intCast(padding_x)),
var g = try cursor.renderGlyph( .offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)),
alloc, .atlas_x = region.x,
atlas, .atlas_y = region.y,
@enumFromInt(cp), .advance_x = @floatFromInt(width),
width, .sprite = true,
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;
},
}; };
} }
/// Kind of sprites we have. Drawing is implemented separately for each kind. /// Used in `testDrawRanges`, checks for diff between the provided atlas
const Kind = enum { /// and the reference file for the range, returns true if there is a diff.
box, fn testDiffAtlas(
underline, alloc: Allocator,
overline, atlas: *z2d.Surface,
strikethrough, path: []const u8,
powerline, i: u32,
cursor, 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 { const cwd_absolute = try std.fs.cwd().realpathAlloc(alloc, ".");
return switch (cp) { defer alloc.free(cwd_absolute);
Sprite.start...Sprite.end => switch (@as(Sprite, @enumFromInt(cp))) {
.underline,
.underline_double,
.underline_dotted,
.underline_dashed,
.underline_curly,
=> .underline,
.overline, // Get the reference file contents to compare.
=> .overline, 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, // Copy the test PNG in to the CWD so it isn't
=> .strikethrough, // 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, return true;
.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,
}; };
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 { test {
@import("std").testing.refAllDecls(@This()); std.testing.refAllDecls(@This());
} }

View File

@ -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);
}
}

View File

@ -81,19 +81,39 @@ pub const Canvas = struct {
/// The underlying z2d surface. /// The underlying z2d surface.
sfc: 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, 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. // Create the surface we'll be using.
// We add padding to both sides (hence `2 *`)
const sfc = try z2d.Surface.initPixel( const sfc = try z2d.Surface.initPixel(
.{ .alpha8 = .{ .a = 0 } }, .{ .alpha8 = .{ .a = 0 } },
alloc, alloc,
@intCast(width), @intCast(width + 2 * padding_x),
@intCast(height), @intCast(height + 2 * padding_y),
); );
errdefer sfc.deinit(alloc); 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 { pub fn deinit(self: *Canvas) void {
@ -109,30 +129,33 @@ pub const Canvas = struct {
) (Allocator.Error || font.Atlas.Error)!font.Atlas.Region { ) (Allocator.Error || font.Atlas.Error)!font.Atlas.Region {
assert(atlas.format == .grayscale); assert(atlas.format == .grayscale);
const width = @as(u32, @intCast(self.sfc.getWidth())); self.trim();
const height = @as(u32, @intCast(self.sfc.getHeight()));
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 // Allocate our texture atlas region
const region = region: { const region = region: {
// We need to add a 1px padding to the font so that we don't // Reserve a region with a 1px margin on the bottom and right edges
// get fuzzy issues when blending textures. // so that we can avoid interpolation between adjacent glyphs during
const padding = 1; // texture sampling.
// Get the full padded region
var region = try atlas.reserve( var region = try atlas.reserve(
alloc, alloc,
width + (padding * 2), // * 2 because left+right region_width + 1,
height + (padding * 2), // * 2 because top+bottom region_height + 1,
); );
// Modify the region so that we remove the padding so that // Modify the region to remove the margin so that we write to the
// we write to the non-zero location. The data in an Altlas // non-zero location. The data in an Altlas is always initialized
// is always initialized to zero (Atlas.clear) so we don't // to zero (Atlas.clear) so we don't need to worry about zero-ing
// need to worry about zero-ing that. // that.
region.x += padding; region.width -= 1;
region.y += padding; region.height -= 1;
region.width -= padding * 2;
region.height -= padding * 2;
break :region region; break :region region;
}; };
@ -140,38 +163,138 @@ pub const Canvas = struct {
const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf); const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf);
// Write the glyph information into the atlas // Write the glyph information into the atlas
assert(region.width == width); assert(region.width == region_width);
assert(region.height == height); assert(region.height == region_height);
atlas.set(region, buffer); atlas.setFromLarger(
region,
buffer,
sfc_width,
self.clip_left,
self.clip_top,
);
} }
return region; 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. /// Acquires a z2d drawing context, caller MUST deinit context.
pub fn getContext(self: *Canvas) z2d.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 /// 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( self.sfc.putPixel(
@intCast(x), x + @as(i32, @intCast(self.padding_x)),
@intCast(y), y + @as(i32, @intCast(self.padding_y)),
.{ .alpha8 = .{ .a = @intFromEnum(color) } }, .{ .alpha8 = .{ .a = @intFromEnum(color) } },
); );
} }
/// Draw and fill a rectangle. This is the main primitive for drawing /// Draw and fill a rectangle. This is the main primitive for drawing
/// lines as well (which are just generally skinny rectangles...) /// lines as well (which are just generally skinny rectangles...)
pub fn rect(self: *Canvas, v: Rect(u32), color: Color) void { pub fn rect(self: *Canvas, v: Rect(i32), color: Color) void {
const x0 = v.x; var y = v.y;
const x1 = v.x + v.width; while (y < v.y + v.height) : (y += 1) {
const y0 = v.y; var x = v.x;
const y1 = v.y + v.height; while (x < v.x + v.width) : (x += 1) {
for (y0..y1) |y| {
for (x0..x1) |x| {
self.pixel( self.pixel(
@intCast(x), @intCast(x),
@intCast(y), @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. /// Draw and fill a quad.
pub fn quad(self: *Canvas, q: Quad(f64), color: Color) !void { pub fn quad(self: *Canvas, q: Quad(f64), color: Color) !void {
var path: z2d.StaticPath(6) = .{}; var path = self.staticPath(6); // nodes.len = 0
path.init(); // nodes.len = 0
path.moveTo(q.p0.x, q.p0.y); // +1, nodes.len = 1 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.p1.x, q.p1.y); // +1, nodes.len = 2
path.lineTo(q.p2.x, q.p2.y); // +1, nodes.len = 3 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.lineTo(q.p3.x, q.p3.y); // +1, nodes.len = 4
path.close(); // +2, nodes.len = 6 path.close(); // +2, nodes.len = 6
try self.fillPath(path.wrapped_path, .{}, color);
try z2d.painter.fill(
self.alloc,
&self.sfc,
&.{ .opaque_pattern = .{
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
} },
path.wrapped_path.nodes.items,
.{},
);
} }
/// Draw and fill a triangle. /// Draw and fill a triangle.
pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void { pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void {
var path: z2d.StaticPath(5) = .{}; var path = self.staticPath(5); // nodes.len = 0
path.init(); // nodes.len = 0
path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1 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.p1.x, t.p1.y); // +1, nodes.len = 2
path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3 path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3
path.close(); // +2, nodes.len = 5 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( try z2d.painter.fill(
self.alloc, self.alloc,
&self.sfc, &self.sfc,
&.{ .opaque_pattern = .{ &.{ .opaque_pattern = .{
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } }, .pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
} }, } },
path.wrapped_path.nodes.items, path.nodes.items,
.{}, opts,
);
}
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,
},
); );
} }
/// Invert all pixels on the canvas.
pub fn invert(self: *Canvas) void { pub fn invert(self: *Canvas) void {
for (std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf)) |*v| { for (std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf)) |*v| {
v.* = 255 - 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);
}
}; };

View File

@ -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),
};
}

View File

@ -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<CODEPOINT>` or `draw<MIN>_<MAX>` 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.

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
},
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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);
}

File diff suppressed because it is too large Load Diff

View File

@ -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);
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Some files were not shown because too many files have changed in this diff Show More