Merge branch 'main' into ssh-integration
|
|
@ -81,6 +81,10 @@
|
|||
# - @ghostty-org/localization/* - Anything related to localization
|
||||
# for a specific locale.
|
||||
#
|
||||
# - @ghosty-org/localization/manager - Manage all localization tasks
|
||||
# and tooling. They are not responsible for any specific locale but
|
||||
# are responsible for the overall localization process and tooling.
|
||||
#
|
||||
# - @ghostty-org/macos - The Ghostty macOS app and any macOS-specific
|
||||
# features, configurations, etc.
|
||||
#
|
||||
|
|
@ -161,6 +165,7 @@
|
|||
/po/ca_ES.UTF-8.po @ghostty-org/ca_ES
|
||||
/po/de_DE.UTF-8.po @ghostty-org/de_DE
|
||||
/po/es_BO.UTF-8.po @ghostty-org/es_BO
|
||||
/po/es_AR.UTF-8.po @ghostty-org/es_AR
|
||||
/po/fr_FR.UTF-8.po @ghostty-org/fr_FR
|
||||
/po/id_ID.UTF-8.po @ghostty-org/id_ID
|
||||
/po/ja_JP.UTF-8.po @ghostty-org/ja_JP
|
||||
|
|
@ -173,6 +178,8 @@
|
|||
/po/tr_TR.UTF-8.po @ghostty-org/tr_TR
|
||||
/po/uk_UA.UTF-8.po @ghostty-org/uk_UA
|
||||
/po/zh_CN.UTF-8.po @ghostty-org/zh_CN
|
||||
/po/ga_IE.UTF-8.po @ghostty-org/ga_IE
|
||||
/po/ko_KR.UTF-8.po @ghostty-org/ko_KR
|
||||
|
||||
# Packaging - Snap
|
||||
/snap/ @ghostty-org/snap
|
||||
|
|
|
|||
21
TODO.md
|
|
@ -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
|
||||
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
.libxev = .{
|
||||
// mitchellh/libxev
|
||||
.url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
|
||||
.hash = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3",
|
||||
.url = "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz",
|
||||
.hash = "libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw",
|
||||
.lazy = true,
|
||||
},
|
||||
.vaxis = .{
|
||||
|
|
@ -20,14 +20,14 @@
|
|||
},
|
||||
.z2d = .{
|
||||
// vancluever/z2d
|
||||
.url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz",
|
||||
.hash = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW",
|
||||
.url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
|
||||
.hash = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg",
|
||||
.lazy = true,
|
||||
},
|
||||
.zig_objc = .{
|
||||
// mitchellh/zig-objc
|
||||
.url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz",
|
||||
.hash = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt",
|
||||
.url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
||||
.hash = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
|
||||
.lazy = true,
|
||||
},
|
||||
.zig_js = .{
|
||||
|
|
@ -103,8 +103,8 @@
|
|||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
|
||||
.hash = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz",
|
||||
.hash = "N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS",
|
||||
.lazy = true,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -54,20 +54,20 @@
|
|||
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
|
||||
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
|
||||
},
|
||||
"N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX": {
|
||||
"N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS": {
|
||||
"name": "iterm2_themes",
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
|
||||
"hash": "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U="
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz",
|
||||
"hash": "sha256-g9o2CIc/TfWYoUS/l/HP5KZECD7qNsdQUlFruaKkVz4="
|
||||
},
|
||||
"N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": {
|
||||
"name": "libpng",
|
||||
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
|
||||
"hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="
|
||||
},
|
||||
"libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3": {
|
||||
"libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw": {
|
||||
"name": "libxev",
|
||||
"url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
|
||||
"hash": "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU="
|
||||
"url": "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz",
|
||||
"hash": "sha256-/CSKSuZZfn0aIQlVZ0O8ch5O4gCajYBTTuoetRdo0n4="
|
||||
},
|
||||
"N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": {
|
||||
"name": "libxml2",
|
||||
|
|
@ -124,10 +124,10 @@
|
|||
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
|
||||
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
|
||||
},
|
||||
"z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW": {
|
||||
"z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg": {
|
||||
"name": "z2d",
|
||||
"url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz",
|
||||
"hash": "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U="
|
||||
"url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
|
||||
"hash": "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4="
|
||||
},
|
||||
"zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": {
|
||||
"name": "zf",
|
||||
|
|
@ -144,10 +144,10 @@
|
|||
"url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz",
|
||||
"hash": "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="
|
||||
},
|
||||
"zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt": {
|
||||
"zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk": {
|
||||
"name": "zig_objc",
|
||||
"url": "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz",
|
||||
"hash": "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw="
|
||||
"url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
||||
"hash": "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw="
|
||||
},
|
||||
"wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy": {
|
||||
"name": "zig_wayland",
|
||||
|
|
|
|||
|
|
@ -170,11 +170,11 @@ in
|
|||
};
|
||||
}
|
||||
{
|
||||
name = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX";
|
||||
name = "N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS";
|
||||
path = fetchZigArtifact {
|
||||
name = "iterm2_themes";
|
||||
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz";
|
||||
hash = "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U=";
|
||||
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz";
|
||||
hash = "sha256-g9o2CIc/TfWYoUS/l/HP5KZECD7qNsdQUlFruaKkVz4=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
@ -186,11 +186,11 @@ in
|
|||
};
|
||||
}
|
||||
{
|
||||
name = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3";
|
||||
name = "libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw";
|
||||
path = fetchZigArtifact {
|
||||
name = "libxev";
|
||||
url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz";
|
||||
hash = "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU=";
|
||||
url = "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz";
|
||||
hash = "sha256-/CSKSuZZfn0aIQlVZ0O8ch5O4gCajYBTTuoetRdo0n4=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
@ -282,11 +282,11 @@ in
|
|||
};
|
||||
}
|
||||
{
|
||||
name = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW";
|
||||
name = "z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg";
|
||||
path = fetchZigArtifact {
|
||||
name = "z2d";
|
||||
url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz";
|
||||
hash = "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U=";
|
||||
url = "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz";
|
||||
hash = "sha256-wfadegeixcbgxRzRtf6M2H34CTuvDM22XVIhuufFBG4=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
@ -314,11 +314,11 @@ in
|
|||
};
|
||||
}
|
||||
{
|
||||
name = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt";
|
||||
name = "zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk";
|
||||
path = fetchZigArtifact {
|
||||
name = "zig_objc";
|
||||
url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz";
|
||||
hash = "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw=";
|
||||
url = "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz";
|
||||
hash = "sha256-o3vl7qfkSi0bKXa6JWuF92qMEGP8Af/shcip5nRo5Nw=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.
|
|||
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
|
||||
https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz
|
||||
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz
|
||||
https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz
|
||||
https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz
|
||||
https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz
|
||||
https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
|
||||
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
|
||||
https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz
|
||||
https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
[Desktop Entry]
|
||||
Name=Ghostty
|
||||
Version=1.0
|
||||
Name=@NAME@
|
||||
Type=Application
|
||||
Comment=A terminal emulator
|
||||
Exec=ghostty
|
||||
TryExec=@GHOSTTY@
|
||||
Exec=@GHOSTTY@ --launched-from=desktop
|
||||
Icon=com.mitchellh.ghostty
|
||||
Categories=System;TerminalEmulator;
|
||||
Keywords=terminal;tty;pty;
|
||||
StartupNotify=true
|
||||
StartupWMClass=com.mitchellh.ghostty
|
||||
StartupWMClass=@APPID@
|
||||
Terminal=false
|
||||
Actions=new-window;
|
||||
X-GNOME-UsesNotifications=true
|
||||
|
|
@ -16,7 +18,8 @@ X-TerminalArgTitle=--title=
|
|||
X-TerminalArgAppId=--class=
|
||||
X-TerminalArgDir=--working-directory=
|
||||
X-TerminalArgHold=--wait-after-command
|
||||
DBusActivatable=true
|
||||
|
||||
[Desktop Action new-window]
|
||||
Name=New Window
|
||||
Exec=ghostty
|
||||
Exec=@GHOSTTY@ --launched-from=desktop
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component type="desktop-application">
|
||||
<id>com.mitchellh.ghostty</id>
|
||||
<launchable type="desktop-id">com.mitchellh.ghostty.desktop</launchable>
|
||||
<name>Ghostty</name>
|
||||
<id>@APPID@</id>
|
||||
<launchable type="desktop-id">@APPID@.desktop</launchable>
|
||||
<name>@NAME@</name>
|
||||
<url type="homepage">https://ghostty.org</url>
|
||||
<url type="help">https://ghostty.org/docs</url>
|
||||
<url type="bugtracker">https://github.com/ghostty-org/ghostty/discussions</url>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
[D-BUS Service]
|
||||
Name=@APPID@
|
||||
Exec=@GHOSTTY@ --launched-from=dbus
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
[D-BUS Service]
|
||||
Name=@APPID@
|
||||
SystemdService=@APPID@.service
|
||||
Exec=@GHOSTTY@ --launched-from=dbus
|
||||
|
|
@ -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
|
||||
|
|
@ -6,11 +6,7 @@ sdk-extensions:
|
|||
- org.freedesktop.Sdk.Extension.ziglang
|
||||
default-branch: tip
|
||||
command: ghostty
|
||||
# Integrate the rename into zig build, maybe?
|
||||
rename-desktop-file: com.mitchellh.ghostty.desktop
|
||||
rename-appdata-file: com.mitchellh.ghostty.metainfo.xml
|
||||
rename-icon: com.mitchellh.ghostty
|
||||
desktop-file-name-suffix: " (Debug)"
|
||||
finish-args:
|
||||
# 3D rendering
|
||||
- --device=dri
|
||||
|
|
|
|||
|
|
@ -67,9 +67,9 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
|
||||
"dest": "vendor/p/N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX",
|
||||
"sha256": "0bddcc4b2360c81fae86fccc4044c35ebefcdfd845c97ecd7d3329e0750ab375"
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6fa671fdc1daf1fcfa025cb960ffa3e7373a2ed8.tar.gz",
|
||||
"dest": "vendor/p/N-V-__8AAGHcWgTaKLjwmFkxToNT4jgz5VXUHR7hz8TQ2_AS",
|
||||
"sha256": "83da3608873f4df598a144bf97f1cfe4a644083eea36c75052516bb9a2a4573e"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
@ -79,9 +79,9 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
|
||||
"dest": "vendor/p/libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3",
|
||||
"sha256": "570141c83a29b6a88de5492894543b4db461c0c11145e0fd12bda98f14c26085"
|
||||
"url": "https://github.com/mitchellh/libxev/archive/75a10d0fb374e8eb84948dcfc68d865e755e59c2.tar.gz",
|
||||
"dest": "vendor/p/libxev-0.0.0-86vtcyMBEwA-UpcjfOICyI2GYG8o6MiRbinS1_8g1_rw",
|
||||
"sha256": "fc248a4ae6597e7d1a2109556743bc721e4ee2009a8d80534eea1eb51768d27e"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
@ -151,9 +151,9 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz",
|
||||
"dest": "vendor/p/z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW",
|
||||
"sha256": "c2226cebf2d48b2f80a42e6ced53f2a5b06e92306be2f8f1deffe5f4ead3ef45"
|
||||
"url": "https://github.com/vancluever/z2d/archive/8bbd035f4101f02b1d27947def0d7da3215df7fe.tar.gz",
|
||||
"dest": "vendor/p/z2d-0.7.0-j5P_Hg_DDACq-2H2Zh7rAq6_TXWdQzv7JAUfnrdeDosg",
|
||||
"sha256": "c1f69d7a07a2c5c6e0c51cd1b5fe8cd87df8093baf0ccdb65d5221bae7c5046e"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
@ -175,9 +175,9 @@
|
|||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz",
|
||||
"dest": "vendor/p/zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt",
|
||||
"sha256": "ce7d6d47ac614a60e56b8509dedf869e2e0d8b747c75e48aded11eec31b3357c"
|
||||
"url": "https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz",
|
||||
"dest": "vendor/p/zig_objc-0.0.0-Ir_SpwsPAQBJgi9YRm2ubJMfdoysSq5gKpsIj3izQ8Zk",
|
||||
"sha256": "a37be5eea7e44a2d1b2976ba256b85f76a8c1063fc01ffec85c8a9e67468e4dc"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
|
|
|||
|
|
@ -112,6 +112,9 @@ class AppDelegate: NSObject,
|
|||
/// The observer for the app appearance.
|
||||
private var appearanceObserver: NSKeyValueObservation? = nil
|
||||
|
||||
/// Signals
|
||||
private var signals: [DispatchSourceSignal] = []
|
||||
|
||||
/// The custom app icon image that is currently in use.
|
||||
@Published private(set) var appIcon: NSImage? = nil {
|
||||
didSet {
|
||||
|
|
@ -249,6 +252,9 @@ class AppDelegate: NSObject,
|
|||
|
||||
// Setup our menu
|
||||
setupMenuImages()
|
||||
|
||||
// Setup signal handlers
|
||||
setupSignals()
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ notification: Notification) {
|
||||
|
|
@ -406,6 +412,34 @@ class AppDelegate: NSObject,
|
|||
return dockMenu
|
||||
}
|
||||
|
||||
/// Setup signal handlers
|
||||
private func setupSignals() {
|
||||
// Register a signal handler for config reloading. It appears that all
|
||||
// of this is required. I've commented each line because its a bit unclear.
|
||||
// Warning: signal handlers don't work when run via Xcode. They have to be
|
||||
// run on a real app bundle.
|
||||
|
||||
// We need to ignore signals we register with makeSignalSource or they
|
||||
// don't seem to handle.
|
||||
signal(SIGUSR2, SIG_IGN)
|
||||
|
||||
// Make the signal source and register our event handle. We keep a weak
|
||||
// ref to ourself so we don't create a retain cycle.
|
||||
let sigusr2 = DispatchSource.makeSignalSource(signal: SIGUSR2, queue: .main)
|
||||
sigusr2.setEventHandler { [weak self] in
|
||||
guard let self else { return }
|
||||
Ghostty.logger.info("reloading configuration in response to SIGUSR2")
|
||||
self.ghostty.reloadConfig()
|
||||
}
|
||||
|
||||
// The signal source starts unactivated, so we have to resume it once
|
||||
// we setup the event handler.
|
||||
sigusr2.resume()
|
||||
|
||||
// We need to keep a strong reference to it so it isn't disabled.
|
||||
signals.append(sigusr2)
|
||||
}
|
||||
|
||||
/// Setup all the images for our menu items.
|
||||
private func setupMenuImages() {
|
||||
// Note: This COULD Be done all in the xib file, but I find it easier to
|
||||
|
|
|
|||
|
|
@ -610,14 +610,18 @@ extension SplitTree.Node {
|
|||
return (self, 1)
|
||||
|
||||
case .split(let split):
|
||||
// Recursively equalize children
|
||||
let (leftNode, leftWeight) = split.left.equalizeWithWeight()
|
||||
let (rightNode, rightWeight) = split.right.equalizeWithWeight()
|
||||
|
||||
// Calculate weights based on split direction
|
||||
let leftWeight = split.left.weightForDirection(split.direction)
|
||||
let rightWeight = split.right.weightForDirection(split.direction)
|
||||
|
||||
// Calculate new ratio based on relative weights
|
||||
let totalWeight = leftWeight + rightWeight
|
||||
let newRatio = Double(leftWeight) / Double(totalWeight)
|
||||
|
||||
|
||||
// Recursively equalize children
|
||||
let (leftNode, _) = split.left.equalizeWithWeight()
|
||||
let (rightNode, _) = split.right.equalizeWithWeight()
|
||||
|
||||
// Create new split with equalized ratio
|
||||
let newSplit = Split(
|
||||
direction: split.direction,
|
||||
|
|
@ -630,6 +634,23 @@ extension SplitTree.Node {
|
|||
}
|
||||
}
|
||||
|
||||
/// Calculate weight for equalization based on split direction.
|
||||
/// Children with the same direction contribute their full weight,
|
||||
/// children with different directions count as 1.
|
||||
private func weightForDirection(_ direction: SplitTree.Direction) -> Int {
|
||||
switch self {
|
||||
case .leaf:
|
||||
return 1
|
||||
case .split(let split):
|
||||
if split.direction == direction {
|
||||
return split.left.weightForDirection(direction) + split.right.weightForDirection(direction)
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Calculate the bounds of all views in this subtree based on split ratios
|
||||
func calculateViewBounds(in bounds: CGRect) -> [(view: ViewType, bounds: CGRect)] {
|
||||
switch self {
|
||||
|
|
|
|||
|
|
@ -19,20 +19,27 @@ class HiddenTitlebarTerminalWindow: TerminalWindow {
|
|||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
private static let hiddenStyleMask: NSWindow.StyleMask = [
|
||||
// We need `titled` in the mask to get the normal window frame
|
||||
.titled,
|
||||
|
||||
// Full size content view so we can extend
|
||||
// content in to the hidden titlebar's area
|
||||
.fullSizeContentView,
|
||||
|
||||
.resizable,
|
||||
.closable,
|
||||
.miniaturizable,
|
||||
]
|
||||
|
||||
/// Apply the hidden titlebar style.
|
||||
private func reapplyHiddenStyle() {
|
||||
styleMask = [
|
||||
// We need `titled` in the mask to get the normal window frame
|
||||
.titled,
|
||||
|
||||
// Full size content view so we can extend
|
||||
// content in to the hidden titlebar's area
|
||||
.fullSizeContentView,
|
||||
|
||||
.resizable,
|
||||
.closable,
|
||||
.miniaturizable,
|
||||
]
|
||||
// Apply our style mask while preserving the .fullScreen option
|
||||
if styleMask.contains(.fullScreen) {
|
||||
styleMask = Self.hiddenStyleMask.union([.fullScreen])
|
||||
} else {
|
||||
styleMask = Self.hiddenStyleMask
|
||||
}
|
||||
|
||||
// Hide the title
|
||||
titleVisibility = .hidden
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
const std = @import("std");
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("gtk4-layer-shell.h");
|
||||
});
|
||||
|
|
@ -31,6 +33,14 @@ pub fn getProtocolVersion() c_uint {
|
|||
return c.gtk_layer_get_protocol_version();
|
||||
}
|
||||
|
||||
pub fn getLibraryVersion() std.SemanticVersion {
|
||||
return .{
|
||||
.major = c.gtk_layer_get_major_version(),
|
||||
.minor = c.gtk_layer_get_minor_version(),
|
||||
.patch = c.gtk_layer_get_micro_version(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initForWindow(window: *gtk.Window) void {
|
||||
c.gtk_layer_init_for_window(@ptrCast(window));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "Копирано в клипборда"
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR Mitchell Hashimoto, Ghostty contributors
|
||||
# Copyright (C) YEAR "Mitchell Hashimoto, Ghostty contributors"
|
||||
# This file is distributed under the same license as the com.mitchellh.ghostty package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2025-04-23 16:58+0800\n"
|
||||
"POT-Creation-Date: 2025-06-28 17:01+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
|
@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title."
|
|||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10
|
||||
#: src/apprt/gtk/CloseDialog.zig:44
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35,22 +36,26 @@ msgid "OK"
|
|||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5
|
||||
msgid "Configuration Errors"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
|
||||
msgid ""
|
||||
"One or more configuration errors were found. Please review the errors below, "
|
||||
"and either reload your configuration or ignore these errors."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9
|
||||
msgid "Ignore"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10
|
||||
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10
|
||||
msgid "Reload Configuration"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -89,7 +94,7 @@ msgstr ""
|
|||
|
||||
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11
|
||||
msgid "Paste"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -119,7 +124,7 @@ msgstr ""
|
|||
|
||||
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
|
||||
#: src/apprt/gtk/Window.zig:255
|
||||
#: src/apprt/gtk/Window.zig:263
|
||||
msgid "New Tab"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -160,7 +165,7 @@ msgid "Terminal Inspector"
|
|||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
|
||||
#: src/apprt/gtk/Window.zig:1024
|
||||
#: src/apprt/gtk/Window.zig:1036
|
||||
msgid "About Ghostty"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -170,10 +175,13 @@ msgstr ""
|
|||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6
|
||||
msgid "Authorize Clipboard Access"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7
|
||||
msgid ""
|
||||
"An application is attempting to read from the clipboard. The current "
|
||||
"clipboard contents are shown below."
|
||||
|
|
@ -181,52 +189,67 @@ msgstr ""
|
|||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10
|
||||
msgid "Deny"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11
|
||||
msgid "Allow"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
|
||||
msgid "Remember choice for this split"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
|
||||
msgid "Reload configuration to show this prompt again"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
|
||||
msgid ""
|
||||
"An application is attempting to write to the clipboard. The current "
|
||||
"clipboard contents are shown below."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
|
||||
msgid "Warning: Potentially Unsafe Paste"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7
|
||||
msgid ""
|
||||
"Pasting this text into the terminal may be dangerous as it looks like some "
|
||||
"commands may be executed."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Window.zig:208
|
||||
#: src/apprt/gtk/Window.zig:216
|
||||
msgid "Main Menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Window.zig:229
|
||||
#: src/apprt/gtk/Window.zig:238
|
||||
msgid "View Open Tabs"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Window.zig:256
|
||||
#: src/apprt/gtk/Window.zig:264
|
||||
msgid "New Split"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Window.zig:319
|
||||
#: src/apprt/gtk/Window.zig:327
|
||||
msgid ""
|
||||
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Window.zig:765
|
||||
#: src/apprt/gtk/Window.zig:773
|
||||
msgid "Reloaded the configuration"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Window.zig:1005
|
||||
#: src/apprt/gtk/Window.zig:1017
|
||||
msgid "Ghostty Developers"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -270,6 +293,6 @@ msgstr ""
|
|||
msgid "The currently running process in this split will be terminated."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:1243
|
||||
#: src/apprt/gtk/Surface.zig:1257
|
||||
msgid "Copied to clipboard"
|
||||
msgstr ""
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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 ""
|
||||
"D’fhéadfadh sé a bheith contúirteach an téacs seo a ghreamú isteach sa "
|
||||
"teirminéal, toisc go d'fhéadfadh roinnt orduithe a fhorghníomhú."
|
||||
|
||||
#: src/apprt/gtk/Window.zig:208
|
||||
msgid "Main Menu"
|
||||
msgstr "Príomh-Roghchlár"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:229
|
||||
msgid "View Open Tabs"
|
||||
msgstr "Féach ar na táib oscailte"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:256
|
||||
msgid "New Split"
|
||||
msgstr "Scoilt nua"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:319
|
||||
msgid ""
|
||||
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
|
||||
msgstr ""
|
||||
"⚠️ Tá leagan dífhabhtaithe de Ghostty á rith agat! Laghdófar an fheidhmíocht."
|
||||
|
||||
#: src/apprt/gtk/Window.zig:765
|
||||
msgid "Reloaded the configuration"
|
||||
msgstr "Tá an chumraíocht athlódáilte"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:1005
|
||||
msgid "Ghostty Developers"
|
||||
msgstr "Forbróirí Ghostty"
|
||||
|
||||
#: src/apprt/gtk/inspector.zig:144
|
||||
msgid "Ghostty: Terminal Inspector"
|
||||
msgstr "Ghostty: Cigire teirminéil"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:47
|
||||
msgid "Close"
|
||||
msgstr "Dún"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:87
|
||||
msgid "Quit Ghostty?"
|
||||
msgstr "Scoir Ghostty?"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:88
|
||||
msgid "Close Window?"
|
||||
msgstr "Dún fuinneog?"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:89
|
||||
msgid "Close Tab?"
|
||||
msgstr "Dún táb?"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:90
|
||||
msgid "Close Split?"
|
||||
msgstr "Dún an scoilt?"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:96
|
||||
msgid "All terminal sessions will be terminated."
|
||||
msgstr "Cuirfear deireadh le gach seisiún teirminéil."
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:97
|
||||
msgid "All terminal sessions in this window will be terminated."
|
||||
msgstr "Cuirfear deireadh le gach seisiún teirminéil san fhuinneog seo."
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:98
|
||||
msgid "All terminal sessions in this tab will be terminated."
|
||||
msgstr "Cuirfear deireadh le gach seisiún teirminéil sa táb seo."
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:99
|
||||
msgid "The currently running process in this split will be terminated."
|
||||
msgstr ""
|
||||
"Cuirfear deireadh leis an bpróiseas atá ar siúl faoi láthair sa scoilt seo."
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:1243
|
||||
msgid "Copied to clipboard"
|
||||
msgstr "Cóipeáilte chuig an ghearrthaisce"
|
||||
|
|
@ -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 개발자들"
|
||||
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2025-04-23 16:58+0800\n"
|
||||
"POT-Creation-Date: 2025-06-28 17:01+0200\n"
|
||||
"PO-Revision-Date: 2025-02-27 09:16+0100\n"
|
||||
"Last-Translator: Leah <hi@pluie.me>\n"
|
||||
"Language-Team: Chinese (simplified) <i18n-zh@googlegroups.com>\n"
|
||||
|
|
@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title."
|
|||
msgstr "留空以重置至默认标题。"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10
|
||||
#: src/apprt/gtk/CloseDialog.zig:44
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
|
|
@ -35,10 +36,12 @@ msgid "OK"
|
|||
msgstr "确认"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5
|
||||
msgid "Configuration Errors"
|
||||
msgstr "配置错误"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
|
||||
msgid ""
|
||||
"One or more configuration errors were found. Please review the errors below, "
|
||||
"and either reload your configuration or ignore these errors."
|
||||
|
|
@ -46,12 +49,14 @@ msgstr ""
|
|||
"加载配置时发现了以下错误。请仔细阅读错误信息,并选择忽略或重新加载配置文件。"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9
|
||||
msgid "Ignore"
|
||||
msgstr "忽略"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10
|
||||
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100
|
||||
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10
|
||||
msgid "Reload Configuration"
|
||||
msgstr "重新加载配置"
|
||||
|
||||
|
|
@ -90,7 +95,7 @@ msgstr "复制"
|
|||
|
||||
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11
|
||||
msgid "Paste"
|
||||
msgstr "粘贴"
|
||||
|
||||
|
|
@ -120,7 +125,7 @@ msgstr "标签页"
|
|||
|
||||
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
|
||||
#: src/apprt/gtk/Window.zig:255
|
||||
#: src/apprt/gtk/Window.zig:263
|
||||
msgid "New Tab"
|
||||
msgstr "新建标签页"
|
||||
|
||||
|
|
@ -161,7 +166,7 @@ msgid "Terminal Inspector"
|
|||
msgstr "终端调试器"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
|
||||
#: src/apprt/gtk/Window.zig:1024
|
||||
#: src/apprt/gtk/Window.zig:1036
|
||||
msgid "About Ghostty"
|
||||
msgstr "关于 Ghostty"
|
||||
|
||||
|
|
@ -171,10 +176,13 @@ msgstr "退出"
|
|||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6
|
||||
msgid "Authorize Clipboard Access"
|
||||
msgstr "剪贴板访问授权"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7
|
||||
msgid ""
|
||||
"An application is attempting to read from the clipboard. The current "
|
||||
"clipboard contents are shown below."
|
||||
|
|
@ -182,52 +190,67 @@ msgstr "一个应用正在试图从剪贴板读取内容。剪贴板目前的内
|
|||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10
|
||||
msgid "Deny"
|
||||
msgstr "拒绝"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11
|
||||
msgid "Allow"
|
||||
msgstr "允许"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
|
||||
msgid "Remember choice for this split"
|
||||
msgstr "为本分屏记住当前选择"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
|
||||
msgid "Reload configuration to show this prompt again"
|
||||
msgstr "本提示将在重载配置后再次出现"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
|
||||
msgid ""
|
||||
"An application is attempting to write to the clipboard. The current "
|
||||
"clipboard contents are shown below."
|
||||
msgstr "一个应用正在试图向剪贴板写入内容。剪贴板目前的内容如下:"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
|
||||
msgid "Warning: Potentially Unsafe Paste"
|
||||
msgstr "警告:粘贴内容可能不安全"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7
|
||||
msgid ""
|
||||
"Pasting this text into the terminal may be dangerous as it looks like some "
|
||||
"commands may be executed."
|
||||
msgstr "将以下内容粘贴至终端内将可能执行有害命令。"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:208
|
||||
#: src/apprt/gtk/Window.zig:216
|
||||
msgid "Main Menu"
|
||||
msgstr "主菜单"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:229
|
||||
#: src/apprt/gtk/Window.zig:238
|
||||
msgid "View Open Tabs"
|
||||
msgstr "浏览标签页"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:256
|
||||
#: src/apprt/gtk/Window.zig:264
|
||||
msgid "New Split"
|
||||
msgstr "新建分屏"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:319
|
||||
#: src/apprt/gtk/Window.zig:327
|
||||
msgid ""
|
||||
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
|
||||
msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:765
|
||||
#: src/apprt/gtk/Window.zig:773
|
||||
msgid "Reloaded the configuration"
|
||||
msgstr "已重新加载配置"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:1005
|
||||
#: src/apprt/gtk/Window.zig:1017
|
||||
msgid "Ghostty Developers"
|
||||
msgstr "Ghostty 开发团队"
|
||||
|
||||
|
|
@ -271,6 +294,6 @@ msgstr "标签页内所有运行中的进程将被终止。"
|
|||
msgid "The currently running process in this split will be terminated."
|
||||
msgstr "分屏内正在运行中的进程将被终止。"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:1243
|
||||
#: src/apprt/gtk/Surface.zig:1257
|
||||
msgid "Copied to clipboard"
|
||||
msgstr "已复制至剪贴板"
|
||||
|
|
|
|||
|
|
@ -76,13 +76,11 @@ parts:
|
|||
- git
|
||||
- patchelf
|
||||
- gettext
|
||||
# TODO: Remove -fno-sys=gtk4-layer-shell when we upgrade to a version that packages it Ubuntu 24.10+
|
||||
override-build: |
|
||||
craftctl set version=$(cat VERSION)
|
||||
$CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast -Dcpu=baseline
|
||||
$CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast -Dcpu=baseline -fno-sys=gtk4-layer-shell
|
||||
cp -rp zig-out/* $CRAFT_PART_INSTALL/
|
||||
# Install libgtk4-layer-shell.so
|
||||
mkdir -p $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR
|
||||
cp .zig-cache/*/*/libgtk4-layer-shell.so $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/
|
||||
sed -i 's|Icon=com.mitchellh.ghostty|Icon=${SNAP}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop
|
||||
|
||||
libs:
|
||||
|
|
|
|||
30
src/App.zig
|
|
@ -76,34 +76,38 @@ first: bool = true,
|
|||
|
||||
pub const CreateError = Allocator.Error || font.SharedGridSet.InitError;
|
||||
|
||||
/// Create a new app instance. This returns a stable pointer to the app
|
||||
/// instance which is required for callbacks.
|
||||
pub fn create(alloc: Allocator) CreateError!*App {
|
||||
var app = try alloc.create(App);
|
||||
errdefer alloc.destroy(app);
|
||||
try app.init(alloc);
|
||||
return app;
|
||||
}
|
||||
|
||||
/// Initialize the main app instance. This creates the main window, sets
|
||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
||||
/// "startup" logic.
|
||||
///
|
||||
/// After calling this function, well behaved apprts should then call
|
||||
/// `focusEvent` to set the initial focus state of the app.
|
||||
pub fn create(
|
||||
pub fn init(
|
||||
self: *App,
|
||||
alloc: Allocator,
|
||||
) CreateError!*App {
|
||||
var app = try alloc.create(App);
|
||||
errdefer alloc.destroy(app);
|
||||
|
||||
) CreateError!void {
|
||||
var font_grid_set = try font.SharedGridSet.init(alloc);
|
||||
errdefer font_grid_set.deinit();
|
||||
|
||||
app.* = .{
|
||||
self.* = .{
|
||||
.alloc = alloc,
|
||||
.surfaces = .{},
|
||||
.mailbox = .{},
|
||||
.font_grid_set = font_grid_set,
|
||||
.config_conditional_state = .{},
|
||||
};
|
||||
errdefer app.surfaces.deinit(alloc);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
pub fn destroy(self: *App) void {
|
||||
pub fn deinit(self: *App) void {
|
||||
// Clean up all our surfaces
|
||||
for (self.surfaces.items) |surface| surface.deinit();
|
||||
self.surfaces.deinit(self.alloc);
|
||||
|
|
@ -114,7 +118,13 @@ pub fn destroy(self: *App) void {
|
|||
// should gracefully close all surfaces.
|
||||
assert(self.font_grid_set.count() == 0);
|
||||
self.font_grid_set.deinit();
|
||||
}
|
||||
|
||||
pub fn destroy(self: *App) void {
|
||||
// Deinitialize the app
|
||||
self.deinit();
|
||||
|
||||
// Free the app memory
|
||||
self.alloc.destroy(self);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -188,10 +188,31 @@ fn startPosix(self: *Command, arena: Allocator) !void {
|
|||
// Finally, replace our process.
|
||||
// Note: we must use the "p"-variant of exec here because we
|
||||
// do not guarantee our command is looked up already in the path.
|
||||
_ = posix.execvpeZ(self.path, argsZ, envp) catch null;
|
||||
const err = posix.execvpeZ(self.path, argsZ, envp);
|
||||
|
||||
// If we are executing this code, the exec failed. In that scenario,
|
||||
// we return a very specific error that can be detected to determine
|
||||
// If we are executing this code, the exec failed. We're in the
|
||||
// child process so there isn't much we can do. We try to output
|
||||
// something reasonable. Its important to note we MUST NOT return
|
||||
// any other error condition from here on out.
|
||||
const stderr = std.io.getStdErr().writer();
|
||||
switch (err) {
|
||||
error.FileNotFound => stderr.print(
|
||||
\\Requested executable not found. Please verify the command is on
|
||||
\\the PATH and try again.
|
||||
\\
|
||||
,
|
||||
.{},
|
||||
) catch {},
|
||||
|
||||
else => stderr.print(
|
||||
\\exec syscall failed with unexpected error: {}
|
||||
\\
|
||||
,
|
||||
.{err},
|
||||
) catch {},
|
||||
}
|
||||
|
||||
// We return a very specific error that can be detected to determine
|
||||
// we're in the child.
|
||||
return error.ExecFailedInChild;
|
||||
}
|
||||
|
|
|
|||
288
src/Surface.zig
|
|
@ -138,6 +138,9 @@ child_exited: bool = false,
|
|||
/// to let us know.
|
||||
focused: bool = true,
|
||||
|
||||
/// Used to determine whether to continuously scroll.
|
||||
selection_scroll_active: bool = false,
|
||||
|
||||
/// The effect of an input event. This can be used by callers to take
|
||||
/// the appropriate action after an input event. For example, key
|
||||
/// input can be forwarded to the OS for further processing if it
|
||||
|
|
@ -237,6 +240,7 @@ const DerivedConfig = struct {
|
|||
/// For docs for these, see the associated config they are derived from.
|
||||
original_font_size: f32,
|
||||
keybind: configpkg.Keybinds,
|
||||
abnormal_command_exit_runtime_ms: u32,
|
||||
clipboard_read: configpkg.ClipboardAccess,
|
||||
clipboard_write: configpkg.ClipboardAccess,
|
||||
clipboard_trim_trailing_spaces: bool,
|
||||
|
|
@ -255,6 +259,7 @@ const DerivedConfig = struct {
|
|||
macos_option_as_alt: ?configpkg.OptionAsAlt,
|
||||
selection_clear_on_typing: bool,
|
||||
vt_kam_allowed: bool,
|
||||
wait_after_command: bool,
|
||||
window_padding_top: u32,
|
||||
window_padding_bottom: u32,
|
||||
window_padding_left: u32,
|
||||
|
|
@ -301,6 +306,7 @@ const DerivedConfig = struct {
|
|||
return .{
|
||||
.original_font_size = config.@"font-size",
|
||||
.keybind = try config.keybind.clone(alloc),
|
||||
.abnormal_command_exit_runtime_ms = config.@"abnormal-command-exit-runtime",
|
||||
.clipboard_read = config.@"clipboard-read",
|
||||
.clipboard_write = config.@"clipboard-write",
|
||||
.clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces",
|
||||
|
|
@ -319,6 +325,7 @@ const DerivedConfig = struct {
|
|||
.macos_option_as_alt = config.@"macos-option-as-alt",
|
||||
.selection_clear_on_typing = config.@"selection-clear-on-typing",
|
||||
.vt_kam_allowed = config.@"vt-kam-allowed",
|
||||
.wait_after_command = config.@"wait-after-command",
|
||||
.window_padding_top = config.@"window-padding-y".top_left,
|
||||
.window_padding_bottom = config.@"window-padding-y".bottom_right,
|
||||
.window_padding_left = config.@"window-padding-x".top_left,
|
||||
|
|
@ -911,11 +918,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
|||
|
||||
.close => self.close(),
|
||||
|
||||
// Close without confirmation.
|
||||
.child_exited => {
|
||||
self.child_exited = true;
|
||||
self.close();
|
||||
},
|
||||
.child_exited => |v| self.childExited(v),
|
||||
|
||||
.desktop_notification => |notification| {
|
||||
if (!self.config.desktop_notifications) {
|
||||
|
|
@ -945,9 +948,182 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
|||
log.warn("apprt failed to ring bell={}", .{err});
|
||||
};
|
||||
},
|
||||
|
||||
.selection_scroll_tick => |active| {
|
||||
self.selection_scroll_active = active;
|
||||
try self.selectionScrollTick();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn selectionScrollTick(self: *Surface) !void {
|
||||
// If we're no longer active then we don't do anything.
|
||||
if (!self.selection_scroll_active) return;
|
||||
|
||||
// If we don't have a left mouse button down then we
|
||||
// don't do anything.
|
||||
if (self.mouse.left_click_count == 0) return;
|
||||
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
const pos_vp = self.posToViewport(pos.x, pos.y);
|
||||
const delta: isize = if (pos.y < 0) -1 else 1;
|
||||
|
||||
// We need our locked state for the remainder
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
const t: *terminal.Terminal = self.renderer_state.terminal;
|
||||
|
||||
// Scroll the viewport as required
|
||||
try t.scrollViewport(.{ .delta = delta });
|
||||
|
||||
// Next, trigger our drag behavior
|
||||
const pin = t.screen.pages.pin(.{
|
||||
.viewport = .{
|
||||
.x = pos_vp.x,
|
||||
.y = pos_vp.y,
|
||||
},
|
||||
}) orelse {
|
||||
if (comptime std.debug.runtime_safety) unreachable;
|
||||
return;
|
||||
};
|
||||
try self.dragLeftClickSingle(pin, pos.x);
|
||||
|
||||
// We modified our viewport and selection so we need to queue
|
||||
// a render.
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
fn childExited(self: *Surface, info: apprt.surface.Message.ChildExited) void {
|
||||
// Mark our flag that we exited immediately
|
||||
self.child_exited = true;
|
||||
|
||||
// If our runtime was below some threshold then we assume that this
|
||||
// was an abnormal exit and we show an error message.
|
||||
if (info.runtime_ms <= self.config.abnormal_command_exit_runtime_ms) runtime: {
|
||||
// On macOS, our exit code detection doesn't work, possibly
|
||||
// because of our `login` wrapper. More investigation required.
|
||||
if (comptime !builtin.target.os.tag.isDarwin()) {
|
||||
// If the exit code is 0 then it was a good exit.
|
||||
if (info.exit_code == 0) break :runtime;
|
||||
}
|
||||
|
||||
log.warn("abnormal process exit detected, showing error message", .{});
|
||||
|
||||
// Update our terminal to note the abnormal exit. In the future we
|
||||
// may want the apprt to handle this to show some native GUI element.
|
||||
self.childExitedAbnormally(info) catch |err| {
|
||||
log.err("error handling abnormal child exit err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// We output a message so that the user knows whats going on and
|
||||
// doesn't think their terminal just froze. We show this unconditionally
|
||||
// on close even if `wait_after_command` is false and the surface closes
|
||||
// immediately because if a user does an `undo` to restore a closed
|
||||
// surface then they will see this message and know the process has
|
||||
// completed.
|
||||
terminal: {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
const t: *terminal.Terminal = self.renderer_state.terminal;
|
||||
t.carriageReturn();
|
||||
t.linefeed() catch break :terminal;
|
||||
t.printString("Process exited. Press any key to close the terminal.") catch
|
||||
break :terminal;
|
||||
t.modes.set(.cursor_visible, false);
|
||||
}
|
||||
|
||||
// Waiting after command we stop here. The terminal is updated, our
|
||||
// state is updated, and now its up to the user to decide what to do.
|
||||
if (self.config.wait_after_command) return;
|
||||
|
||||
// If we aren't waiting after the command, then we exit immediately
|
||||
// with no confirmation.
|
||||
self.close();
|
||||
}
|
||||
|
||||
/// Called when the child process exited abnormally.
|
||||
fn childExitedAbnormally(
|
||||
self: *Surface,
|
||||
info: apprt.surface.Message.ChildExited,
|
||||
) !void {
|
||||
var arena = ArenaAllocator.init(self.alloc);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// Build up our command for the error message
|
||||
const command = try std.mem.join(alloc, " ", switch (self.io.backend) {
|
||||
.exec => |*exec| exec.subprocess.args,
|
||||
});
|
||||
const runtime_str = try std.fmt.allocPrint(alloc, "{d} ms", .{info.runtime_ms});
|
||||
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
const t: *terminal.Terminal = self.renderer_state.terminal;
|
||||
|
||||
// No matter what move the cursor back to the column 0.
|
||||
t.carriageReturn();
|
||||
|
||||
// Reset styles
|
||||
try t.setAttribute(.{ .unset = {} });
|
||||
|
||||
// If there is data in the viewport, we want to scroll down
|
||||
// a little bit and write a horizontal rule before writing
|
||||
// our message. This lets the use see the error message the
|
||||
// command may have output.
|
||||
const viewport_str = try t.plainString(alloc);
|
||||
if (viewport_str.len > 0) {
|
||||
try t.linefeed();
|
||||
for (0..t.cols) |_| try t.print(0x2501);
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.linefeed();
|
||||
}
|
||||
|
||||
// Output our error message
|
||||
try t.setAttribute(.{ .@"8_fg" = .bright_red });
|
||||
try t.setAttribute(.{ .bold = {} });
|
||||
try t.printString("Ghostty failed to launch the requested command:");
|
||||
try t.setAttribute(.{ .unset = {} });
|
||||
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.printString(command);
|
||||
try t.setAttribute(.{ .unset = {} });
|
||||
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.printString("Runtime: ");
|
||||
try t.setAttribute(.{ .@"8_fg" = .red });
|
||||
try t.printString(runtime_str);
|
||||
try t.setAttribute(.{ .unset = {} });
|
||||
|
||||
// We don't print this on macOS because the exit code is always 0
|
||||
// due to the way we launch the process.
|
||||
if (comptime !builtin.target.os.tag.isDarwin()) {
|
||||
const exit_code_str = try std.fmt.allocPrint(alloc, "{d}", .{info.exit_code});
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.printString("Exit Code: ");
|
||||
try t.setAttribute(.{ .@"8_fg" = .red });
|
||||
try t.printString(exit_code_str);
|
||||
try t.setAttribute(.{ .unset = {} });
|
||||
}
|
||||
|
||||
t.carriageReturn();
|
||||
try t.linefeed();
|
||||
try t.linefeed();
|
||||
try t.printString("Press any key to close the window.");
|
||||
|
||||
// Hide the cursor
|
||||
t.modes.set(.cursor_visible, false);
|
||||
}
|
||||
|
||||
/// Called when the terminal detects there is a password input prompt.
|
||||
fn passwordInput(self: *Surface, v: bool) !void {
|
||||
{
|
||||
|
|
@ -1953,6 +2129,14 @@ pub fn keyCallback(
|
|||
if (self.io.terminal.modes.get(.disable_keyboard)) return .consumed;
|
||||
}
|
||||
|
||||
// If our process is exited and we press a key then we close the
|
||||
// surface. We may want to eventually move this to the apprt rather
|
||||
// than in core.
|
||||
if (self.child_exited and event.action == .press) {
|
||||
self.close();
|
||||
return .closed;
|
||||
}
|
||||
|
||||
// If this input event has text, then we hide the mouse if configured.
|
||||
// We only do this on pressed events to avoid hiding the mouse when we
|
||||
// change focus due to a keybinding (i.e. switching tabs).
|
||||
|
|
@ -3094,15 +3278,42 @@ pub fn mouseButtonCallback(
|
|||
}
|
||||
}
|
||||
|
||||
// Handle link clicking. We want to do this before we do mouse
|
||||
// reporting or any other mouse handling because a successfully
|
||||
// clicked link will swallow the event.
|
||||
if (button == .left and action == .release and self.mouse.over_link) {
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
if (self.processLinks(pos)) |processed| {
|
||||
if (processed) return true;
|
||||
} else |err| {
|
||||
log.warn("error processing links err={}", .{err});
|
||||
if (button == .left and action == .release) {
|
||||
// Stop selection scrolling when releasing the left mouse button
|
||||
// but only when selection scrolling is active.
|
||||
if (self.selection_scroll_active) {
|
||||
self.io.queueMessage(
|
||||
.{ .selection_scroll = false },
|
||||
.unlocked,
|
||||
);
|
||||
}
|
||||
|
||||
// The selection clipboard is only updated for left-click drag when
|
||||
// the left button is released. This is to avoid the clipboard
|
||||
// being updated on every mouse move which would be noisy.
|
||||
if (self.config.copy_on_select != .false) {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
const prev_ = self.io.terminal.screen.selection;
|
||||
if (prev_) |prev| {
|
||||
try self.setSelection(terminal.Selection.init(
|
||||
prev.start(),
|
||||
prev.end(),
|
||||
false,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle link clicking. We want to do this before we do mouse
|
||||
// reporting or any other mouse handling because a successfully
|
||||
// clicked link will swallow the event.
|
||||
if (self.mouse.over_link) {
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
if (self.processLinks(pos)) |processed| {
|
||||
if (processed) return true;
|
||||
} else |err| {
|
||||
log.warn("error processing links err={}", .{err});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3238,12 +3449,16 @@ pub fn mouseButtonCallback(
|
|||
log.err("error reading time, mouse multi-click won't work err={}", .{err});
|
||||
}
|
||||
|
||||
// In all cases below, we set the selection directly rather than use
|
||||
// `setSelection` because we want to avoid copying the selection
|
||||
// to the selection clipboard. For left mouse clicks we only set
|
||||
// the clipboard on release.
|
||||
switch (self.mouse.left_click_count) {
|
||||
// Single click
|
||||
1 => {
|
||||
// If we have a selection, clear it. This always happens.
|
||||
if (self.io.terminal.screen.selection != null) {
|
||||
try self.setSelection(null);
|
||||
try self.io.terminal.screen.select(null);
|
||||
try self.queueRender();
|
||||
}
|
||||
},
|
||||
|
|
@ -3252,7 +3467,7 @@ pub fn mouseButtonCallback(
|
|||
2 => {
|
||||
const sel_ = self.io.terminal.screen.selectWord(pin.*);
|
||||
if (sel_) |sel| {
|
||||
try self.setSelection(sel);
|
||||
try self.io.terminal.screen.select(sel);
|
||||
try self.queueRender();
|
||||
}
|
||||
},
|
||||
|
|
@ -3264,7 +3479,7 @@ pub fn mouseButtonCallback(
|
|||
else
|
||||
self.io.terminal.screen.selectLine(.{ .pin = pin.* });
|
||||
if (sel_) |sel| {
|
||||
try self.setSelection(sel);
|
||||
try self.io.terminal.screen.select(sel);
|
||||
try self.queueRender();
|
||||
}
|
||||
},
|
||||
|
|
@ -3549,7 +3764,7 @@ pub fn mousePressureCallback(
|
|||
// to handle state inconsistency here.
|
||||
const pin = self.mouse.left_click_pin orelse break :select;
|
||||
const sel = self.io.terminal.screen.selectWord(pin.*) orelse break :select;
|
||||
try self.setSelection(sel);
|
||||
try self.io.terminal.screen.select(sel);
|
||||
try self.queueRender();
|
||||
}
|
||||
}
|
||||
|
|
@ -3626,6 +3841,15 @@ pub fn cursorPosCallback(
|
|||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// Stop selection scrolling when inside the viewport within a 1px buffer
|
||||
// for fullscreen windows, but only when selection scrolling is active.
|
||||
if (pos.x >= 1 and pos.y >= 1 and self.selection_scroll_active) {
|
||||
self.io.queueMessage(
|
||||
.{ .selection_scroll = false },
|
||||
.locked,
|
||||
);
|
||||
}
|
||||
|
||||
// Update our mouse state. We set this to null initially because we only
|
||||
// want to set it when we're not selecting or doing any other mouse
|
||||
// event.
|
||||
|
|
@ -3708,13 +3932,16 @@ pub fn cursorPosCallback(
|
|||
// Note: one day, we can change this from distance to time based if we want.
|
||||
//log.warn("CURSOR POS: {} {}", .{ pos, self.size.screen });
|
||||
const max_y: f32 = @floatFromInt(self.size.screen.height);
|
||||
if (pos.y <= 1 or pos.y > max_y - 1) {
|
||||
const delta: isize = if (pos.y < 0) -1 else 1;
|
||||
try self.io.terminal.scrollViewport(.{ .delta = delta });
|
||||
|
||||
// TODO: We want a timer or something to repeat while we're still
|
||||
// at this cursor position. Right now, the user has to jiggle their
|
||||
// mouse in order to scroll.
|
||||
// If the mouse is outside the viewport and we have the left
|
||||
// mouse button pressed then we need to start the scroll timer.
|
||||
if ((pos.y <= 1 or pos.y > max_y - 1) and
|
||||
!self.selection_scroll_active)
|
||||
{
|
||||
self.io.queueMessage(
|
||||
.{ .selection_scroll = true },
|
||||
.locked,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert to points
|
||||
|
|
@ -3768,13 +3995,13 @@ fn dragLeftClickDouble(
|
|||
// If our current mouse position is before the starting position,
|
||||
// then the selection start is the word nearest our current position.
|
||||
if (drag_pin.before(click_pin)) {
|
||||
try self.setSelection(terminal.Selection.init(
|
||||
try self.io.terminal.screen.select(.init(
|
||||
word_current.start(),
|
||||
word_start.end(),
|
||||
false,
|
||||
));
|
||||
} else {
|
||||
try self.setSelection(terminal.Selection.init(
|
||||
try self.io.terminal.screen.select(.init(
|
||||
word_start.start(),
|
||||
word_current.end(),
|
||||
false,
|
||||
|
|
@ -3806,7 +4033,7 @@ fn dragLeftClickTriple(
|
|||
} else {
|
||||
sel.endPtr().* = line.end();
|
||||
}
|
||||
try self.setSelection(sel);
|
||||
try self.io.terminal.screen.select(sel);
|
||||
}
|
||||
|
||||
fn dragLeftClickSingle(
|
||||
|
|
@ -3815,7 +4042,7 @@ fn dragLeftClickSingle(
|
|||
drag_x: f64,
|
||||
) !void {
|
||||
// This logic is in a separate function so that it can be unit tested.
|
||||
try self.setSelection(mouseSelection(
|
||||
try self.io.terminal.screen.select(mouseSelection(
|
||||
self.mouse.left_click_pin.?.*,
|
||||
drag_pin,
|
||||
@intFromFloat(@max(0.0, self.mouse.left_click_xpos)),
|
||||
|
|
@ -4685,6 +4912,11 @@ fn writeScreenFile(
|
|||
const path = try tmp_dir.dir.realpath(filename, &path_buf);
|
||||
|
||||
switch (write_action) {
|
||||
.copy => {
|
||||
const pathZ = try self.alloc.dupeZ(u8, path);
|
||||
defer self.alloc.free(pathZ);
|
||||
try self.rt_surface.setClipboardString(pathZ, .standard, false);
|
||||
},
|
||||
.open => try internal_os.open(self.alloc, .text, path),
|
||||
.paste => self.io.queueMessage(try termio.Message.writeReq(
|
||||
self.alloc,
|
||||
|
|
|
|||
|
|
@ -117,10 +117,11 @@ pub const App = struct {
|
|||
config: Config,
|
||||
|
||||
pub fn init(
|
||||
self: *App,
|
||||
core_app: *CoreApp,
|
||||
config: *const Config,
|
||||
opts: Options,
|
||||
) !App {
|
||||
) !void {
|
||||
// We have to clone the config.
|
||||
const alloc = core_app.alloc;
|
||||
var config_clone = try config.clone(alloc);
|
||||
|
|
@ -129,7 +130,7 @@ pub const App = struct {
|
|||
var keymap = try input.Keymap.init();
|
||||
errdefer keymap.deinit();
|
||||
|
||||
return .{
|
||||
self.* = .{
|
||||
.core_app = core_app,
|
||||
.config = config_clone,
|
||||
.opts = opts,
|
||||
|
|
@ -1316,13 +1317,13 @@ pub const CAPI = struct {
|
|||
opts: *const apprt.runtime.App.Options,
|
||||
config: *const Config,
|
||||
) !*App {
|
||||
var core_app = try CoreApp.create(global.alloc);
|
||||
const core_app = try CoreApp.create(global.alloc);
|
||||
errdefer core_app.destroy();
|
||||
|
||||
// Create our runtime app
|
||||
var app = try global.alloc.create(App);
|
||||
errdefer global.alloc.destroy(app);
|
||||
app.* = try .init(core_app, config, opts.*);
|
||||
try app.init(core_app, config, opts.*);
|
||||
errdefer app.terminate();
|
||||
|
||||
return app;
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ pub const App = struct {
|
|||
|
||||
pub const Options = struct {};
|
||||
|
||||
pub fn init(core_app: *CoreApp, _: Options) !App {
|
||||
pub fn init(self: *App, core_app: *CoreApp, _: Options) !void {
|
||||
if (comptime builtin.target.os.tag.isDarwin()) {
|
||||
log.warn("WARNING WARNING WARNING: GLFW ON MAC HAS BUGS.", .{});
|
||||
log.warn("You should use the AppKit-based app instead. The official download", .{});
|
||||
|
|
@ -107,7 +107,7 @@ pub const App = struct {
|
|||
// We want the event loop to wake up instantly so we can process our tick.
|
||||
glfw.postEmptyEvent();
|
||||
|
||||
return .{
|
||||
self.* = .{
|
||||
.app = core_app,
|
||||
.config = config,
|
||||
.darwin = darwin,
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ quit_timer: union(enum) {
|
|||
expired: void,
|
||||
} = .{ .off = {} },
|
||||
|
||||
pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
pub fn init(self: *App, core_app: *CoreApp, opts: Options) !void {
|
||||
_ = opts;
|
||||
|
||||
// Log our GTK version
|
||||
|
|
@ -373,6 +373,13 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||
.{},
|
||||
);
|
||||
|
||||
// Setup a listener for SIGUSR2 to reload the configuration.
|
||||
_ = glib.unixSignalAdd(
|
||||
std.posix.SIG.USR2,
|
||||
sigusr2,
|
||||
self,
|
||||
);
|
||||
|
||||
// We don't use g_application_run, we want to manually control the
|
||||
// loop so we have to do the same things the run function does:
|
||||
// https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533
|
||||
|
|
@ -405,11 +412,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||
// This just calls the `activate` signal but its part of the normal startup
|
||||
// routine so we just call it, but only if the config allows it (this allows
|
||||
// for launching Ghostty in the "background" without immediately opening
|
||||
// a window)
|
||||
// a window). An initial window will not be immediately created if we were
|
||||
// launched by D-Bus activation or systemd. D-Bus activation will send it's
|
||||
// own `activate` or `new-window` signal later.
|
||||
//
|
||||
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
||||
if (config.@"initial-window")
|
||||
gio_app.activate();
|
||||
if (config.@"initial-window") switch (config.@"launched-from".?) {
|
||||
.desktop, .cli => gio_app.activate(),
|
||||
.dbus, .systemd => {},
|
||||
};
|
||||
|
||||
// Internally, GTK ensures that only one instance of this provider exists in the provider list
|
||||
// for the display.
|
||||
|
|
@ -420,7 +431,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||
gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 3,
|
||||
);
|
||||
|
||||
return .{
|
||||
self.* = .{
|
||||
.core_app = core_app,
|
||||
.app = adw_app,
|
||||
.config = config,
|
||||
|
|
@ -1504,6 +1515,22 @@ pub fn quitNow(self: *App) void {
|
|||
self.running = false;
|
||||
}
|
||||
|
||||
// SIGUSR2 signal handler via g_unix_signal_add
|
||||
fn sigusr2(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *App = @ptrCast(@alignCast(ud orelse
|
||||
return @intFromBool(glib.SOURCE_CONTINUE)));
|
||||
|
||||
log.info("received SIGUSR2, reloading configuration", .{});
|
||||
self.reloadConfig(.app, .{ .soft = false }) catch |err| {
|
||||
log.err(
|
||||
"error reloading configuration for SIGUSR2: {}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
|
||||
return @intFromBool(glib.SOURCE_CONTINUE);
|
||||
}
|
||||
|
||||
/// This is called by the `activate` signal. This is sent on program startup and
|
||||
/// also when a secondary instance launches and requests a new window.
|
||||
fn gtkActivate(_: *adw.Application, core_app: *CoreApp) callconv(.c) void {
|
||||
|
|
@ -1683,6 +1710,17 @@ fn gtkActionShowGTKInspector(
|
|||
};
|
||||
}
|
||||
|
||||
fn gtkActionNewWindow(
|
||||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *App,
|
||||
) callconv(.c) void {
|
||||
log.info("received new window action", .{});
|
||||
_ = self.core_app.mailbox.push(.{
|
||||
.new_window = .{},
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
/// This is called to setup the action map that this application supports.
|
||||
/// This should be called only once on startup.
|
||||
fn initActions(self: *App) void {
|
||||
|
|
@ -1702,7 +1740,9 @@ fn initActions(self: *App) void {
|
|||
.{ "reload-config", gtkActionReloadConfig, null },
|
||||
.{ "present-surface", gtkActionPresentSurface, t },
|
||||
.{ "show-gtk-inspector", gtkActionShowGTKInspector, null },
|
||||
.{ "new-window", gtkActionNewWindow, null },
|
||||
};
|
||||
|
||||
inline for (actions) |entry| {
|
||||
const action = gio.SimpleAction.new(entry[0], entry[2]);
|
||||
defer action.unref();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const adw_version = @import("adw_version.zig");
|
|||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
const DialogType = if (adw_version.atLeast(1, 5, 0)) adw.AlertDialog else adw.MessageDialog;
|
||||
const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
|
||||
|
||||
app: *App,
|
||||
dialog: *DialogType,
|
||||
|
|
@ -28,6 +28,7 @@ text_view: *gtk.TextView,
|
|||
text_view_scroll: *gtk.ScrolledWindow,
|
||||
reveal_button: *gtk.Button,
|
||||
hide_button: *gtk.Button,
|
||||
remember_choice: if (adw_version.supportsSwitchRow()) ?*adw.SwitchRow else ?*anyopaque,
|
||||
|
||||
pub fn create(
|
||||
app: *App,
|
||||
|
|
@ -89,6 +90,10 @@ fn init(
|
|||
const reveal_button = builder.getObject(gtk.Button, "reveal_button").?;
|
||||
const hide_button = builder.getObject(gtk.Button, "hide_button").?;
|
||||
const text_view_scroll = builder.getObject(gtk.ScrolledWindow, "text_view_scroll").?;
|
||||
const remember_choice = if (adw_version.supportsSwitchRow())
|
||||
builder.getObject(adw.SwitchRow, "remember_choice")
|
||||
else
|
||||
null;
|
||||
|
||||
const copy = try app.core_app.alloc.dupeZ(u8, data);
|
||||
errdefer app.core_app.alloc.free(copy);
|
||||
|
|
@ -102,6 +107,7 @@ fn init(
|
|||
.text_view_scroll = text_view_scroll,
|
||||
.reveal_button = reveal_button,
|
||||
.hide_button = hide_button,
|
||||
.remember_choice = remember_choice,
|
||||
};
|
||||
|
||||
const buffer = gtk.TextBuffer.new(null);
|
||||
|
|
@ -152,8 +158,10 @@ fn init(
|
|||
}
|
||||
}
|
||||
|
||||
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void {
|
||||
if (std.mem.orderZ(u8, response, "ok") == .eq) {
|
||||
fn handleResponse(self: *ClipboardConfirmation, response: [*:0]const u8) void {
|
||||
const is_ok = std.mem.orderZ(u8, response, "ok") == .eq;
|
||||
|
||||
if (is_ok) {
|
||||
self.core_surface.completeClipboardRequest(
|
||||
self.pending_req,
|
||||
self.data,
|
||||
|
|
@ -162,8 +170,30 @@ fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation)
|
|||
log.err("Failed to requeue clipboard request: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
if (self.remember_choice) |remember| remember: {
|
||||
if (!adw_version.supportsSwitchRow()) break :remember;
|
||||
if (remember.getActive() == 0) break :remember;
|
||||
|
||||
switch (self.pending_req) {
|
||||
.osc_52_read => self.core_surface.config.clipboard_read = if (is_ok) .allow else .deny,
|
||||
.osc_52_write => self.core_surface.config.clipboard_write = if (is_ok) .allow else .deny,
|
||||
.paste => {},
|
||||
}
|
||||
}
|
||||
|
||||
self.destroy();
|
||||
}
|
||||
fn gtkChoose(dialog_: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void {
|
||||
const dialog = gobject.ext.cast(DialogType, dialog_.?).?;
|
||||
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud.?));
|
||||
const response = dialog.chooseFinish(result);
|
||||
self.handleResponse(response);
|
||||
}
|
||||
|
||||
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.C) void {
|
||||
self.handleResponse(response);
|
||||
}
|
||||
|
||||
fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
|
||||
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));
|
||||
|
|
|
|||
|
|
@ -2325,6 +2325,15 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap {
|
|||
env.remove("GDK_DISABLE");
|
||||
env.remove("GSK_RENDERER");
|
||||
|
||||
// Remove some environment variables that are set when Ghostty is launched
|
||||
// from a `.desktop` file, by D-Bus activation, or systemd.
|
||||
env.remove("GIO_LAUNCHED_DESKTOP_FILE");
|
||||
env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID");
|
||||
env.remove("DBUS_STARTER_ADDRESS");
|
||||
env.remove("DBUS_STARTER_BUS_TYPE");
|
||||
env.remove("INVOCATION_ID");
|
||||
env.remove("JOURNAL_STREAM");
|
||||
|
||||
// Unset environment varies set by snaps if we're running in a snap.
|
||||
// This allows Ghostty to further launch additional snaps.
|
||||
if (env.get("SNAP")) |_| {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ window: *adw.ApplicationWindow,
|
|||
/// The header bar for the window.
|
||||
headerbar: HeaderBar,
|
||||
|
||||
/// The tab bar for the window.
|
||||
tab_bar: *adw.TabBar,
|
||||
|
||||
/// The tab overview for the window. This is possibly null since there is no
|
||||
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
|
||||
tab_overview: ?*adw.TabOverview,
|
||||
|
|
@ -86,6 +89,7 @@ pub const DerivedConfig = struct {
|
|||
gtk_tabs_location: configpkg.Config.GtkTabsLocation,
|
||||
gtk_wide_tabs: bool,
|
||||
gtk_toolbar_style: configpkg.Config.GtkToolbarStyle,
|
||||
window_show_tab_bar: configpkg.Config.WindowShowTabBar,
|
||||
|
||||
quick_terminal_position: configpkg.Config.QuickTerminalPosition,
|
||||
quick_terminal_size: configpkg.Config.QuickTerminalSize,
|
||||
|
|
@ -106,6 +110,7 @@ pub const DerivedConfig = struct {
|
|||
.gtk_tabs_location = config.@"gtk-tabs-location",
|
||||
.gtk_wide_tabs = config.@"gtk-wide-tabs",
|
||||
.gtk_toolbar_style = config.@"gtk-toolbar-style",
|
||||
.window_show_tab_bar = config.@"window-show-tab-bar",
|
||||
|
||||
.quick_terminal_position = config.@"quick-terminal-position",
|
||||
.quick_terminal_size = config.@"quick-terminal-size",
|
||||
|
|
@ -141,6 +146,7 @@ pub fn init(self: *Window, app: *App) !void {
|
|||
.config = .init(&app.config),
|
||||
.window = undefined,
|
||||
.headerbar = undefined,
|
||||
.tab_bar = undefined,
|
||||
.tab_overview = null,
|
||||
.notebook = undefined,
|
||||
.titlebar_menu = undefined,
|
||||
|
|
@ -225,8 +231,9 @@ pub fn init(self: *Window, app: *App) !void {
|
|||
// If we're using an AdwWindow then we can support the tab overview.
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (!adw_version.supportsTabOverview()) unreachable;
|
||||
const btn = switch (self.config.gtk_tabs_location) {
|
||||
.top, .bottom => btn: {
|
||||
|
||||
const btn = switch (self.config.window_show_tab_bar) {
|
||||
.always, .auto => btn: {
|
||||
const btn = gtk.ToggleButton.new();
|
||||
btn.as(gtk.Widget).setTooltipText(i18n._("View Open Tabs"));
|
||||
btn.as(gtk.Button).setIconName("view-grid-symbolic");
|
||||
|
|
@ -238,8 +245,7 @@ pub fn init(self: *Window, app: *App) !void {
|
|||
);
|
||||
break :btn btn.as(gtk.Widget);
|
||||
},
|
||||
|
||||
.hidden => btn: {
|
||||
.never => btn: {
|
||||
const btn = adw.TabButton.new();
|
||||
btn.setView(self.notebook.tab_view);
|
||||
btn.as(gtk.Actionable).setActionName("overview.open");
|
||||
|
|
@ -385,21 +391,16 @@ pub fn init(self: *Window, app: *App) !void {
|
|||
// Our actions for the menu
|
||||
initActions(self);
|
||||
|
||||
self.tab_bar = adw.TabBar.new();
|
||||
self.tab_bar.setView(self.notebook.tab_view);
|
||||
|
||||
if (adw_version.supportsToolbarView()) {
|
||||
const toolbar_view = adw.ToolbarView.new();
|
||||
toolbar_view.addTopBar(self.headerbar.asWidget());
|
||||
|
||||
if (self.config.gtk_tabs_location != .hidden) {
|
||||
const tab_bar = adw.TabBar.new();
|
||||
tab_bar.setView(self.notebook.tab_view);
|
||||
|
||||
if (!self.config.gtk_wide_tabs) tab_bar.setExpandTabs(0);
|
||||
|
||||
switch (self.config.gtk_tabs_location) {
|
||||
.top => toolbar_view.addTopBar(tab_bar.as(gtk.Widget)),
|
||||
.bottom => toolbar_view.addBottomBar(tab_bar.as(gtk.Widget)),
|
||||
.hidden => unreachable,
|
||||
}
|
||||
switch (self.config.gtk_tabs_location) {
|
||||
.top => toolbar_view.addTopBar(self.tab_bar.as(gtk.Widget)),
|
||||
.bottom => toolbar_view.addBottomBar(self.tab_bar.as(gtk.Widget)),
|
||||
}
|
||||
toolbar_view.setContent(box.as(gtk.Widget));
|
||||
|
||||
|
|
@ -414,23 +415,18 @@ pub fn init(self: *Window, app: *App) !void {
|
|||
// Set our application window content.
|
||||
self.tab_overview.?.setChild(toolbar_view.as(gtk.Widget));
|
||||
self.window.setContent(self.tab_overview.?.as(gtk.Widget));
|
||||
} else tab_bar: {
|
||||
if (self.config.gtk_tabs_location == .hidden) break :tab_bar;
|
||||
} else {
|
||||
// In earlier adwaita versions, we need to add the tabbar manually since we do not use
|
||||
// an AdwToolbarView.
|
||||
const tab_bar = adw.TabBar.new();
|
||||
tab_bar.as(gtk.Widget).addCssClass("inline");
|
||||
self.tab_bar.as(gtk.Widget).addCssClass("inline");
|
||||
|
||||
switch (self.config.gtk_tabs_location) {
|
||||
.top => box.insertChildAfter(
|
||||
tab_bar.as(gtk.Widget),
|
||||
self.tab_bar.as(gtk.Widget),
|
||||
self.headerbar.asWidget(),
|
||||
),
|
||||
.bottom => box.append(tab_bar.as(gtk.Widget)),
|
||||
.hidden => unreachable,
|
||||
.bottom => box.append(self.tab_bar.as(gtk.Widget)),
|
||||
}
|
||||
tab_bar.setView(self.notebook.tab_view);
|
||||
|
||||
if (!self.config.gtk_wide_tabs) tab_bar.setExpandTabs(0);
|
||||
}
|
||||
|
||||
// If we want the window to be maximized, we do that here.
|
||||
|
|
@ -555,6 +551,16 @@ pub fn syncAppearance(self: *Window) !void {
|
|||
}
|
||||
}
|
||||
|
||||
self.tab_bar.setExpandTabs(@intFromBool(self.config.gtk_wide_tabs));
|
||||
self.tab_bar.setAutohide(switch (self.config.window_show_tab_bar) {
|
||||
.auto, .never => @intFromBool(true),
|
||||
.always => @intFromBool(false),
|
||||
});
|
||||
self.tab_bar.as(gtk.Widget).setVisible(switch (self.config.window_show_tab_bar) {
|
||||
.always, .auto => @intFromBool(true),
|
||||
.never => @intFromBool(false),
|
||||
});
|
||||
|
||||
self.winproto.syncAppearance() catch |err| {
|
||||
log.warn("failed to sync winproto appearance error={}", .{err});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -109,6 +109,10 @@ pub inline fn supportsTabOverview() bool {
|
|||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsSwitchRow() bool {
|
||||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsToolbarView() bool {
|
||||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,14 +64,18 @@ window.ssd.no-border-radius {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.clipboard-overlay {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.clipboard-content-view {
|
||||
filter: blur(0px);
|
||||
transition: filter 0.3s ease;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.clipboard-content-view.blurred {
|
||||
filter: blur(5px);
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.command-palette-search {
|
||||
|
|
|
|||
|
|
@ -14,58 +14,72 @@ Adw.AlertDialog clipboard_confirmation_window {
|
|||
default-response: "cancel";
|
||||
close-response: "cancel";
|
||||
|
||||
extra-child: Overlay {
|
||||
extra-child: ListBox {
|
||||
selection-mode: none;
|
||||
|
||||
styles [
|
||||
"osd",
|
||||
"boxed-list-separate",
|
||||
]
|
||||
|
||||
ScrolledWindow text_view_scroll {
|
||||
width-request: 500;
|
||||
height-request: 250;
|
||||
|
||||
TextView text_view {
|
||||
cursor-visible: false;
|
||||
editable: false;
|
||||
monospace: true;
|
||||
top-margin: 8;
|
||||
left-margin: 8;
|
||||
bottom-margin: 8;
|
||||
right-margin: 8;
|
||||
|
||||
styles [
|
||||
"clipboard-content-view",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
Button reveal_button {
|
||||
visible: false;
|
||||
halign: end;
|
||||
valign: start;
|
||||
margin-end: 12;
|
||||
margin-top: 12;
|
||||
|
||||
Image {
|
||||
icon-name: "view-reveal-symbolic";
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
Button hide_button {
|
||||
visible: false;
|
||||
halign: end;
|
||||
valign: start;
|
||||
margin-end: 12;
|
||||
margin-top: 12;
|
||||
|
||||
Overlay {
|
||||
styles [
|
||||
"opaque",
|
||||
"osd",
|
||||
"clipboard-overlay",
|
||||
]
|
||||
|
||||
Image {
|
||||
icon-name: "view-conceal-symbolic";
|
||||
ScrolledWindow text_view_scroll {
|
||||
width-request: 500;
|
||||
height-request: 200;
|
||||
|
||||
TextView text_view {
|
||||
cursor-visible: false;
|
||||
editable: false;
|
||||
monospace: true;
|
||||
top-margin: 8;
|
||||
left-margin: 8;
|
||||
bottom-margin: 8;
|
||||
right-margin: 8;
|
||||
|
||||
styles [
|
||||
"clipboard-content-view",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
Button reveal_button {
|
||||
visible: false;
|
||||
halign: end;
|
||||
valign: start;
|
||||
margin-end: 12;
|
||||
margin-top: 12;
|
||||
|
||||
Image {
|
||||
icon-name: "view-reveal-symbolic";
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
Button hide_button {
|
||||
visible: false;
|
||||
halign: end;
|
||||
valign: start;
|
||||
margin-end: 12;
|
||||
margin-top: 12;
|
||||
|
||||
styles [
|
||||
"opaque",
|
||||
]
|
||||
|
||||
Image {
|
||||
icon-name: "view-conceal-symbolic";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Adw.SwitchRow remember_choice {
|
||||
title: _("Remember choice for this split");
|
||||
subtitle: _("Reload configuration to show this prompt again");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,58 +14,68 @@ Adw.AlertDialog clipboard_confirmation_window {
|
|||
default-response: "cancel";
|
||||
close-response: "cancel";
|
||||
|
||||
extra-child: Overlay {
|
||||
extra-child: ListBox {
|
||||
selection-mode: none;
|
||||
|
||||
styles [
|
||||
"osd",
|
||||
"boxed-list-separate",
|
||||
]
|
||||
|
||||
ScrolledWindow text_view_scroll {
|
||||
width-request: 500;
|
||||
height-request: 250;
|
||||
Overlay {
|
||||
styles [
|
||||
"osd",
|
||||
"clipboard-overlay",
|
||||
]
|
||||
|
||||
TextView text_view {
|
||||
cursor-visible: false;
|
||||
editable: false;
|
||||
monospace: true;
|
||||
top-margin: 8;
|
||||
left-margin: 8;
|
||||
bottom-margin: 8;
|
||||
right-margin: 8;
|
||||
ScrolledWindow text_view_scroll {
|
||||
width-request: 500;
|
||||
height-request: 200;
|
||||
|
||||
TextView text_view {
|
||||
cursor-visible: false;
|
||||
editable: false;
|
||||
monospace: true;
|
||||
top-margin: 8;
|
||||
left-margin: 8;
|
||||
bottom-margin: 8;
|
||||
right-margin: 8;
|
||||
|
||||
styles [
|
||||
"clipboard-content-view",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
Button reveal_button {
|
||||
visible: false;
|
||||
halign: end;
|
||||
valign: start;
|
||||
margin-end: 12;
|
||||
margin-top: 12;
|
||||
|
||||
Image {
|
||||
icon-name: "view-reveal-symbolic";
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
Button hide_button {
|
||||
visible: false;
|
||||
halign: end;
|
||||
valign: start;
|
||||
margin-end: 12;
|
||||
margin-top: 12;
|
||||
|
||||
styles [
|
||||
"clipboard-content-view",
|
||||
"opaque",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
Button reveal_button {
|
||||
visible: false;
|
||||
halign: end;
|
||||
valign: start;
|
||||
margin-end: 12;
|
||||
margin-top: 12;
|
||||
|
||||
Image {
|
||||
icon-name: "view-reveal-symbolic";
|
||||
}
|
||||
}
|
||||
|
||||
[overlay]
|
||||
Button hide_button {
|
||||
visible: false;
|
||||
halign: end;
|
||||
valign: start;
|
||||
margin-end: 12;
|
||||
margin-top: 12;
|
||||
|
||||
styles [
|
||||
"opaque",
|
||||
]
|
||||
|
||||
Image {
|
||||
icon-name: "view-conceal-symbolic";
|
||||
}
|
||||
Adw.SwitchRow remember_choice {
|
||||
title: _("Remember choice for this split");
|
||||
subtitle: _("Reload configuration to show this prompt again");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,19 @@ pub const App = struct {
|
|||
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
||||
|
||||
xdg_activation: ?*xdg.ActivationV1 = null,
|
||||
|
||||
/// Whether the xdg_wm_dialog_v1 protocol is present.
|
||||
///
|
||||
/// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user
|
||||
/// creates a quick terminal, and we need to ensure this fails
|
||||
/// gracefully if this situation occurs.
|
||||
///
|
||||
/// FIXME: This is a temporary workaround - we should remove this when
|
||||
/// all of our supported distros drop support for affected old
|
||||
/// gtk4-layer-shell versions.
|
||||
///
|
||||
/// See https://github.com/wmww/gtk4-layer-shell/issues/50
|
||||
xdg_wm_dialog_present: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
|
|
@ -95,11 +108,21 @@ pub const App = struct {
|
|||
return null;
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(_: App) bool {
|
||||
pub fn supportsQuickTerminal(self: App) bool {
|
||||
if (!layer_shell.isSupported()) {
|
||||
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self.context.xdg_wm_dialog_present and layer_shell.getLibraryVersion().order(.{
|
||||
.major = 1,
|
||||
.minor = 0,
|
||||
.patch = 4,
|
||||
}) == .lt) {
|
||||
log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -111,26 +134,38 @@ pub const App = struct {
|
|||
layer_shell.setNamespace(window, "ghostty-quick-terminal");
|
||||
}
|
||||
|
||||
fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
|
||||
// Globals should be optional pointers
|
||||
const T = switch (@typeInfo(field.type)) {
|
||||
.optional => |o| switch (@typeInfo(o.child)) {
|
||||
.pointer => |v| v.child,
|
||||
else => return null,
|
||||
},
|
||||
else => return null,
|
||||
};
|
||||
|
||||
// Only process Wayland interfaces
|
||||
if (!@hasDecl(T, "interface")) return null;
|
||||
return T;
|
||||
}
|
||||
|
||||
fn registryListener(
|
||||
registry: *wl.Registry,
|
||||
event: wl.Registry.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
inline for (@typeInfo(Context).@"struct".fields) |field| {
|
||||
// Globals should be optional pointers
|
||||
const T = switch (@typeInfo(field.type)) {
|
||||
.optional => |o| switch (@typeInfo(o.child)) {
|
||||
.pointer => |v| v.child,
|
||||
else => continue,
|
||||
},
|
||||
else => continue,
|
||||
};
|
||||
const ctx_fields = @typeInfo(Context).@"struct".fields;
|
||||
|
||||
// Only process Wayland interfaces
|
||||
if (!@hasDecl(T, "interface")) continue;
|
||||
switch (event) {
|
||||
.global => |v| global: {
|
||||
// We don't actually do anything with this other than checking
|
||||
// for its existence, so we process this separately.
|
||||
if (std.mem.orderZ(u8, v.interface, "xdg_wm_dialog_v1") == .eq)
|
||||
context.xdg_wm_dialog_present = true;
|
||||
|
||||
inline for (ctx_fields) |field| {
|
||||
const T = getInterfaceType(field) orelse continue;
|
||||
|
||||
switch (event) {
|
||||
.global => |v| global: {
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
v.interface,
|
||||
|
|
@ -148,19 +183,22 @@ pub const App = struct {
|
|||
);
|
||||
return;
|
||||
};
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
// This should be a rare occurrence, but in case a global
|
||||
// is suddenly no longer available, we destroy and unset it
|
||||
// as the protocol mandates.
|
||||
.global_remove => |v| remove: {
|
||||
// This should be a rare occurrence, but in case a global
|
||||
// is suddenly no longer available, we destroy and unset it
|
||||
// as the protocol mandates.
|
||||
.global_remove => |v| remove: {
|
||||
inline for (ctx_fields) |field| {
|
||||
if (getInterfaceType(field) == null) continue;
|
||||
const global = @field(context, field.name) orelse break :remove;
|
||||
if (global.getId() == v.name) {
|
||||
global.destroy();
|
||||
@field(context, field.name) = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,8 +43,9 @@ pub const Message = union(enum) {
|
|||
close: void,
|
||||
|
||||
/// The child process running in the surface has exited. This may trigger
|
||||
/// a surface close, it may not.
|
||||
child_exited: void,
|
||||
/// a surface close, it may not. Additional details about the child
|
||||
/// command are given in the `ChildExited` struct.
|
||||
child_exited: ChildExited,
|
||||
|
||||
/// Show a desktop notification.
|
||||
desktop_notification: struct {
|
||||
|
|
@ -78,6 +79,13 @@ pub const Message = union(enum) {
|
|||
color: terminal.color.RGB,
|
||||
},
|
||||
|
||||
/// Notifies the surface that a tick of the timer that is timing
|
||||
/// out selection scrolling has occurred. "selection scrolling"
|
||||
/// is when the user has clicked and dragged the mouse outside
|
||||
/// the viewport of the terminal and the terminal is scrolling
|
||||
/// the viewport to follow the mouse cursor.
|
||||
selection_scroll_tick: bool,
|
||||
|
||||
/// The terminal has reported a change in the working directory.
|
||||
pwd_change: WriteReq,
|
||||
|
||||
|
|
@ -89,6 +97,11 @@ pub const Message = union(enum) {
|
|||
|
||||
// This enum is a placeholder for future title styles.
|
||||
};
|
||||
|
||||
pub const ChildExited = struct {
|
||||
exit_code: u32,
|
||||
runtime_ms: u64,
|
||||
};
|
||||
};
|
||||
|
||||
/// A surface mailbox.
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ pub fn init(b: *std.Build) !Config {
|
|||
// This is set to true when we're building a system package. For now
|
||||
// this is trivially detected using the "system_package_mode" bool
|
||||
// but we may want to make this more sophisticated in the future.
|
||||
const system_package: bool = b.graph.system_package_mode;
|
||||
const system_package = b.graph.system_package_mode;
|
||||
|
||||
// This specifies our target wasm runtime. For now only one semi-usable
|
||||
// one exists so this is hardcoded.
|
||||
|
|
@ -361,7 +361,6 @@ pub fn init(b: *std.Build) !Config {
|
|||
"libpng",
|
||||
"zlib",
|
||||
"oniguruma",
|
||||
"gtk4-layer-shell",
|
||||
}) |dep| {
|
||||
_ = b.systemIntegrationOption(
|
||||
dep,
|
||||
|
|
@ -387,6 +386,15 @@ pub fn init(b: *std.Build) !Config {
|
|||
}) |dep| {
|
||||
_ = b.systemIntegrationOption(dep, .{ .default = false });
|
||||
}
|
||||
|
||||
// These are dynamic libraries we default to true, preferring
|
||||
// to use system packages over building and installing libs
|
||||
// as they require additional ldconfig of library paths or
|
||||
// patching the rpath of the program to discover the dynamic library
|
||||
// at runtime
|
||||
for (&[_][]const u8{"gtk4-layer-shell"}) |dep| {
|
||||
_ = b.systemIntegrationOption(dep, .{ .default = true });
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const GhosttyResources = @This();
|
|||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const buildpkg = @import("main.zig");
|
||||
const Config = @import("Config.zig");
|
||||
const config_vim = @import("../config/vim.zig");
|
||||
|
|
@ -220,83 +221,178 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources {
|
|||
}
|
||||
|
||||
// App (Linux)
|
||||
if (cfg.target.result.os.tag == .linux) {
|
||||
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
|
||||
if (cfg.target.result.os.tag == .linux) try addLinuxAppResources(
|
||||
b,
|
||||
cfg,
|
||||
&steps,
|
||||
);
|
||||
|
||||
return .{ .steps = steps.items };
|
||||
}
|
||||
|
||||
/// Add the resource files needed to make Ghostty a proper
|
||||
/// Linux desktop application (for various desktop environments).
|
||||
fn addLinuxAppResources(
|
||||
b: *std.Build,
|
||||
cfg: *const Config,
|
||||
steps: *std.ArrayList(*std.Build.Step),
|
||||
) !void {
|
||||
assert(cfg.target.result.os.tag == .linux);
|
||||
|
||||
// Background:
|
||||
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
|
||||
|
||||
const name = b.fmt("Ghostty{s}", .{
|
||||
switch (cfg.optimize) {
|
||||
.Debug, .ReleaseSafe => " (Debug)",
|
||||
.ReleaseFast, .ReleaseSmall => "",
|
||||
},
|
||||
});
|
||||
|
||||
const app_id = b.fmt("com.mitchellh.ghostty{s}", .{
|
||||
switch (cfg.optimize) {
|
||||
.Debug, .ReleaseSafe => "-debug",
|
||||
.ReleaseFast, .ReleaseSmall => "",
|
||||
},
|
||||
});
|
||||
|
||||
const exe_abs_path = b.fmt(
|
||||
"{s}/bin/ghostty",
|
||||
.{b.install_prefix},
|
||||
);
|
||||
|
||||
// The templates that we will process. The templates are in
|
||||
// cmake format and will be processed and saved to the
|
||||
// second element of the tuple.
|
||||
const Template = struct { std.Build.LazyPath, []const u8 };
|
||||
const templates: []const Template = templates: {
|
||||
var ts: std.ArrayList(Template) = .init(b.allocator);
|
||||
|
||||
// Desktop file so that we have an icon and other metadata
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("dist/linux/app.desktop"),
|
||||
"share/applications/com.mitchellh.ghostty.desktop",
|
||||
).step);
|
||||
try ts.append(.{
|
||||
b.path("dist/linux/app.desktop.in"),
|
||||
b.fmt("share/applications/{s}.desktop", .{app_id}),
|
||||
});
|
||||
|
||||
// AppStream metainfo so that application has rich metadata within app stores
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml"),
|
||||
"share/metainfo/com.mitchellh.ghostty.metainfo.xml",
|
||||
).step);
|
||||
// Service for DBus activation.
|
||||
try ts.append(.{
|
||||
if (cfg.flatpak)
|
||||
b.path("dist/linux/dbus.service.flatpak.in")
|
||||
else
|
||||
b.path("dist/linux/dbus.service.in"),
|
||||
b.fmt("share/dbus-1/services/{s}.service", .{app_id}),
|
||||
});
|
||||
|
||||
// Right click menu action for Plasma desktop
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("dist/linux/ghostty_dolphin.desktop"),
|
||||
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
|
||||
).step);
|
||||
// systemd user service. This is kind of nasty but systemd
|
||||
// looks for user services in different paths depending on
|
||||
// if we are installed as a system package or not (lib vs.
|
||||
// share) so we have to handle that here. We might be able
|
||||
// to get away with always installing to both because it
|
||||
// only ever searches in one... but I don't want to do that hack
|
||||
// until we have to.
|
||||
if (!cfg.flatpak) try ts.append(.{
|
||||
b.path("dist/linux/systemd.service.in"),
|
||||
b.fmt(
|
||||
"{s}/systemd/user/{s}.service",
|
||||
.{
|
||||
if (b.graph.system_package_mode) "lib" else "share",
|
||||
app_id,
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
// Right click menu action for Nautilus. Note that this _must_ be named
|
||||
// `ghostty.py`. Using the full app id causes problems (see #5468).
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("dist/linux/ghostty_nautilus.py"),
|
||||
"share/nautilus-python/extensions/ghostty.py",
|
||||
).step);
|
||||
// AppStream metainfo so that application has rich metadata
|
||||
// within app stores
|
||||
try ts.append(.{
|
||||
b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml.in"),
|
||||
b.fmt("share/metainfo/{s}.metainfo.xml", .{app_id}),
|
||||
});
|
||||
|
||||
// Various icons that our application can use, including the icon
|
||||
// that will be used for the desktop.
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_16.png"),
|
||||
"share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_32.png"),
|
||||
"share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_128.png"),
|
||||
"share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_256.png"),
|
||||
"share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_512.png"),
|
||||
"share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
// Flatpaks only support icons up to 512x512.
|
||||
if (!cfg.flatpak) {
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_1024.png"),
|
||||
"share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
}
|
||||
break :templates ts.items;
|
||||
};
|
||||
|
||||
// Process all our templates
|
||||
for (templates) |template| {
|
||||
const tpl = b.addConfigHeader(.{
|
||||
.style = .{ .cmake = template[0] },
|
||||
}, .{
|
||||
.NAME = name,
|
||||
.APPID = app_id,
|
||||
.GHOSTTY = exe_abs_path,
|
||||
});
|
||||
|
||||
// Template output has a single header line we want to remove.
|
||||
// We use `tail` to do it since its part of the POSIX standard.
|
||||
const tail = b.addSystemCommand(&.{ "tail", "-n", "+2" });
|
||||
tail.setStdIn(.{ .lazy_path = tpl.getOutput() });
|
||||
|
||||
const copy = b.addInstallFile(
|
||||
tail.captureStdOut(),
|
||||
template[1],
|
||||
);
|
||||
|
||||
try steps.append(©.step);
|
||||
}
|
||||
|
||||
// Right click menu action for Plasma desktop
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("dist/linux/ghostty_dolphin.desktop"),
|
||||
"share/kio/servicemenus/com.mitchellh.ghostty.desktop",
|
||||
).step);
|
||||
|
||||
// Right click menu action for Nautilus. Note that this _must_ be named
|
||||
// `ghostty.py`. Using the full app id causes problems (see #5468).
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("dist/linux/ghostty_nautilus.py"),
|
||||
"share/nautilus-python/extensions/ghostty.py",
|
||||
).step);
|
||||
|
||||
// Various icons that our application can use, including the icon
|
||||
// that will be used for the desktop.
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_16.png"),
|
||||
"share/icons/hicolor/16x16/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_32.png"),
|
||||
"share/icons/hicolor/32x32/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_128.png"),
|
||||
"share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_256.png"),
|
||||
"share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_512.png"),
|
||||
"share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
// Flatpaks only support icons up to 512x512.
|
||||
if (!cfg.flatpak) {
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_16@2x.png"),
|
||||
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_32@2x.png"),
|
||||
"share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_128@2x.png"),
|
||||
"share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_256@2x.png"),
|
||||
"share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png",
|
||||
b.path("images/icons/icon_1024.png"),
|
||||
"share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
}
|
||||
|
||||
return .{ .steps = steps.items };
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_16@2x.png"),
|
||||
"share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_32@2x.png"),
|
||||
"share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_128@2x.png"),
|
||||
"share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
try steps.append(&b.addInstallFile(
|
||||
b.path("images/icons/icon_256@2x.png"),
|
||||
"share/icons/hicolor/256x256@2/apps/com.mitchellh.ghostty.png",
|
||||
).step);
|
||||
}
|
||||
|
||||
pub fn install(self: *const GhosttyResources) void {
|
||||
|
|
|
|||
|
|
@ -405,12 +405,11 @@ pub fn add(
|
|||
})) |dep| {
|
||||
step.root_module.addImport("xev", dep.module("xev"));
|
||||
}
|
||||
if (b.lazyDependency("z2d", .{})) |dep| {
|
||||
step.root_module.addImport("z2d", b.addModule("z2d", .{
|
||||
.root_source_file = dep.path("src/z2d.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}));
|
||||
if (b.lazyDependency("z2d", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
})) |dep| {
|
||||
step.root_module.addImport("z2d", dep.module("z2d"));
|
||||
}
|
||||
if (b.lazyDependency("ziglyph", .{
|
||||
.target = target,
|
||||
|
|
@ -652,14 +651,13 @@ fn addGTK(
|
|||
// IMPORTANT: gtk4-layer-shell must be linked BEFORE
|
||||
// wayland-client, as it relies on shimming libwayland's APIs.
|
||||
if (b.systemIntegrationOption("gtk4-layer-shell", .{})) {
|
||||
step.linkSystemLibrary2(
|
||||
"gtk4-layer-shell-0",
|
||||
dynamic_link_opts,
|
||||
);
|
||||
step.linkSystemLibrary2("gtk4-layer-shell-0", dynamic_link_opts);
|
||||
} else {
|
||||
// gtk4-layer-shell *must* be dynamically linked,
|
||||
// so we don't add it as a static library
|
||||
step.linkLibrary(gtk4_layer_shell.artifact("gtk4-layer-shell"));
|
||||
const shared_lib = gtk4_layer_shell.artifact("gtk4-layer-shell");
|
||||
b.installArtifact(shared_lib);
|
||||
step.linkLibrary(shared_lib);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ const diags = @import("cli/diagnostics.zig");
|
|||
|
||||
pub const args = @import("cli/args.zig");
|
||||
pub const Action = @import("cli/action.zig").Action;
|
||||
pub const CompatibilityHandler = args.CompatibilityHandler;
|
||||
pub const compatibilityRenamed = args.compatibilityRenamed;
|
||||
pub const DiagnosticList = diags.DiagnosticList;
|
||||
pub const Diagnostic = diags.Diagnostic;
|
||||
pub const Location = diags.Location;
|
||||
|
|
|
|||
200
src/cli/args.zig
|
|
@ -40,11 +40,14 @@ pub const Error = error{
|
|||
/// "DiagnosticList" and any diagnostic messages will be added to that list.
|
||||
/// When diagnostics are present, only allocation errors will be returned.
|
||||
///
|
||||
/// If the destination type has a decl "renamed", it must be of type
|
||||
/// std.StaticStringMap([]const u8) and contains a mapping from the old
|
||||
/// field name to the new field name. This is used to allow renaming fields
|
||||
/// while still supporting the old name. If a renamed field is set, parsing
|
||||
/// will automatically set the new field name.
|
||||
/// If the destination type has a decl "compatibility", it must be of type
|
||||
/// std.StaticStringMap(CompatibilityHandler(T)), and it will be used to
|
||||
/// handle backwards compatibility for fields with the given name. The
|
||||
/// field name doesn't need to exist (so you can setup compatibility for
|
||||
/// removed fields). The value is a function that will be called when
|
||||
/// all other parsing fails for that field. If a field changes such that
|
||||
/// the old values would NOT error, then the caller should handle that
|
||||
/// downstream after parsing is done, not through this method.
|
||||
///
|
||||
/// Note: If the arena is already non-null, then it will be used. In this
|
||||
/// case, in the case of an error some memory might be leaked into the arena.
|
||||
|
|
@ -57,24 +60,6 @@ pub fn parse(
|
|||
const info = @typeInfo(T);
|
||||
assert(info == .@"struct");
|
||||
|
||||
comptime {
|
||||
// Verify all renamed fields are valid (source does not exist,
|
||||
// destination does exist).
|
||||
if (@hasDecl(T, "renamed")) {
|
||||
for (T.renamed.keys(), T.renamed.values()) |key, value| {
|
||||
if (@hasField(T, key)) {
|
||||
@compileLog(key);
|
||||
@compileError("renamed field source exists");
|
||||
}
|
||||
|
||||
if (!@hasField(T, value)) {
|
||||
@compileLog(value);
|
||||
@compileError("renamed field destination does not exist");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make an arena for all our allocations if we support it. Otherwise,
|
||||
// use an allocator that always fails. If the arena is already set on
|
||||
// the config, then we reuse that. See memory note in parse docs.
|
||||
|
|
@ -147,7 +132,23 @@ pub fn parse(
|
|||
break :value null;
|
||||
};
|
||||
|
||||
parseIntoField(T, arena_alloc, dst, key, value) catch |err| {
|
||||
parseIntoField(T, arena_alloc, dst, key, value) catch |err| err: {
|
||||
// If we get an error parsing a field, then we try to fall
|
||||
// back to compatibility handlers if able.
|
||||
if (@hasDecl(T, "compatibility")) {
|
||||
// If we have a compatibility handler for this key, then
|
||||
// we call it and see if it handles the error.
|
||||
if (T.compatibility.get(key)) |handler| {
|
||||
if (handler(dst, arena_alloc, key, value)) {
|
||||
log.info(
|
||||
"compatibility handler for {s} handled error, you may be using a deprecated field: {}",
|
||||
.{ key, err },
|
||||
);
|
||||
break :err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime !canTrackDiags(T)) return err;
|
||||
|
||||
// The error set is dependent on comptime T, so we always add
|
||||
|
|
@ -177,6 +178,58 @@ pub fn parse(
|
|||
}
|
||||
}
|
||||
|
||||
/// The function type for a compatibility handler. The compatibility
|
||||
/// handler is documented in the `parse` function documentation.
|
||||
///
|
||||
/// The function type should return bool if the compatibility was
|
||||
/// handled, and false otherwise. If false is returned then the
|
||||
/// naturally occurring error will continue to be processed as if
|
||||
/// this compatibility handler was not present.
|
||||
///
|
||||
/// Compatibility handlers aren't allowed to return errors because
|
||||
/// they're generally only called in error cases, so we already have
|
||||
/// an error message to show users. If there is an error in handling
|
||||
/// the compatibility, then the handler should return false.
|
||||
pub fn CompatibilityHandler(comptime T: type) type {
|
||||
return *const fn (
|
||||
dst: *T,
|
||||
alloc: Allocator,
|
||||
key: []const u8,
|
||||
value: ?[]const u8,
|
||||
) bool;
|
||||
}
|
||||
|
||||
/// Convenience function to create a compatibility handler that
|
||||
/// renames a field from `from` to `to`.
|
||||
pub fn compatibilityRenamed(
|
||||
comptime T: type,
|
||||
comptime to: []const u8,
|
||||
) CompatibilityHandler(T) {
|
||||
comptime assert(@hasField(T, to));
|
||||
|
||||
return (struct {
|
||||
fn compat(
|
||||
dst: *T,
|
||||
alloc: Allocator,
|
||||
key: []const u8,
|
||||
value: ?[]const u8,
|
||||
) bool {
|
||||
_ = key;
|
||||
|
||||
parseIntoField(T, alloc, dst, to, value) catch |err| {
|
||||
log.warn("error parsing renamed field {s}: {}", .{
|
||||
to,
|
||||
err,
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}).compat;
|
||||
}
|
||||
|
||||
fn formatValueRequired(
|
||||
comptime T: type,
|
||||
arena_alloc: std.mem.Allocator,
|
||||
|
|
@ -401,16 +454,6 @@ pub fn parseIntoField(
|
|||
}
|
||||
}
|
||||
|
||||
// Unknown field, is the field renamed?
|
||||
if (@hasDecl(T, "renamed")) {
|
||||
for (T.renamed.keys(), T.renamed.values()) |old, new| {
|
||||
if (mem.eql(u8, old, key)) {
|
||||
try parseIntoField(T, alloc, dst, new, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return error.InvalidField;
|
||||
}
|
||||
|
||||
|
|
@ -752,6 +795,77 @@ test "parse: diagnostic location" {
|
|||
}
|
||||
}
|
||||
|
||||
test "parse: compatibility handler" {
|
||||
const testing = std.testing;
|
||||
|
||||
var data: struct {
|
||||
a: bool = false,
|
||||
_arena: ?ArenaAllocator = null,
|
||||
|
||||
pub const compatibility: std.StaticStringMap(
|
||||
CompatibilityHandler(@This()),
|
||||
) = .initComptime(&.{
|
||||
.{ "a", compat },
|
||||
});
|
||||
|
||||
fn compat(
|
||||
self: *@This(),
|
||||
alloc: Allocator,
|
||||
key: []const u8,
|
||||
value: ?[]const u8,
|
||||
) bool {
|
||||
_ = alloc;
|
||||
if (std.mem.eql(u8, key, "a")) {
|
||||
if (value) |v| {
|
||||
if (mem.eql(u8, v, "yuh")) {
|
||||
self.a = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
} = .{};
|
||||
defer if (data._arena) |arena| arena.deinit();
|
||||
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
testing.allocator,
|
||||
"--a=yuh",
|
||||
);
|
||||
defer iter.deinit();
|
||||
try parse(@TypeOf(data), testing.allocator, &data, &iter);
|
||||
try testing.expect(data._arena != null);
|
||||
try testing.expect(data.a);
|
||||
}
|
||||
|
||||
test "parse: compatibility renamed" {
|
||||
const testing = std.testing;
|
||||
|
||||
var data: struct {
|
||||
a: bool = false,
|
||||
b: bool = false,
|
||||
_arena: ?ArenaAllocator = null,
|
||||
|
||||
pub const compatibility: std.StaticStringMap(
|
||||
CompatibilityHandler(@This()),
|
||||
) = .initComptime(&.{
|
||||
.{ "old", compatibilityRenamed(@This(), "a") },
|
||||
});
|
||||
} = .{};
|
||||
defer if (data._arena) |arena| arena.deinit();
|
||||
|
||||
var iter = try std.process.ArgIteratorGeneral(.{}).init(
|
||||
testing.allocator,
|
||||
"--old=true --b=true",
|
||||
);
|
||||
defer iter.deinit();
|
||||
try parse(@TypeOf(data), testing.allocator, &data, &iter);
|
||||
try testing.expect(data._arena != null);
|
||||
try testing.expect(data.a);
|
||||
try testing.expect(data.b);
|
||||
}
|
||||
|
||||
test "parseIntoField: ignore underscore-prefixed fields" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
|
|
@ -1176,24 +1290,6 @@ test "parseIntoField: tagged union missing tag" {
|
|||
);
|
||||
}
|
||||
|
||||
test "parseIntoField: renamed field" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var data: struct {
|
||||
a: []const u8,
|
||||
|
||||
const renamed = std.StaticStringMap([]const u8).initComptime(&.{
|
||||
.{ "old", "a" },
|
||||
});
|
||||
} = undefined;
|
||||
|
||||
try parseIntoField(@TypeOf(data), alloc, &data, "old", "42");
|
||||
try testing.expectEqualStrings("42", data.a);
|
||||
}
|
||||
|
||||
/// An iterator that considers its location to be CLI args. It
|
||||
/// iterates through an underlying iterator and increments a counter
|
||||
/// to track the current CLI arg index.
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ const Boo = struct {
|
|||
pub fn run(gpa: Allocator) !u8 {
|
||||
// Disable on non-desktop systems.
|
||||
switch (builtin.os.tag) {
|
||||
.windows, .macos, .linux => {},
|
||||
.windows, .macos, .linux, .freebsd => {},
|
||||
else => return 1,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
|
|||
pub const CopyOnSelect = Config.CopyOnSelect;
|
||||
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
|
||||
pub const FontSyntheticStyle = Config.FontSyntheticStyle;
|
||||
pub const FontShapingBreak = Config.FontShapingBreak;
|
||||
pub const FontStyle = Config.FontStyle;
|
||||
pub const FreetypeLoadFlags = Config.FreetypeLoadFlags;
|
||||
pub const Keybinds = Config.Keybinds;
|
||||
|
|
|
|||
|
|
@ -46,14 +46,29 @@ const c = @cImport({
|
|||
@cInclude("unistd.h");
|
||||
});
|
||||
|
||||
/// Renamed fields, used by cli.parse
|
||||
pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
|
||||
pub const compatibility = std.StaticStringMap(
|
||||
cli.CompatibilityHandler(Config),
|
||||
).initComptime(&.{
|
||||
// Ghostty 1.1 introduced background-blur support for Linux which
|
||||
// doesn't support a specific radius value. The renaming is to let
|
||||
// one field be used for both platforms (macOS retained the ability
|
||||
// to set a radius).
|
||||
.{ "background-blur-radius", "background-blur" },
|
||||
.{ "adw-toolbar-style", "gtk-toolbar-style" },
|
||||
.{ "background-blur-radius", cli.compatibilityRenamed(Config, "background-blur") },
|
||||
|
||||
// Ghostty 1.2 renamed all our adw options to gtk because we now have
|
||||
// a hard dependency on libadwaita.
|
||||
.{ "adw-toolbar-style", cli.compatibilityRenamed(Config, "gtk-toolbar-style") },
|
||||
|
||||
// Ghostty 1.2 removed the `hidden` value from `gtk-tabs-location` and
|
||||
// moved it to `window-show-tab-bar`.
|
||||
.{ "gtk-tabs-location", compatGtkTabsLocation },
|
||||
|
||||
// Ghostty 1.2 lets you set `cell-foreground` and `cell-background`
|
||||
// to match the cell foreground and background colors, respectively.
|
||||
// This can be used with `cursor-color` and `cursor-text` to recreate
|
||||
// this behavior. This applies to selection too.
|
||||
.{ "cursor-invert-fg-bg", compatCursorInvertFgBg },
|
||||
.{ "selection-invert-fg-bg", compatSelectionInvertFgBg },
|
||||
});
|
||||
|
||||
/// The font families to use.
|
||||
|
|
@ -262,6 +277,32 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{
|
|||
/// This is currently only supported on macOS.
|
||||
@"font-thicken-strength": u8 = 255,
|
||||
|
||||
/// Locations to break font shaping into multiple runs.
|
||||
///
|
||||
/// A "run" is a contiguous segment of text that is shaped together. "Shaping"
|
||||
/// is the process of converting text (codepoints) into glyphs (renderable
|
||||
/// characters). This is how ligatures are formed, among other things.
|
||||
/// For example, if a coding font turns "!=" into a single glyph, then it
|
||||
/// must see "!" and "=" next to each other in a single run. When a run
|
||||
/// is broken, the text is shaped separately. To continue our example, if
|
||||
/// "!" is at the end of one run and "=" is at the start of the next run,
|
||||
/// then the ligature will not be formed.
|
||||
///
|
||||
/// Ghostty breaks runs at certain points to improve readability or usability.
|
||||
/// For example, Ghostty by default will break runs under the cursor so that
|
||||
/// text editing can see the individual characters rather than a ligature.
|
||||
/// This configuration lets you configure this behavior.
|
||||
///
|
||||
/// Combine values with a comma to set multiple options. Prefix an
|
||||
/// option with "no-" to disable it. Enabling and disabling options
|
||||
/// can be done at the same time.
|
||||
///
|
||||
/// Available options:
|
||||
///
|
||||
/// * `cursor` - Break runs under the cursor.
|
||||
///
|
||||
@"font-shaping-break": FontShapingBreak = .{},
|
||||
|
||||
/// What color space to use when performing alpha blending.
|
||||
///
|
||||
/// This affects the appearance of text and of any images with transparency.
|
||||
|
|
@ -557,16 +598,11 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
|
|||
/// the selection color is just the inverted window background and foreground
|
||||
/// (note: not to be confused with the cell bg/fg).
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
@"selection-foreground": ?Color = null,
|
||||
@"selection-background": ?Color = null,
|
||||
|
||||
/// Swap the foreground and background colors of cells for selection. This
|
||||
/// option overrides the `selection-foreground` and `selection-background`
|
||||
/// options.
|
||||
///
|
||||
/// If you select across cells with differing foregrounds and backgrounds, the
|
||||
/// selection color will vary across the selection.
|
||||
@"selection-invert-fg-bg": bool = false,
|
||||
/// Since version 1.2.0, this can also be set to `cell-foreground` to match
|
||||
/// the cell foreground color, or `cell-background` to match the cell
|
||||
/// background color.
|
||||
@"selection-foreground": ?TerminalColor = null,
|
||||
@"selection-background": ?TerminalColor = null,
|
||||
|
||||
/// Whether to clear selected text when typing. This defaults to `true`.
|
||||
/// This is typical behavior for most terminal emulators as well as
|
||||
|
|
@ -610,12 +646,20 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
|
|||
palette: Palette = .{},
|
||||
|
||||
/// The color of the cursor. If this is not set, a default will be chosen.
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
@"cursor-color": ?Color = null,
|
||||
|
||||
/// Swap the foreground and background colors of the cell under the cursor. This
|
||||
/// option overrides the `cursor-color` and `cursor-text` options.
|
||||
@"cursor-invert-fg-bg": bool = false,
|
||||
///
|
||||
/// Direct colors can be specified as either hex (`#RRGGBB` or `RRGGBB`)
|
||||
/// or a named X11 color.
|
||||
///
|
||||
/// Additionally, special values can be used to set the color to match
|
||||
/// other colors at runtime:
|
||||
///
|
||||
/// * `cell-foreground` - Match the cell foreground color.
|
||||
/// (Available since version 1.2.0)
|
||||
///
|
||||
/// * `cell-background` - Match the cell background color.
|
||||
/// (Available since version 1.2.0)
|
||||
///
|
||||
@"cursor-color": ?TerminalColor = null,
|
||||
|
||||
/// The opacity level (opposite of transparency) of the cursor. A value of 1
|
||||
/// is fully opaque and a value of 0 is fully transparent. A value less than 0
|
||||
|
|
@ -665,7 +709,10 @@ palette: Palette = .{},
|
|||
/// The color of the text under the cursor. If this is not set, a default will
|
||||
/// be chosen.
|
||||
/// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color.
|
||||
@"cursor-text": ?Color = null,
|
||||
/// Since version 1.2.0, this can also be set to `cell-foreground` to match
|
||||
/// the cell foreground color, or `cell-background` to match the cell
|
||||
/// background color.
|
||||
@"cursor-text": ?TerminalColor = null,
|
||||
|
||||
/// Enables the ability to move the cursor at prompts by using `alt+click` on
|
||||
/// Linux and `option+click` on macOS.
|
||||
|
|
@ -1029,12 +1076,17 @@ title: ?[:0]const u8 = null,
|
|||
/// The setting that will change the application class value.
|
||||
///
|
||||
/// This controls the class field of the `WM_CLASS` X11 property (when running
|
||||
/// under X11), and the Wayland application ID (when running under Wayland).
|
||||
/// under X11), the Wayland application ID (when running under Wayland), and the
|
||||
/// bus name that Ghostty uses to connect to DBus.
|
||||
///
|
||||
/// Note that changing this value between invocations will create new, separate
|
||||
/// instances, of Ghostty when running with `gtk-single-instance=true`. See that
|
||||
/// option for more details.
|
||||
///
|
||||
/// Changing this value may break launching Ghostty from `.desktop` files, via
|
||||
/// DBus activation, or systemd user services as the system is expecting Ghostty
|
||||
/// to connect to DBus using the default `class` when it is launched.
|
||||
///
|
||||
/// The class name must follow the requirements defined [in the GTK
|
||||
/// documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html).
|
||||
///
|
||||
|
|
@ -1581,6 +1633,27 @@ keybind: Keybinds = .{},
|
|||
/// * `end` - Insert the new tab at the end of the tab list.
|
||||
@"window-new-tab-position": WindowNewTabPosition = .current,
|
||||
|
||||
/// Whether to show the tab bar.
|
||||
///
|
||||
/// Valid values:
|
||||
///
|
||||
/// - `always`
|
||||
///
|
||||
/// Always display the tab bar, even when there's only one tab.
|
||||
///
|
||||
/// - `auto` *(default)*
|
||||
///
|
||||
/// Automatically show and hide the tab bar. The tab bar is only
|
||||
/// shown when there are two or more tabs present.
|
||||
///
|
||||
/// - `never`
|
||||
///
|
||||
/// Never show the tab bar. Tabs are only accessible via the tab
|
||||
/// overview or by keybind actions.
|
||||
///
|
||||
/// Currently only supported on Linux (GTK).
|
||||
@"window-show-tab-bar": WindowShowTabBar = .auto,
|
||||
|
||||
/// Background color for the window titlebar. This only takes effect if
|
||||
/// window-theme is set to ghostty. Currently only supported in the GTK app
|
||||
/// runtime.
|
||||
|
|
@ -2720,14 +2793,14 @@ else
|
|||
///
|
||||
/// GTK CSS documentation can be found at the following links:
|
||||
///
|
||||
/// * <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-overview.html - An overview of GTK CSS.
|
||||
/// * https://docs.gtk.org/gtk4/css-properties.html - A comprehensive list
|
||||
/// of supported CSS properties.
|
||||
///
|
||||
/// Launch Ghostty with `env GTK_DEBUG=interactive ghostty` to tweak Ghostty's
|
||||
/// CSS in real time using the GTK Inspector. Errors in your CSS files would
|
||||
/// also be reported in the terminal you started Ghostty from. See
|
||||
/// <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.
|
||||
///
|
||||
/// This configuration can be repeated multiple times to load multiple files.
|
||||
|
|
@ -3785,6 +3858,68 @@ pub fn parseManuallyHook(
|
|||
return true;
|
||||
}
|
||||
|
||||
fn compatGtkTabsLocation(
|
||||
self: *Config,
|
||||
alloc: Allocator,
|
||||
key: []const u8,
|
||||
value: ?[]const u8,
|
||||
) bool {
|
||||
_ = alloc;
|
||||
assert(std.mem.eql(u8, key, "gtk-tabs-location"));
|
||||
|
||||
if (std.mem.eql(u8, value orelse "", "hidden")) {
|
||||
self.@"window-show-tab-bar" = .never;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
fn compatCursorInvertFgBg(
|
||||
self: *Config,
|
||||
alloc: Allocator,
|
||||
key: []const u8,
|
||||
value_: ?[]const u8,
|
||||
) bool {
|
||||
_ = alloc;
|
||||
assert(std.mem.eql(u8, key, "cursor-invert-fg-bg"));
|
||||
|
||||
// We don't do anything if the value is unset, which is technically
|
||||
// not EXACTLY the same as prior behavior since it would fallback
|
||||
// to doing whatever cursor-color/cursor-text were set to, but
|
||||
// I don't want to store what that is separately so this is close
|
||||
// enough.
|
||||
//
|
||||
// Realistically, these fields were mutually exclusive so anyone
|
||||
// relying on that behavior should just upgrade to the new
|
||||
// cursor-color/cursor-text fields.
|
||||
const set = cli.args.parseBool(value_ orelse "t") catch return false;
|
||||
if (set) {
|
||||
self.@"cursor-color" = .@"cell-foreground";
|
||||
self.@"cursor-text" = .@"cell-background";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn compatSelectionInvertFgBg(
|
||||
self: *Config,
|
||||
alloc: Allocator,
|
||||
key: []const u8,
|
||||
value_: ?[]const u8,
|
||||
) bool {
|
||||
_ = alloc;
|
||||
assert(std.mem.eql(u8, key, "selection-invert-fg-bg"));
|
||||
|
||||
const set = cli.args.parseBool(value_ orelse "t") catch return false;
|
||||
if (set) {
|
||||
self.@"selection-foreground" = .@"cell-background";
|
||||
self.@"selection-background" = .@"cell-foreground";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Create a shallow copy of this config. This will share all the memory
|
||||
/// allocated with the previous config but will have a new arena for
|
||||
/// any changes or new allocations. The config should have `deinit`
|
||||
|
|
@ -4347,6 +4482,65 @@ pub const Color = struct {
|
|||
}
|
||||
};
|
||||
|
||||
/// Represents color values that can also reference special color
|
||||
/// values such as "cell-foreground" or "cell-background".
|
||||
pub const TerminalColor = union(enum) {
|
||||
color: Color,
|
||||
@"cell-foreground",
|
||||
@"cell-background",
|
||||
|
||||
pub fn parseCLI(input_: ?[]const u8) !TerminalColor {
|
||||
const input = input_ orelse return error.ValueRequired;
|
||||
if (std.mem.eql(u8, input, "cell-foreground")) return .@"cell-foreground";
|
||||
if (std.mem.eql(u8, input, "cell-background")) return .@"cell-background";
|
||||
return .{ .color = try Color.parseCLI(input) };
|
||||
}
|
||||
|
||||
/// Used by Formatter
|
||||
pub fn formatEntry(self: TerminalColor, formatter: anytype) !void {
|
||||
switch (self) {
|
||||
.color => try self.color.formatEntry(formatter),
|
||||
|
||||
.@"cell-foreground",
|
||||
.@"cell-background",
|
||||
=> try formatter.formatEntry([:0]const u8, @tagName(self)),
|
||||
}
|
||||
}
|
||||
|
||||
test "parseCLI" {
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expectEqual(
|
||||
TerminalColor{ .color = Color{ .r = 78, .g = 42, .b = 132 } },
|
||||
try TerminalColor.parseCLI("#4e2a84"),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
TerminalColor{ .color = Color{ .r = 0, .g = 0, .b = 0 } },
|
||||
try TerminalColor.parseCLI("black"),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
TerminalColor.@"cell-foreground",
|
||||
try TerminalColor.parseCLI("cell-foreground"),
|
||||
);
|
||||
try testing.expectEqual(
|
||||
TerminalColor.@"cell-background",
|
||||
try TerminalColor.parseCLI("cell-background"),
|
||||
);
|
||||
|
||||
try testing.expectError(error.InvalidValue, TerminalColor.parseCLI("a"));
|
||||
}
|
||||
|
||||
test "formatConfig" {
|
||||
const testing = std.testing;
|
||||
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
var sc: TerminalColor = .@"cell-foreground";
|
||||
try sc.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
try testing.expectEqualSlices(u8, "a = cell-foreground\n", buf.items);
|
||||
}
|
||||
};
|
||||
|
||||
pub const ColorList = struct {
|
||||
const Self = @This();
|
||||
|
||||
|
|
@ -4996,6 +5190,12 @@ pub const Keybinds = struct {
|
|||
.{ .reset_font_size = {} },
|
||||
);
|
||||
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .unicode = 'j' }, .mods = .{ .shift = true, .ctrl = true, .super = true } },
|
||||
.{ .write_screen_file = .copy },
|
||||
);
|
||||
|
||||
try self.set.put(
|
||||
alloc,
|
||||
.{ .key = .{ .unicode = 'j' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
|
||||
|
|
@ -5274,7 +5474,14 @@ pub const Keybinds = struct {
|
|||
.mods = mods,
|
||||
},
|
||||
.{ .goto_tab = (i - start) + 1 },
|
||||
.{ .performable = true },
|
||||
.{
|
||||
// On macOS we keep this not performable so that the
|
||||
// keyboard shortcuts in tabs work. In the future the
|
||||
// correct fix is to fix the reverse mapping lookup
|
||||
// to allow us to lookup performable keybinds
|
||||
// conditionally.
|
||||
.performable = !builtin.target.os.tag.isDarwin(),
|
||||
},
|
||||
);
|
||||
}
|
||||
try self.set.putFlags(
|
||||
|
|
@ -5284,7 +5491,10 @@ pub const Keybinds = struct {
|
|||
.mods = mods,
|
||||
},
|
||||
.{ .last_tab = {} },
|
||||
.{ .performable = true },
|
||||
.{
|
||||
// See comment above with the numeric goto_tab
|
||||
.performable = !builtin.target.os.tag.isDarwin(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -6172,6 +6382,11 @@ pub const FontSyntheticStyle = packed struct {
|
|||
@"bold-italic": bool = true,
|
||||
};
|
||||
|
||||
/// See "font-shaping-break" for documentation
|
||||
pub const FontShapingBreak = packed struct {
|
||||
cursor: bool = true,
|
||||
};
|
||||
|
||||
/// See "link" for documentation.
|
||||
pub const RepeatableLink = struct {
|
||||
const Self = @This();
|
||||
|
|
@ -6492,7 +6707,6 @@ pub const GtkSingleInstance = enum {
|
|||
pub const GtkTabsLocation = enum {
|
||||
top,
|
||||
bottom,
|
||||
hidden,
|
||||
};
|
||||
|
||||
/// See gtk-toolbar-style
|
||||
|
|
@ -6543,6 +6757,13 @@ pub const WindowNewTabPosition = enum {
|
|||
end,
|
||||
};
|
||||
|
||||
/// See window-show-tab-bar
|
||||
pub const WindowShowTabBar = enum {
|
||||
always,
|
||||
auto,
|
||||
never,
|
||||
};
|
||||
|
||||
/// See resize-overlay
|
||||
pub const ResizeOverlay = enum {
|
||||
always,
|
||||
|
|
@ -7965,3 +8186,51 @@ test "theme specifying light/dark sets theme usage in conditional state" {
|
|||
try testing.expect(cfg._conditional_set.contains(.theme));
|
||||
}
|
||||
}
|
||||
|
||||
test "compatibility: removed cursor-invert-fg-bg" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
{
|
||||
var cfg = try Config.default(alloc);
|
||||
defer cfg.deinit();
|
||||
var it: TestIterator = .{ .data = &.{
|
||||
"--cursor-invert-fg-bg",
|
||||
} };
|
||||
try cfg.loadIter(alloc, &it);
|
||||
try cfg.finalize();
|
||||
|
||||
try testing.expectEqual(
|
||||
TerminalColor.@"cell-foreground",
|
||||
cfg.@"cursor-color",
|
||||
);
|
||||
try testing.expectEqual(
|
||||
TerminalColor.@"cell-background",
|
||||
cfg.@"cursor-text",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
test "compatibility: removed selection-invert-fg-bg" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
{
|
||||
var cfg = try Config.default(alloc);
|
||||
defer cfg.deinit();
|
||||
var it: TestIterator = .{ .data = &.{
|
||||
"--selection-invert-fg-bg",
|
||||
} };
|
||||
try cfg.loadIter(alloc, &it);
|
||||
try cfg.finalize();
|
||||
|
||||
try testing.expectEqual(
|
||||
TerminalColor.@"cell-background",
|
||||
cfg.@"selection-foreground",
|
||||
);
|
||||
try testing.expectEqual(
|
||||
TerminalColor.@"cell-foreground",
|
||||
cfg.@"selection-background",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ pub fn open(alloc_gpa: Allocator) !void {
|
|||
// Use an arena to make memory management easier in here.
|
||||
var arena = ArenaAllocator.init(alloc_gpa);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
const alloc_arena = arena.allocator();
|
||||
|
||||
// Get the path we should open
|
||||
const config_path = try configPath(alloc);
|
||||
const config_path = try configPath(alloc_arena);
|
||||
|
||||
// Create config directory recursively.
|
||||
if (std.fs.path.dirname(config_path)) |config_dir| {
|
||||
|
|
@ -41,7 +41,7 @@ pub fn open(alloc_gpa: Allocator) !void {
|
|||
}
|
||||
};
|
||||
|
||||
try internal_os.open(alloc, .text, config_path);
|
||||
try internal_os.open(alloc_gpa, .text, config_path);
|
||||
}
|
||||
|
||||
/// Returns the config path to use for open for the current OS.
|
||||
|
|
|
|||
|
|
@ -251,6 +251,37 @@ pub fn set(self: *Atlas, reg: Region, data: []const u8) void {
|
|||
_ = self.modified.fetchAdd(1, .monotonic);
|
||||
}
|
||||
|
||||
/// Like `set` but allows specifying a width for the source data and an
|
||||
/// offset x and y, so that a section of a larger buffer may be copied
|
||||
/// in to the atlas.
|
||||
pub fn setFromLarger(
|
||||
self: *Atlas,
|
||||
reg: Region,
|
||||
src: []const u8,
|
||||
src_width: u32,
|
||||
src_x: u32,
|
||||
src_y: u32,
|
||||
) void {
|
||||
assert(reg.x < (self.size - 1));
|
||||
assert((reg.x + reg.width) <= (self.size - 1));
|
||||
assert(reg.y < (self.size - 1));
|
||||
assert((reg.y + reg.height) <= (self.size - 1));
|
||||
|
||||
const depth = self.format.depth();
|
||||
var i: u32 = 0;
|
||||
while (i < reg.height) : (i += 1) {
|
||||
const tex_offset = (((reg.y + i) * self.size) + reg.x) * depth;
|
||||
const src_offset = (((src_y + i) * src_width) + src_x) * depth;
|
||||
fastmem.copy(
|
||||
u8,
|
||||
self.data[tex_offset..],
|
||||
src[src_offset .. src_offset + (reg.width * depth)],
|
||||
);
|
||||
}
|
||||
|
||||
_ = self.modified.fetchAdd(1, .monotonic);
|
||||
}
|
||||
|
||||
// Grow the texture to the new size, preserving all previously written data.
|
||||
pub fn grow(self: *Atlas, alloc: Allocator, size_new: u32) Allocator.Error!void {
|
||||
assert(size_new >= self.size);
|
||||
|
|
@ -556,6 +587,35 @@ test "writing data" {
|
|||
try testing.expectEqual(@as(u8, 4), atlas.data[66]);
|
||||
}
|
||||
|
||||
test "writing data from a larger source" {
|
||||
const alloc = testing.allocator;
|
||||
var atlas = try init(alloc, 32, .grayscale);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
const reg = try atlas.reserve(alloc, 2, 2);
|
||||
const old = atlas.modified.load(.monotonic);
|
||||
// zig fmt: off
|
||||
atlas.setFromLarger(reg, &[_]u8{
|
||||
8, 8, 8, 8, 8,
|
||||
8, 8, 1, 2, 8,
|
||||
8, 8, 3, 4, 8,
|
||||
8, 8, 8, 8, 8,
|
||||
}, 5, 2, 1);
|
||||
// zig fmt: on
|
||||
const new = atlas.modified.load(.monotonic);
|
||||
try testing.expect(new > old);
|
||||
|
||||
// 33 because of the 1px border and so on
|
||||
try testing.expectEqual(@as(u8, 1), atlas.data[33]);
|
||||
try testing.expectEqual(@as(u8, 2), atlas.data[34]);
|
||||
try testing.expectEqual(@as(u8, 3), atlas.data[65]);
|
||||
try testing.expectEqual(@as(u8, 4), atlas.data[66]);
|
||||
|
||||
// None of the `8`s from the source data outside of the
|
||||
// specified region should have made it on to the atlas.
|
||||
try testing.expectEqual(null, std.mem.indexOfScalar(u8, atlas.data, 8));
|
||||
}
|
||||
|
||||
test "grow" {
|
||||
const alloc = testing.allocator;
|
||||
var atlas = try init(alloc, 4, .grayscale); // +2 for 1px border
|
||||
|
|
|
|||
|
|
@ -20,3 +20,6 @@ atlas_y: u32,
|
|||
|
||||
/// horizontal position to increase drawing position for strings
|
||||
advance_x: f32,
|
||||
|
||||
/// Whether we drew this glyph ourselves with the sprite font.
|
||||
sprite: bool = false,
|
||||
|
|
|
|||
|
|
@ -831,6 +831,9 @@ pub const CoreText = struct {
|
|||
i: usize,
|
||||
|
||||
pub fn deinit(self: *DiscoverIterator) void {
|
||||
for (self.list) |desc| {
|
||||
desc.release();
|
||||
}
|
||||
self.alloc.free(self.list);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ const builtin = @import("builtin");
|
|||
const options = @import("main.zig").options;
|
||||
const run = @import("shaper/run.zig");
|
||||
const feature = @import("shaper/feature.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const terminal = @import("../terminal/main.zig");
|
||||
const SharedGrid = @import("main.zig").SharedGrid;
|
||||
pub const noop = @import("shaper/noop.zig");
|
||||
pub const harfbuzz = @import("shaper/harfbuzz.zig");
|
||||
pub const coretext = @import("shaper/coretext.zig");
|
||||
|
|
@ -61,6 +64,38 @@ pub const Options = struct {
|
|||
features: []const []const u8 = &.{},
|
||||
};
|
||||
|
||||
/// Options for runIterator.
|
||||
pub const RunOptions = struct {
|
||||
/// The font state for the terminal screen. This is mutable because
|
||||
/// cached values may be updated during shaping.
|
||||
grid: *SharedGrid,
|
||||
|
||||
/// The terminal screen to shape.
|
||||
screen: *const terminal.Screen,
|
||||
|
||||
/// The row within the screen to shape. This row must exist within
|
||||
/// screen; it is not validated.
|
||||
row: terminal.Pin,
|
||||
|
||||
/// The selection boundaries. This is used to break shaping on
|
||||
/// selection boundaries. This can be disabled by setting this to
|
||||
/// null.
|
||||
selection: ?terminal.Selection = null,
|
||||
|
||||
/// The cursor position within this row. This is used to break shaping
|
||||
/// on cursor boundaries. This can be disabled by setting this to
|
||||
/// null.
|
||||
cursor_x: ?usize = null,
|
||||
|
||||
/// Apply the font break configuration to the run.
|
||||
pub fn applyBreakConfig(
|
||||
self: *RunOptions,
|
||||
config: configpkg.FontShapingBreak,
|
||||
) void {
|
||||
if (!config.cursor) self.cursor_x = null;
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
_ = Cache;
|
||||
_ = Shaper;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const trace = @import("tracy").trace;
|
|||
const font = @import("../main.zig");
|
||||
const os = @import("../../os/main.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const config = @import("../../config.zig");
|
||||
const Feature = font.shape.Feature;
|
||||
const FeatureList = font.shape.FeatureList;
|
||||
const default_features = font.shape.default_features;
|
||||
|
|
@ -108,7 +109,8 @@ pub const Shaper = struct {
|
|||
/// settings the font features of a CoreText font.
|
||||
fn makeFeaturesDict(feats: []const Feature) !*macos.foundation.Dictionary {
|
||||
const list = try macos.foundation.MutableArray.create();
|
||||
errdefer list.release();
|
||||
// The list will be retained by the dict once we add it to it.
|
||||
defer list.release();
|
||||
|
||||
for (feats) |feat| {
|
||||
const value_num: c_int = @intCast(feat.value);
|
||||
|
|
@ -288,19 +290,11 @@ pub const Shaper = struct {
|
|||
|
||||
pub fn runIterator(
|
||||
self: *Shaper,
|
||||
grid: *SharedGrid,
|
||||
screen: *const terminal.Screen,
|
||||
row: terminal.Pin,
|
||||
selection: ?terminal.Selection,
|
||||
cursor_x: ?usize,
|
||||
opts: font.shape.RunOptions,
|
||||
) font.shape.RunIterator {
|
||||
return .{
|
||||
.hooks = .{ .shaper = self },
|
||||
.grid = grid,
|
||||
.screen = screen,
|
||||
.row = row,
|
||||
.selection = selection,
|
||||
.cursor_x = cursor_x,
|
||||
.opts = opts,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -594,13 +588,11 @@ test "run iterator" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |_| count += 1;
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
|
|
@ -613,13 +605,11 @@ test "run iterator" {
|
|||
try screen.testWriteString("ABCD EFG");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |_| count += 1;
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
|
|
@ -633,13 +623,11 @@ test "run iterator" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |_| count += 1;
|
||||
try testing.expectEqual(@as(usize, 3), count);
|
||||
|
|
@ -654,13 +642,11 @@ test "run iterator" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |_| count += 1;
|
||||
try testing.expectEqual(@as(usize, 2), count);
|
||||
|
|
@ -701,13 +687,11 @@ test "run iterator: empty cells with background set" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
{
|
||||
const run = (try it.next(alloc)).?;
|
||||
const cells = try shaper.shape(run);
|
||||
|
|
@ -737,13 +721,11 @@ test "shape" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -772,13 +754,11 @@ test "shape nerd fonts" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -800,13 +780,11 @@ test "shape inconsolata ligs" {
|
|||
try screen.testWriteString(">=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -825,13 +803,11 @@ test "shape inconsolata ligs" {
|
|||
try screen.testWriteString("===");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -858,13 +834,11 @@ test "shape monaspace ligs" {
|
|||
try screen.testWriteString("===");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -892,13 +866,11 @@ test "shape left-replaced lig in last run" {
|
|||
try screen.testWriteString("!==");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -926,13 +898,11 @@ test "shape left-replaced lig in early run" {
|
|||
try screen.testWriteString("!==X");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
|
||||
const run = (try it.next(alloc)).?;
|
||||
|
||||
|
|
@ -957,13 +927,11 @@ test "shape U+3C9 with JB Mono" {
|
|||
try screen.testWriteString("\u{03C9} foo");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
|
||||
var run_count: usize = 0;
|
||||
var cell_count: usize = 0;
|
||||
|
|
@ -990,13 +958,11 @@ test "shape emoji width" {
|
|||
try screen.testWriteString("👍");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1040,13 +1006,11 @@ test "shape emoji width long" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1076,13 +1040,11 @@ test "shape variation selector VS15" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1111,13 +1073,11 @@ test "shape variation selector VS16" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1143,13 +1103,11 @@ test "shape with empty cells in between" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1181,13 +1139,11 @@ test "shape Chinese characters" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1221,13 +1177,11 @@ test "shape box glyphs" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1257,17 +1211,16 @@ test "shape selection boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
terminal.Selection.init(
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.selection = terminal.Selection.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
|
||||
false,
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1280,17 +1233,16 @@ test "shape selection boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
terminal.Selection.init(
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.selection = terminal.Selection.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
|
||||
false,
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1303,17 +1255,16 @@ test "shape selection boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
terminal.Selection.init(
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.selection = terminal.Selection.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
|
||||
false,
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1326,17 +1277,16 @@ test "shape selection boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
terminal.Selection.init(
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.selection = terminal.Selection.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
|
||||
false,
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1349,17 +1299,16 @@ test "shape selection boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
terminal.Selection.init(
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.selection = terminal.Selection.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
|
||||
false,
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1385,13 +1334,11 @@ test "shape cursor boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1400,61 +1347,111 @@ test "shape cursor boundary" {
|
|||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
|
||||
// Cursor at index 0 is two runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
0,
|
||||
);
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
// Cursor at index 0 is two runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.cursor_x = 0,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 2), count);
|
||||
}
|
||||
// And without cursor splitting remains one
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 2), count);
|
||||
}
|
||||
|
||||
// Cursor at index 1 is three runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
1,
|
||||
);
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
// Cursor at index 1 is three runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.cursor_x = 1,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 3), count);
|
||||
}
|
||||
// And without cursor splitting remains one
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 3), count);
|
||||
}
|
||||
|
||||
// Cursor at last col is two runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
9,
|
||||
);
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
// Cursor at last col is two runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.cursor_x = 9,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 2), count);
|
||||
}
|
||||
// And without cursor splitting remains one
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 2), count);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1474,13 +1471,11 @@ test "shape cursor boundary and colored emoji" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1493,13 +1488,12 @@ test "shape cursor boundary and colored emoji" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
0,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.cursor_x = 0,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1510,13 +1504,42 @@ test "shape cursor boundary and colored emoji" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
1,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.cursor_x = 1,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1540,13 +1563,11 @@ test "shape cell attribute change" {
|
|||
try screen.testWriteString(">=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1564,13 +1585,11 @@ test "shape cell attribute change" {
|
|||
try screen.testWriteString("=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1589,13 +1608,11 @@ test "shape cell attribute change" {
|
|||
try screen.testWriteString("=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1614,13 +1631,11 @@ test "shape cell attribute change" {
|
|||
try screen.testWriteString("=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1638,13 +1653,11 @@ test "shape cell attribute change" {
|
|||
try screen.testWriteString("=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1678,13 +1691,11 @@ test "shape high plane sprite font codepoint" {
|
|||
try screen.testWriteString("\u{1FB70}");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
// We should get one run
|
||||
const run = (try it.next(alloc)).?;
|
||||
// The run state should have the UTF-16 encoding of the character.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator;
|
|||
const harfbuzz = @import("harfbuzz");
|
||||
const font = @import("../main.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
const config = @import("../../config.zig");
|
||||
const Feature = font.shape.Feature;
|
||||
const FeatureList = font.shape.FeatureList;
|
||||
const default_features = font.shape.default_features;
|
||||
|
|
@ -89,19 +90,11 @@ pub const Shaper = struct {
|
|||
/// and assume the y value matches.
|
||||
pub fn runIterator(
|
||||
self: *Shaper,
|
||||
grid: *SharedGrid,
|
||||
screen: *const terminal.Screen,
|
||||
row: terminal.Pin,
|
||||
selection: ?terminal.Selection,
|
||||
cursor_x: ?usize,
|
||||
opts: font.shape.RunOptions,
|
||||
) font.shape.RunIterator {
|
||||
return .{
|
||||
.hooks = .{ .shaper = self },
|
||||
.grid = grid,
|
||||
.screen = screen,
|
||||
.row = row,
|
||||
.selection = selection,
|
||||
.cursor_x = cursor_x,
|
||||
.opts = opts,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -225,13 +218,11 @@ test "run iterator" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |_| count += 1;
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
|
|
@ -244,13 +235,11 @@ test "run iterator" {
|
|||
try screen.testWriteString("ABCD EFG");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |_| count += 1;
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
|
|
@ -264,13 +253,11 @@ test "run iterator" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |_| {
|
||||
count += 1;
|
||||
|
|
@ -316,13 +303,11 @@ test "run iterator: empty cells with background set" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
{
|
||||
const run = (try it.next(alloc)).?;
|
||||
try testing.expectEqual(@as(u32, 3), shaper.hb_buf.getLength());
|
||||
|
|
@ -353,13 +338,11 @@ test "shape" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -382,13 +365,11 @@ test "shape inconsolata ligs" {
|
|||
try screen.testWriteString(">=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -407,13 +388,11 @@ test "shape inconsolata ligs" {
|
|||
try screen.testWriteString("===");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -440,13 +419,11 @@ test "shape monaspace ligs" {
|
|||
try screen.testWriteString("===");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -476,13 +453,11 @@ test "shape arabic forced LTR" {
|
|||
try screen.testWriteString(@embedFile("testdata/arabic.txt"));
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -513,13 +488,11 @@ test "shape emoji width" {
|
|||
try screen.testWriteString("👍");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -565,13 +538,11 @@ test "shape emoji width long" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 1 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -603,13 +574,11 @@ test "shape variation selector VS15" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -640,13 +609,11 @@ test "shape variation selector VS16" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -674,13 +641,11 @@ test "shape with empty cells in between" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -712,13 +677,11 @@ test "shape Chinese characters" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -752,13 +715,11 @@ test "shape box glyphs" {
|
|||
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -789,17 +750,16 @@ test "shape selection boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
terminal.Selection.init(
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.selection = terminal.Selection.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
|
||||
false,
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -812,17 +772,16 @@ test "shape selection boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
terminal.Selection.init(
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.selection = terminal.Selection.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = screen.pages.cols - 1, .y = 0 } }).?,
|
||||
false,
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -835,17 +794,16 @@ test "shape selection boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
terminal.Selection.init(
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.selection = terminal.Selection.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
|
||||
false,
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -858,17 +816,16 @@ test "shape selection boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
terminal.Selection.init(
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.selection = terminal.Selection.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = 3, .y = 0 } }).?,
|
||||
false,
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -881,17 +838,16 @@ test "shape selection boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
terminal.Selection.init(
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.selection = terminal.Selection.init(
|
||||
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
|
||||
screen.pages.pin(.{ .active = .{ .x = 1, .y = 0 } }).?,
|
||||
false,
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -917,13 +873,11 @@ test "shape cursor boundary" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -932,61 +886,111 @@ test "shape cursor boundary" {
|
|||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
|
||||
// Cursor at index 0 is two runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
0,
|
||||
);
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
// Cursor at index 0 is two runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.cursor_x = 0,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 2), count);
|
||||
}
|
||||
// And without cursor splitting remains one
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 2), count);
|
||||
}
|
||||
|
||||
// Cursor at index 1 is three runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
1,
|
||||
);
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
// Cursor at index 1 is three runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.cursor_x = 1,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 3), count);
|
||||
}
|
||||
// And without cursor splitting remains one
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 3), count);
|
||||
}
|
||||
|
||||
// Cursor at last col is two runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
9,
|
||||
);
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
// Cursor at last col is two runs
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.cursor_x = 9,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 2), count);
|
||||
}
|
||||
// And without cursor splitting remains one
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 2), count);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1006,13 +1010,11 @@ test "shape cursor boundary and colored emoji" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1025,13 +1027,12 @@ test "shape cursor boundary and colored emoji" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
0,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.cursor_x = 0,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1042,13 +1043,42 @@ test "shape cursor boundary and colored emoji" {
|
|||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
1,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
.cursor_x = 1,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
_ = try shaper.shape(run);
|
||||
}
|
||||
try testing.expectEqual(@as(usize, 1), count);
|
||||
}
|
||||
{
|
||||
// Get our run iterator
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1072,13 +1102,11 @@ test "shape cell attribute change" {
|
|||
try screen.testWriteString(">=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1096,13 +1124,11 @@ test "shape cell attribute change" {
|
|||
try screen.testWriteString("=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1121,13 +1147,11 @@ test "shape cell attribute change" {
|
|||
try screen.testWriteString("=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1146,13 +1170,11 @@ test "shape cell attribute change" {
|
|||
try screen.testWriteString("=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
@ -1170,13 +1192,11 @@ test "shape cell attribute change" {
|
|||
try screen.testWriteString("=");
|
||||
|
||||
var shaper = &testdata.shaper;
|
||||
var it = shaper.runIterator(
|
||||
testdata.grid,
|
||||
&screen,
|
||||
screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
var it = shaper.runIterator(.{
|
||||
.grid = testdata.grid,
|
||||
.screen = &screen,
|
||||
.row = screen.pages.pin(.{ .screen = .{ .y = 0 } }).?,
|
||||
});
|
||||
var count: usize = 0;
|
||||
while (try it.next(alloc)) |run| {
|
||||
count += 1;
|
||||
|
|
|
|||
|
|
@ -70,19 +70,11 @@ pub const Shaper = struct {
|
|||
|
||||
pub fn runIterator(
|
||||
self: *Shaper,
|
||||
grid: *SharedGrid,
|
||||
screen: *const terminal.Screen,
|
||||
row: terminal.Pin,
|
||||
selection: ?terminal.Selection,
|
||||
cursor_x: ?usize,
|
||||
opts: font.shape.RunOptions,
|
||||
) font.shape.RunIterator {
|
||||
return .{
|
||||
.hooks = .{ .shaper = self },
|
||||
.grid = grid,
|
||||
.screen = screen,
|
||||
.row = row,
|
||||
.selection = selection,
|
||||
.cursor_x = cursor_x,
|
||||
.opts = opts,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,15 +35,11 @@ pub const TextRun = struct {
|
|||
/// RunIterator is an iterator that yields text runs.
|
||||
pub const RunIterator = struct {
|
||||
hooks: font.Shaper.RunIteratorHook,
|
||||
grid: *font.SharedGrid,
|
||||
screen: *const terminal.Screen,
|
||||
row: terminal.Pin,
|
||||
selection: ?terminal.Selection = null,
|
||||
cursor_x: ?usize = null,
|
||||
opts: shape.RunOptions,
|
||||
i: usize = 0,
|
||||
|
||||
pub fn next(self: *RunIterator, alloc: Allocator) !?TextRun {
|
||||
const cells = self.row.cells(.all);
|
||||
const cells = self.opts.row.cells(.all);
|
||||
|
||||
// Trim the right side of a row that might be empty
|
||||
const max: usize = max: {
|
||||
|
|
@ -58,7 +54,7 @@ pub const RunIterator = struct {
|
|||
// Invisible cells don't have any glyphs rendered,
|
||||
// so we explicitly skip them in the shaping process.
|
||||
while (self.i < max and
|
||||
self.row.style(&cells[self.i]).flags.invisible)
|
||||
self.opts.row.style(&cells[self.i]).flags.invisible)
|
||||
{
|
||||
self.i += 1;
|
||||
}
|
||||
|
|
@ -76,7 +72,7 @@ pub const RunIterator = struct {
|
|||
var hasher = Hasher.init(0);
|
||||
|
||||
// Let's get our style that we'll expect for the run.
|
||||
const style = self.row.style(&cells[self.i]);
|
||||
const style = self.opts.row.style(&cells[self.i]);
|
||||
|
||||
// Go through cell by cell and accumulate while we build our run.
|
||||
var j: usize = self.i;
|
||||
|
|
@ -86,9 +82,9 @@ pub const RunIterator = struct {
|
|||
|
||||
// If we have a selection and we're at a boundary point, then
|
||||
// we break the run here.
|
||||
if (self.selection) |unordered_sel| {
|
||||
if (self.opts.selection) |unordered_sel| {
|
||||
if (j > self.i) {
|
||||
const sel = unordered_sel.ordered(self.screen, .forward);
|
||||
const sel = unordered_sel.ordered(self.opts.screen, .forward);
|
||||
const start_x = sel.start().x;
|
||||
const end_x = sel.end().x;
|
||||
|
||||
|
|
@ -142,7 +138,7 @@ pub const RunIterator = struct {
|
|||
// The style is different. We allow differing background
|
||||
// styles but any other change results in a new run.
|
||||
const c1 = comparableStyle(style);
|
||||
const c2 = comparableStyle(self.row.style(&cells[j]));
|
||||
const c2 = comparableStyle(self.opts.row.style(&cells[j]));
|
||||
if (!c1.eql(c2)) break;
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +158,7 @@ pub const RunIterator = struct {
|
|||
const presentation: ?font.Presentation = if (cell.hasGrapheme()) p: {
|
||||
// We only check the FIRST codepoint because I believe the
|
||||
// presentation format must be directly adjacent to the codepoint.
|
||||
const cps = self.row.grapheme(cell) orelse break :p null;
|
||||
const cps = self.opts.row.grapheme(cell) orelse break :p null;
|
||||
assert(cps.len > 0);
|
||||
if (cps[0] == 0xFE0E) break :p .text;
|
||||
if (cps[0] == 0xFE0F) break :p .emoji;
|
||||
|
|
@ -186,7 +182,7 @@ pub const RunIterator = struct {
|
|||
// joiners will show the joiners allowing you to modify the
|
||||
// emoji.
|
||||
if (!cell.hasGrapheme()) {
|
||||
if (self.cursor_x) |cursor_x| {
|
||||
if (self.opts.cursor_x) |cursor_x| {
|
||||
// Exactly: self.i is the cursor and we iterated once. This
|
||||
// means that we started exactly at the cursor and did at
|
||||
// exactly one iteration. Why exactly one? Because we may
|
||||
|
|
@ -227,7 +223,7 @@ pub const RunIterator = struct {
|
|||
|
||||
// Otherwise we need a fallback character. Prefer the
|
||||
// official replacement character.
|
||||
if (try self.grid.getIndex(
|
||||
if (try self.opts.grid.getIndex(
|
||||
alloc,
|
||||
0xFFFD, // replacement char
|
||||
font_style,
|
||||
|
|
@ -235,7 +231,7 @@ pub const RunIterator = struct {
|
|||
)) |idx| break :font_info .{ .idx = idx, .fallback = 0xFFFD };
|
||||
|
||||
// Fallback to space
|
||||
if (try self.grid.getIndex(
|
||||
if (try self.opts.grid.getIndex(
|
||||
alloc,
|
||||
' ',
|
||||
font_style,
|
||||
|
|
@ -273,7 +269,7 @@ pub const RunIterator = struct {
|
|||
@intCast(cluster),
|
||||
);
|
||||
if (cell.hasGrapheme()) {
|
||||
const cps = self.row.grapheme(cell).?;
|
||||
const cps = self.opts.row.grapheme(cell).?;
|
||||
for (cps) |cp| {
|
||||
// Do not send presentation modifiers
|
||||
if (cp == 0xFE0E or cp == 0xFE0F) continue;
|
||||
|
|
@ -298,7 +294,7 @@ pub const RunIterator = struct {
|
|||
.hash = hasher.final(),
|
||||
.offset = @intCast(self.i),
|
||||
.cells = @intCast(j - self.i),
|
||||
.grid = self.grid,
|
||||
.grid = self.opts.grid,
|
||||
.font_index = current_font,
|
||||
};
|
||||
}
|
||||
|
|
@ -326,7 +322,7 @@ pub const RunIterator = struct {
|
|||
cell.codepoint() == 0 or
|
||||
cell.codepoint() == terminal.kitty.graphics.unicode.placeholder)
|
||||
{
|
||||
return try self.grid.getIndex(
|
||||
return try self.opts.grid.getIndex(
|
||||
alloc,
|
||||
' ',
|
||||
style,
|
||||
|
|
@ -336,7 +332,7 @@ pub const RunIterator = struct {
|
|||
|
||||
// Get the font index for the primary codepoint.
|
||||
const primary_cp: u32 = cell.codepoint();
|
||||
const primary = try self.grid.getIndex(
|
||||
const primary = try self.opts.grid.getIndex(
|
||||
alloc,
|
||||
primary_cp,
|
||||
style,
|
||||
|
|
@ -349,7 +345,7 @@ pub const RunIterator = struct {
|
|||
|
||||
// If this is a grapheme, we need to find a font that supports
|
||||
// all of the codepoints in the grapheme.
|
||||
const cps = self.row.grapheme(cell) orelse return primary;
|
||||
const cps = self.opts.row.grapheme(cell) orelse return primary;
|
||||
var candidates = try std.ArrayList(font.Collection.Index).initCapacity(alloc, cps.len + 1);
|
||||
defer candidates.deinit();
|
||||
candidates.appendAssumeCapacity(primary);
|
||||
|
|
@ -365,7 +361,7 @@ pub const RunIterator = struct {
|
|||
// to support the base presentation, since it is common for emoji
|
||||
// fonts to support the base emoji with emoji presentation but not
|
||||
// certain ZWJ-combined characters like the male and female signs.
|
||||
const idx = try self.grid.getIndex(
|
||||
const idx = try self.opts.grid.getIndex(
|
||||
alloc,
|
||||
cp,
|
||||
style,
|
||||
|
|
@ -376,11 +372,11 @@ pub const RunIterator = struct {
|
|||
|
||||
// We need to find a candidate that has ALL of our codepoints
|
||||
for (candidates.items) |idx| {
|
||||
if (!self.grid.hasCodepoint(idx, primary_cp, presentation)) continue;
|
||||
if (!self.opts.grid.hasCodepoint(idx, primary_cp, presentation)) continue;
|
||||
for (cps) |cp| {
|
||||
// Ignore Emoji ZWJs
|
||||
if (cp == 0xFE0E or cp == 0xFE0F or cp == 0x200D) continue;
|
||||
if (!self.grid.hasCodepoint(idx, cp, null)) break;
|
||||
if (!self.opts.grid.hasCodepoint(idx, cp, null)) break;
|
||||
} else {
|
||||
// If the while completed, then we have a candidate that
|
||||
// supports all of our codepoints.
|
||||
|
|
|
|||
|
|
@ -61,17 +61,11 @@ pub const Shaper = struct {
|
|||
/// for a Shaper struct since they share state.
|
||||
pub fn runIterator(
|
||||
self: *Shaper,
|
||||
group: *font.GroupCache,
|
||||
row: terminal.Screen.Row,
|
||||
selection: ?terminal.Selection,
|
||||
cursor_x: ?usize,
|
||||
opts: font.shape.RunOptions,
|
||||
) font.shape.RunIterator {
|
||||
return .{
|
||||
.hooks = .{ .shaper = self },
|
||||
.group = group,
|
||||
.row = row,
|
||||
.selection = selection,
|
||||
.cursor_x = cursor_x,
|
||||
.opts = opts,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,12 +32,7 @@ pub const Sprite = enum(u32) {
|
|||
cursor_rect,
|
||||
cursor_hollow_rect,
|
||||
cursor_bar,
|
||||
|
||||
// Note: we don't currently put the box drawing glyphs in here because
|
||||
// there are a LOT and I'm lazy. What I want to do is spend more time
|
||||
// studying the patterns to see if we can programmatically build our
|
||||
// enum perhaps and comptime generate the drawing code at the same time.
|
||||
// I'm not sure if that's advisable yet though.
|
||||
cursor_underline,
|
||||
|
||||
test {
|
||||
const testing = std.testing;
|
||||
|
|
|
|||
|
|
@ -16,25 +16,158 @@ const std = @import("std");
|
|||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const wuffs = @import("wuffs");
|
||||
const z2d = @import("z2d");
|
||||
const font = @import("../main.zig");
|
||||
const Sprite = font.sprite.Sprite;
|
||||
const Box = @import("Box.zig");
|
||||
const Powerline = @import("Powerline.zig");
|
||||
const underline = @import("underline.zig");
|
||||
const cursor = @import("cursor.zig");
|
||||
|
||||
const special = @import("draw/special.zig");
|
||||
|
||||
const log = std.log.scoped(.font_sprite);
|
||||
|
||||
/// Grid metrics for rendering sprites.
|
||||
metrics: font.Metrics,
|
||||
|
||||
pub const DrawFnError =
|
||||
Allocator.Error ||
|
||||
z2d.painter.FillError ||
|
||||
z2d.painter.StrokeError ||
|
||||
error{
|
||||
/// Something went wrong while doing math.
|
||||
MathError,
|
||||
};
|
||||
|
||||
/// A function that draws a glyph on the provided canvas.
|
||||
pub const DrawFn = fn (
|
||||
cp: u32,
|
||||
canvas: *font.sprite.Canvas,
|
||||
width: u32,
|
||||
height: u32,
|
||||
metrics: font.Metrics,
|
||||
) DrawFnError!void;
|
||||
|
||||
const Range = struct {
|
||||
min: u32,
|
||||
max: u32,
|
||||
draw: DrawFn,
|
||||
};
|
||||
|
||||
/// Automatically collect ranges for functions with names
|
||||
/// in the format `draw<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.
|
||||
pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool {
|
||||
// We ignore presentation. No matter what presentation is requested
|
||||
// we always provide glyphs for our codepoints.
|
||||
// We ignore presentation. No matter what presentation is
|
||||
// requested we always provide glyphs for our codepoints.
|
||||
_ = p;
|
||||
_ = self;
|
||||
return Kind.init(cp) != null;
|
||||
return getDrawFn(cp) != null;
|
||||
}
|
||||
|
||||
/// Render the glyph.
|
||||
|
|
@ -52,18 +185,10 @@ pub fn renderGlyph(
|
|||
}
|
||||
}
|
||||
|
||||
const metrics = self.metrics;
|
||||
|
||||
// We adjust our sprite width based on the cell width.
|
||||
const width = switch (opts.cell_width orelse 1) {
|
||||
0, 1 => metrics.cell_width,
|
||||
else => |width| metrics.cell_width * width,
|
||||
};
|
||||
|
||||
// It should be impossible for this to be null and we assert that
|
||||
// in runtime safety modes but in case it is its not worth memory
|
||||
// corruption so we return a valid, blank glyph.
|
||||
const kind = Kind.init(cp) orelse return .{
|
||||
const draw = getDrawFn(cp) orelse return .{
|
||||
.width = 0,
|
||||
.height = 0,
|
||||
.offset_x = 0,
|
||||
|
|
@ -73,217 +198,350 @@ pub fn renderGlyph(
|
|||
.advance_x = 0,
|
||||
};
|
||||
|
||||
// Safe to ".?" because of the above assertion.
|
||||
return switch (kind) {
|
||||
.box => (Box{ .metrics = metrics }).renderGlyph(alloc, atlas, cp),
|
||||
const metrics = self.metrics;
|
||||
|
||||
.underline => try underline.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
@enumFromInt(cp),
|
||||
width,
|
||||
metrics.cell_height,
|
||||
metrics.underline_position,
|
||||
metrics.underline_thickness,
|
||||
),
|
||||
// We adjust our sprite width based on the cell width.
|
||||
const width = switch (opts.cell_width orelse 1) {
|
||||
0, 1 => metrics.cell_width,
|
||||
else => |width| metrics.cell_width * width,
|
||||
};
|
||||
|
||||
.strikethrough => try underline.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
@enumFromInt(cp),
|
||||
width,
|
||||
metrics.cell_height,
|
||||
metrics.strikethrough_position,
|
||||
metrics.strikethrough_thickness,
|
||||
),
|
||||
const height = metrics.cell_height;
|
||||
|
||||
.overline => overline: {
|
||||
var g = try underline.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
@enumFromInt(cp),
|
||||
width,
|
||||
metrics.cell_height,
|
||||
0,
|
||||
metrics.overline_thickness,
|
||||
);
|
||||
const padding_x = width / 4;
|
||||
const padding_y = height / 4;
|
||||
|
||||
// We have to manually subtract the overline position
|
||||
// on the rendered glyph since it can be negative.
|
||||
g.offset_y -= metrics.overline_position;
|
||||
// Make a canvas of the desired size
|
||||
var canvas = try font.sprite.Canvas.init(alloc, width, height, padding_x, padding_y);
|
||||
defer canvas.deinit();
|
||||
|
||||
break :overline g;
|
||||
},
|
||||
try draw(cp, &canvas, width, height, metrics);
|
||||
|
||||
.powerline => powerline: {
|
||||
const f: Powerline = .{
|
||||
.width = metrics.cell_width,
|
||||
.height = metrics.cell_height,
|
||||
.thickness = metrics.box_thickness,
|
||||
};
|
||||
// Write the drawing to the atlas
|
||||
const region = try canvas.writeAtlas(alloc, atlas);
|
||||
|
||||
break :powerline try f.renderGlyph(alloc, atlas, cp);
|
||||
},
|
||||
|
||||
.cursor => cursor: {
|
||||
var g = try cursor.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
@enumFromInt(cp),
|
||||
width,
|
||||
metrics.cursor_height,
|
||||
metrics.cursor_thickness,
|
||||
);
|
||||
|
||||
// Cursors are drawn at their specified height
|
||||
// and are centered vertically within the cell.
|
||||
const cursor_height: i32 = @intCast(metrics.cursor_height);
|
||||
const cell_height: i32 = @intCast(metrics.cell_height);
|
||||
g.offset_y += @divTrunc(cell_height - cursor_height, 2);
|
||||
|
||||
break :cursor g;
|
||||
},
|
||||
return .{
|
||||
.width = region.width,
|
||||
.height = region.height,
|
||||
.offset_x = @as(i32, @intCast(canvas.clip_left)) - @as(i32, @intCast(padding_x)),
|
||||
.offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)),
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = @floatFromInt(width),
|
||||
.sprite = true,
|
||||
};
|
||||
}
|
||||
|
||||
/// Kind of sprites we have. Drawing is implemented separately for each kind.
|
||||
const Kind = enum {
|
||||
box,
|
||||
underline,
|
||||
overline,
|
||||
strikethrough,
|
||||
powerline,
|
||||
cursor,
|
||||
/// Used in `testDrawRanges`, checks for diff between the provided atlas
|
||||
/// and the reference file for the range, returns true if there is a diff.
|
||||
fn testDiffAtlas(
|
||||
alloc: Allocator,
|
||||
atlas: *z2d.Surface,
|
||||
path: []const u8,
|
||||
i: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
thickness: u32,
|
||||
) !bool {
|
||||
// Get the file contents, we compare the PNG data first in
|
||||
// order to ensure that no one smuggles arbitrary binary
|
||||
// data in to the reference PNGs.
|
||||
const test_file = try std.fs.openFileAbsolute(path, .{ .mode = .read_only });
|
||||
defer test_file.close();
|
||||
const test_bytes = try test_file.readToEndAlloc(
|
||||
alloc,
|
||||
std.math.maxInt(usize),
|
||||
);
|
||||
defer alloc.free(test_bytes);
|
||||
|
||||
pub fn init(cp: u32) ?Kind {
|
||||
return switch (cp) {
|
||||
Sprite.start...Sprite.end => switch (@as(Sprite, @enumFromInt(cp))) {
|
||||
.underline,
|
||||
.underline_double,
|
||||
.underline_dotted,
|
||||
.underline_dashed,
|
||||
.underline_curly,
|
||||
=> .underline,
|
||||
const cwd_absolute = try std.fs.cwd().realpathAlloc(alloc, ".");
|
||||
defer alloc.free(cwd_absolute);
|
||||
|
||||
.overline,
|
||||
=> .overline,
|
||||
// Get the reference file contents to compare.
|
||||
const ref_path = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"./src/font/sprite/testdata/U+{X}...U+{X}-{d}x{d}+{d}.png",
|
||||
.{ i, i + 0xFF, width, height, thickness },
|
||||
);
|
||||
defer alloc.free(ref_path);
|
||||
const ref_file =
|
||||
std.fs.cwd().openFile(ref_path, .{ .mode = .read_only }) catch |err| {
|
||||
log.err("Can't open reference file {s}: {}\n", .{
|
||||
ref_path,
|
||||
err,
|
||||
});
|
||||
|
||||
.strikethrough,
|
||||
=> .strikethrough,
|
||||
// Copy the test PNG in to the CWD so it isn't
|
||||
// cleaned up with the rest of the tmp dir files.
|
||||
const test_path = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"{s}/sprite_face_test-U+{X}...U+{X}-{d}x{d}+{d}.png",
|
||||
.{ cwd_absolute, i, i + 0xFF, width, height, thickness },
|
||||
);
|
||||
defer alloc.free(test_path);
|
||||
try std.fs.copyFileAbsolute(path, test_path, .{});
|
||||
|
||||
.cursor_rect,
|
||||
.cursor_hollow_rect,
|
||||
.cursor_bar,
|
||||
=> .cursor,
|
||||
},
|
||||
|
||||
// == Box fonts ==
|
||||
|
||||
// "Box Drawing" block
|
||||
// ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ ┠
|
||||
// ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ ╀ ╁
|
||||
// ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ ╠ ╡ ╢
|
||||
// ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
|
||||
0x2500...0x257F,
|
||||
|
||||
// "Block Elements" block
|
||||
// ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟
|
||||
0x2580...0x259F,
|
||||
|
||||
// "Geometric Shapes" block
|
||||
0x25e2...0x25e5, // ◢◣◤◥
|
||||
0x25f8...0x25fa, // ◸◹◺
|
||||
0x25ff, // ◿
|
||||
|
||||
// "Braille" block
|
||||
0x2800...0x28FF,
|
||||
|
||||
// "Symbols for Legacy Computing" block
|
||||
// (Block Mosaics / "Sextants")
|
||||
// 🬀 🬁 🬂 🬃 🬄 🬅 🬆 🬇 🬈 🬉 🬊 🬋 🬌 🬍 🬎 🬏 🬐 🬑 🬒 🬓 🬔 🬕 🬖 🬗 🬘 🬙 🬚 🬛 🬜 🬝 🬞 🬟 🬠
|
||||
// 🬡 🬢 🬣 🬤 🬥 🬦 🬧 🬨 🬩 🬪 🬫 🬬 🬭 🬮 🬯 🬰 🬱 🬲 🬳 🬴 🬵 🬶 🬷 🬸 🬹 🬺 🬻
|
||||
// (Smooth Mosaics)
|
||||
// 🬼 🬽 🬾 🬿 🭀 🭁 🭂 🭃 🭄 🭅 🭆
|
||||
// 🭇 🭈 🭉 🭊 🭋 🭌 🭍 🭎 🭏 🭐 🭑
|
||||
// 🭒 🭓 🭔 🭕 🭖 🭗 🭘 🭙 🭚 🭛 🭜
|
||||
// 🭝 🭞 🭟 🭠 🭡 🭢 🭣 🭤 🭥 🭦 🭧
|
||||
// 🭨 🭩 🭪 🭫 🭬 🭭 🭮 🭯
|
||||
// (Block Elements)
|
||||
// 🭰 🭱 🭲 🭳 🭴 🭵 🭶 🭷 🭸 🭹 🭺 🭻
|
||||
// 🭼 🭽 🭾 🭿 🮀 🮁
|
||||
// 🮂 🮃 🮄 🮅 🮆
|
||||
// 🮇 🮈 🮉 🮊 🮋
|
||||
// (Rectangular Shade Characters)
|
||||
// 🮌 🮍 🮎 🮏 🮐 🮑 🮒
|
||||
0x1FB00...0x1FB92,
|
||||
// (Rectangular Shade Characters)
|
||||
// 🮔
|
||||
// (Fill Characters)
|
||||
// 🮕 🮖 🮗
|
||||
// (Diagonal Fill Characters)
|
||||
// 🮘 🮙
|
||||
// (Smooth Mosaics)
|
||||
// 🮚 🮛
|
||||
// (Triangular Shade Characters)
|
||||
// 🮜 🮝 🮞 🮟
|
||||
// (Character Cell Diagonals)
|
||||
// 🮠 🮡 🮢 🮣 🮤 🮥 🮦 🮧 🮨 🮩 🮪 🮫 🮬 🮭 🮮
|
||||
// (Light Solid Line With Stroke)
|
||||
// 🮯
|
||||
0x1FB94...0x1FBAF,
|
||||
// (Negative Terminal Characters)
|
||||
// 🮽 🮾 🮿
|
||||
0x1FBBD...0x1FBBF,
|
||||
// (Block Elements)
|
||||
//
|
||||
// (Character Cell Diagonals)
|
||||
//
|
||||
// (Geometric Shapes)
|
||||
//
|
||||
0x1FBCE...0x1FBEF,
|
||||
// (Octants)
|
||||
0x1CD00...0x1CDE5,
|
||||
=> .box,
|
||||
|
||||
// Branch drawing character set, used for drawing git-like
|
||||
// graphs in the terminal. Originally implemented in Kitty.
|
||||
// Ref:
|
||||
// - https://github.com/kovidgoyal/kitty/pull/7681
|
||||
// - https://github.com/kovidgoyal/kitty/pull/7805
|
||||
// NOTE: Kitty is GPL licensed, and its code was not referenced
|
||||
// for these characters, only the loose specification of
|
||||
// the character set in the pull request descriptions.
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
0xF5D0...0xF60D => .box,
|
||||
|
||||
// Separated Block Quadrants from Symbols for Legacy Computing Supplement
|
||||
//
|
||||
0x1CC21...0x1CC2F => .box,
|
||||
|
||||
// Powerline fonts
|
||||
0xE0B0,
|
||||
0xE0B1,
|
||||
0xE0B3,
|
||||
0xE0B4,
|
||||
0xE0B6,
|
||||
0xE0B2,
|
||||
0xE0B8,
|
||||
0xE0BA,
|
||||
0xE0BC,
|
||||
0xE0BE,
|
||||
0xE0D2,
|
||||
0xE0D4,
|
||||
=> .powerline,
|
||||
|
||||
else => null,
|
||||
return true;
|
||||
};
|
||||
defer ref_file.close();
|
||||
const ref_bytes = try ref_file.readToEndAlloc(
|
||||
alloc,
|
||||
std.math.maxInt(usize),
|
||||
);
|
||||
defer alloc.free(ref_bytes);
|
||||
|
||||
// Do our PNG bytes comparison, if it's the same then we can
|
||||
// move on, otherwise we'll decode the reference file and do
|
||||
// a pixel-for-pixel diff.
|
||||
if (std.mem.eql(u8, test_bytes, ref_bytes)) return false;
|
||||
|
||||
// Copy the test PNG in to the CWD so it isn't
|
||||
// cleaned up with the rest of the tmp dir files.
|
||||
const test_path = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"{s}/sprite_face_test-U+{X}...U+{X}-{d}x{d}+{d}.png",
|
||||
.{ cwd_absolute, i, i + 0xFF, width, height, thickness },
|
||||
);
|
||||
defer alloc.free(test_path);
|
||||
try std.fs.copyFileAbsolute(path, test_path, .{});
|
||||
|
||||
// Use wuffs to decode the reference PNG to raw pixels.
|
||||
// These will be RGBA, so when diffing we can just compare
|
||||
// every fourth byte.
|
||||
const ref_rgba = try wuffs.png.decode(alloc, ref_bytes);
|
||||
defer alloc.free(ref_rgba.data);
|
||||
|
||||
assert(ref_rgba.width == atlas.getWidth());
|
||||
assert(ref_rgba.height == atlas.getHeight());
|
||||
|
||||
// We'll make a visual representation of the diff using
|
||||
// red for removed pixels and green for added. We make
|
||||
// a z2d surface for that here.
|
||||
var diff = try z2d.Surface.init(
|
||||
.image_surface_rgb,
|
||||
alloc,
|
||||
atlas.getWidth(),
|
||||
atlas.getHeight(),
|
||||
);
|
||||
defer diff.deinit(alloc);
|
||||
const diff_pix = diff.image_surface_rgb.buf;
|
||||
|
||||
const test_gray = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf);
|
||||
|
||||
var differs: bool = false;
|
||||
for (0..test_gray.len) |j| {
|
||||
const t = test_gray[j];
|
||||
const r = ref_rgba.data[j * 4];
|
||||
if (t == r) {
|
||||
// If the pixels match, write it as a faded gray.
|
||||
diff_pix[j].r = t / 3;
|
||||
diff_pix[j].g = t / 3;
|
||||
diff_pix[j].b = t / 3;
|
||||
} else {
|
||||
differs = true;
|
||||
// Otherwise put the reference value in the red
|
||||
// channel and the new value in the green channel.
|
||||
diff_pix[j].r = r;
|
||||
diff_pix[j].g = t;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// If the PNG data differs but not the raw pixels, that's
|
||||
// a big red flag, since it could mean someone is trying to
|
||||
// smuggle binary data in to the test files.
|
||||
if (!differs) {
|
||||
log.err(
|
||||
"!!! Test PNG data does not match reference, but pixels do match! " ++
|
||||
"Either z2d's PNG exporter changed or someone is " ++
|
||||
"trying to smuggle binary data in the test files!\n" ++
|
||||
"test={s}, reference={s}",
|
||||
.{ test_path, ref_path },
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Drop the diff image as a PNG in the cwd.
|
||||
const diff_path = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"./sprite_face_diff-U+{X}...U+{X}-{d}x{d}+{d}.png",
|
||||
.{ i, i + 0xFF, width, height, thickness },
|
||||
);
|
||||
defer alloc.free(diff_path);
|
||||
try z2d.png_exporter.writeToPNGFile(diff, diff_path, .{});
|
||||
log.err(
|
||||
"One or more glyphs differ from reference file in range U+{X}...U+{X}! " ++
|
||||
"test={s}, reference={s}, diff={s}",
|
||||
.{ i, i + 0xFF, test_path, ref_path, diff_path },
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Draws all ranges in to a set of 16x16 glyph atlases, checks for regressions
|
||||
/// against reference files, logs errors and exposes a diff for any difference
|
||||
/// between the reference and test atlas.
|
||||
///
|
||||
/// Returns true if there was a diff.
|
||||
fn testDrawRanges(
|
||||
width: u32,
|
||||
ascent: u32,
|
||||
descent: u32,
|
||||
thickness: u32,
|
||||
) !bool {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const metrics: font.Metrics = .calc(.{
|
||||
.cell_width = @floatFromInt(width),
|
||||
.ascent = @floatFromInt(ascent),
|
||||
.descent = -@as(f64, @floatFromInt(descent)),
|
||||
.line_gap = 0.0,
|
||||
.underline_thickness = @floatFromInt(thickness),
|
||||
.strikethrough_thickness = @floatFromInt(thickness),
|
||||
});
|
||||
|
||||
const height = ascent + descent;
|
||||
|
||||
const padding_x = width / 4;
|
||||
const padding_y = height / 4;
|
||||
|
||||
// Canvas to draw glyphs on, we'll re-use this for all glyphs.
|
||||
var canvas = try font.sprite.Canvas.init(
|
||||
alloc,
|
||||
width,
|
||||
height,
|
||||
padding_x,
|
||||
padding_y,
|
||||
);
|
||||
defer canvas.deinit();
|
||||
|
||||
// We render glyphs in batches of 256, which we copy (including padding) to
|
||||
// a 16 by 16 surface to be compared with the reference file for that range.
|
||||
const stride_x = width + 2 * padding_x;
|
||||
const stride_y = height + 2 * padding_y;
|
||||
var atlas = try z2d.Surface.init(
|
||||
.image_surface_alpha8,
|
||||
alloc,
|
||||
@intCast(stride_x * 16),
|
||||
@intCast(stride_y * 16),
|
||||
);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var i: u32 = std.mem.alignBackward(u32, ranges[0].min, 0x100);
|
||||
|
||||
// Try to make the sprite_face_test folder if it doesn't already exist.
|
||||
var dir = testing.tmpDir(.{});
|
||||
defer dir.cleanup();
|
||||
const tmp_dir = try dir.dir.realpathAlloc(alloc, ".");
|
||||
defer alloc.free(tmp_dir);
|
||||
|
||||
// We set this to true if we have any fails so we can
|
||||
// return an error after we're done comparing all glyphs.
|
||||
var fail: bool = false;
|
||||
|
||||
inline for (ranges) |range| {
|
||||
for (range.min..range.max + 1) |cp| {
|
||||
// If we've moved to a new batch of 256, check the
|
||||
// current one and clear the surface for the next one.
|
||||
if (cp - i >= 0x100) {
|
||||
// Export to our tmp dir.
|
||||
const path = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"{s}/U+{X}...U+{X}-{d}x{d}+{d}.png",
|
||||
.{ tmp_dir, i, i + 0xFF, width, height, thickness },
|
||||
);
|
||||
defer alloc.free(path);
|
||||
try z2d.png_exporter.writeToPNGFile(atlas, path, .{});
|
||||
|
||||
if (try testDiffAtlas(
|
||||
alloc,
|
||||
&atlas,
|
||||
path,
|
||||
i,
|
||||
width,
|
||||
height,
|
||||
thickness,
|
||||
)) fail = true;
|
||||
|
||||
i = std.mem.alignBackward(u32, @intCast(cp), 0x100);
|
||||
@memset(std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf), 0);
|
||||
}
|
||||
|
||||
try getDrawFn(@intCast(cp)).?(
|
||||
@intCast(cp),
|
||||
&canvas,
|
||||
width,
|
||||
height,
|
||||
metrics,
|
||||
);
|
||||
canvas.clearClippingRegions();
|
||||
atlas.composite(
|
||||
&canvas.sfc,
|
||||
.src,
|
||||
@intCast(stride_x * ((cp - i) % 16)),
|
||||
@intCast(stride_y * ((cp - i) / 16)),
|
||||
.{},
|
||||
);
|
||||
@memset(std.mem.sliceAsBytes(canvas.sfc.image_surface_alpha8.buf), 0);
|
||||
canvas.clip_top = 0;
|
||||
canvas.clip_left = 0;
|
||||
canvas.clip_right = 0;
|
||||
canvas.clip_bottom = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const path = try std.fmt.allocPrint(
|
||||
alloc,
|
||||
"{s}/U+{X}...U+{X}-{d}x{d}+{d}.png",
|
||||
.{ tmp_dir, i, i + 0xFF, width, height, thickness },
|
||||
);
|
||||
defer alloc.free(path);
|
||||
try z2d.png_exporter.writeToPNGFile(atlas, path, .{});
|
||||
if (try testDiffAtlas(
|
||||
alloc,
|
||||
&atlas,
|
||||
path,
|
||||
i,
|
||||
width,
|
||||
height,
|
||||
thickness,
|
||||
)) fail = true;
|
||||
|
||||
return fail;
|
||||
}
|
||||
|
||||
test "sprite face render all sprites" {
|
||||
// Renders all sprites to an atlas and compares
|
||||
// it to a ground truth for regression testing.
|
||||
|
||||
var diff: bool = false;
|
||||
|
||||
// testDrawRanges(width, ascent, descent, thickness):
|
||||
//
|
||||
// We compare 4 different sets of metrics;
|
||||
// - even cell size / even thickness
|
||||
// - even cell size / odd thickness
|
||||
// - odd cell size / even thickness
|
||||
// - odd cell size / odd thickness
|
||||
// (Also a decreasing range of sizes.)
|
||||
if (try testDrawRanges(18, 30, 6, 4)) diff = true;
|
||||
if (try testDrawRanges(12, 20, 4, 3)) diff = true;
|
||||
if (try testDrawRanges(11, 19, 2, 2)) diff = true;
|
||||
if (try testDrawRanges(9, 15, 2, 1)) diff = true;
|
||||
|
||||
try std.testing.expect(!diff); // There should be no diffs from reference.
|
||||
}
|
||||
|
||||
// test "sprite face print all sprites" {
|
||||
// std.debug.print("\n\n", .{});
|
||||
// inline for (ranges) |range| {
|
||||
// for (range.min..range.max + 1) |cp| {
|
||||
// std.debug.print("{u}", .{ @as(u21, @intCast(cp)) });
|
||||
// }
|
||||
// }
|
||||
// std.debug.print("\n\n", .{});
|
||||
// }
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
std.testing.refAllDecls(@This());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -81,19 +81,39 @@ pub const Canvas = struct {
|
|||
/// The underlying z2d surface.
|
||||
sfc: z2d.Surface,
|
||||
|
||||
padding_x: u32,
|
||||
padding_y: u32,
|
||||
|
||||
clip_top: u32 = 0,
|
||||
clip_left: u32 = 0,
|
||||
clip_right: u32 = 0,
|
||||
clip_bottom: u32 = 0,
|
||||
|
||||
alloc: Allocator,
|
||||
|
||||
pub fn init(alloc: Allocator, width: u32, height: u32) !Canvas {
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
width: u32,
|
||||
height: u32,
|
||||
padding_x: u32,
|
||||
padding_y: u32,
|
||||
) !Canvas {
|
||||
// Create the surface we'll be using.
|
||||
// We add padding to both sides (hence `2 *`)
|
||||
const sfc = try z2d.Surface.initPixel(
|
||||
.{ .alpha8 = .{ .a = 0 } },
|
||||
alloc,
|
||||
@intCast(width),
|
||||
@intCast(height),
|
||||
@intCast(width + 2 * padding_x),
|
||||
@intCast(height + 2 * padding_y),
|
||||
);
|
||||
errdefer sfc.deinit(alloc);
|
||||
|
||||
return .{ .sfc = sfc, .alloc = alloc };
|
||||
return .{
|
||||
.sfc = sfc,
|
||||
.padding_x = padding_x,
|
||||
.padding_y = padding_y,
|
||||
.alloc = alloc,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Canvas) void {
|
||||
|
|
@ -109,30 +129,33 @@ pub const Canvas = struct {
|
|||
) (Allocator.Error || font.Atlas.Error)!font.Atlas.Region {
|
||||
assert(atlas.format == .grayscale);
|
||||
|
||||
const width = @as(u32, @intCast(self.sfc.getWidth()));
|
||||
const height = @as(u32, @intCast(self.sfc.getHeight()));
|
||||
self.trim();
|
||||
|
||||
const sfc_width: u32 = @intCast(self.sfc.getWidth());
|
||||
const sfc_height: u32 = @intCast(self.sfc.getHeight());
|
||||
|
||||
// Subtract our clip margins from the
|
||||
// width and height to get region size.
|
||||
const region_width = sfc_width -| self.clip_left -| self.clip_right;
|
||||
const region_height = sfc_height -| self.clip_top -| self.clip_bottom;
|
||||
|
||||
// Allocate our texture atlas region
|
||||
const region = region: {
|
||||
// We need to add a 1px padding to the font so that we don't
|
||||
// get fuzzy issues when blending textures.
|
||||
const padding = 1;
|
||||
|
||||
// Get the full padded region
|
||||
// Reserve a region with a 1px margin on the bottom and right edges
|
||||
// so that we can avoid interpolation between adjacent glyphs during
|
||||
// texture sampling.
|
||||
var region = try atlas.reserve(
|
||||
alloc,
|
||||
width + (padding * 2), // * 2 because left+right
|
||||
height + (padding * 2), // * 2 because top+bottom
|
||||
region_width + 1,
|
||||
region_height + 1,
|
||||
);
|
||||
|
||||
// Modify the region so that we remove the padding so that
|
||||
// we write to the non-zero location. The data in an Altlas
|
||||
// is always initialized to zero (Atlas.clear) so we don't
|
||||
// need to worry about zero-ing that.
|
||||
region.x += padding;
|
||||
region.y += padding;
|
||||
region.width -= padding * 2;
|
||||
region.height -= padding * 2;
|
||||
// Modify the region to remove the margin so that we write to the
|
||||
// non-zero location. The data in an Altlas is always initialized
|
||||
// to zero (Atlas.clear) so we don't need to worry about zero-ing
|
||||
// that.
|
||||
region.width -= 1;
|
||||
region.height -= 1;
|
||||
break :region region;
|
||||
};
|
||||
|
||||
|
|
@ -140,38 +163,138 @@ pub const Canvas = struct {
|
|||
const buffer: []u8 = @ptrCast(self.sfc.image_surface_alpha8.buf);
|
||||
|
||||
// Write the glyph information into the atlas
|
||||
assert(region.width == width);
|
||||
assert(region.height == height);
|
||||
atlas.set(region, buffer);
|
||||
assert(region.width == region_width);
|
||||
assert(region.height == region_height);
|
||||
atlas.setFromLarger(
|
||||
region,
|
||||
buffer,
|
||||
sfc_width,
|
||||
self.clip_left,
|
||||
self.clip_top,
|
||||
);
|
||||
}
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
// Adjust clip boundaries to trim off any fully transparent rows or columns.
|
||||
// This circumvents abstractions from z2d so that it can be performant.
|
||||
fn trim(self: *Canvas) void {
|
||||
const width: u32 = @intCast(self.sfc.getWidth());
|
||||
const height: u32 = @intCast(self.sfc.getHeight());
|
||||
|
||||
const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf);
|
||||
|
||||
top: while (self.clip_top < height - self.clip_bottom) {
|
||||
const y = self.clip_top;
|
||||
const x0 = self.clip_left;
|
||||
const x1 = width - self.clip_right;
|
||||
for (buf[y * width ..][x0..x1]) |v| {
|
||||
if (v != 0) break :top;
|
||||
}
|
||||
self.clip_top += 1;
|
||||
}
|
||||
|
||||
bottom: while (self.clip_bottom < height - self.clip_top) {
|
||||
const y = height - self.clip_bottom -| 1;
|
||||
const x0 = self.clip_left;
|
||||
const x1 = width - self.clip_right;
|
||||
for (buf[y * width ..][x0..x1]) |v| {
|
||||
if (v != 0) break :bottom;
|
||||
}
|
||||
self.clip_bottom += 1;
|
||||
}
|
||||
|
||||
left: while (self.clip_left < width - self.clip_right) {
|
||||
const x = self.clip_left;
|
||||
const y0 = self.clip_top;
|
||||
const y1 = height - self.clip_bottom;
|
||||
for (y0..y1) |y| {
|
||||
if (buf[y * width + x] != 0) break :left;
|
||||
}
|
||||
self.clip_left += 1;
|
||||
}
|
||||
|
||||
right: while (self.clip_right < width - self.clip_left) {
|
||||
const x = width - self.clip_right -| 1;
|
||||
const y0 = self.clip_top;
|
||||
const y1 = height - self.clip_bottom;
|
||||
for (y0..y1) |y| {
|
||||
if (buf[y * width + x] != 0) break :right;
|
||||
}
|
||||
self.clip_right += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Only really useful for test purposes, since the clipping region is
|
||||
/// automatically excluded when writing to an atlas with `writeAtlas`.
|
||||
pub fn clearClippingRegions(self: *Canvas) void {
|
||||
const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf);
|
||||
const width: usize = @intCast(self.sfc.getWidth());
|
||||
const height: usize = @intCast(self.sfc.getHeight());
|
||||
|
||||
for (0..height) |y| {
|
||||
for (0..self.clip_left) |x| {
|
||||
buf[y * width + x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (0..height) |y| {
|
||||
for (width - self.clip_right..width) |x| {
|
||||
buf[y * width + x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (0..self.clip_top) |y| {
|
||||
for (0..width) |x| {
|
||||
buf[y * width + x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (height - self.clip_bottom..height) |y| {
|
||||
for (0..width) |x| {
|
||||
buf[y * width + x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a transformation representing the translation for our padding.
|
||||
pub fn transformation(self: Canvas) z2d.Transformation {
|
||||
return .{
|
||||
.ax = 1,
|
||||
.by = 0,
|
||||
.cx = 0,
|
||||
.dy = 1,
|
||||
.tx = @as(f64, @floatFromInt(self.padding_x)),
|
||||
.ty = @as(f64, @floatFromInt(self.padding_y)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Acquires a z2d drawing context, caller MUST deinit context.
|
||||
pub fn getContext(self: *Canvas) z2d.Context {
|
||||
return .init(self.alloc, &self.sfc);
|
||||
var ctx = z2d.Context.init(self.alloc, &self.sfc);
|
||||
// Offset by our padding to keep
|
||||
// coordinates relative to the cell.
|
||||
ctx.setTransformation(self.transformation());
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/// Draw and fill a single pixel
|
||||
pub fn pixel(self: *Canvas, x: u32, y: u32, color: Color) void {
|
||||
pub fn pixel(self: *Canvas, x: i32, y: i32, color: Color) void {
|
||||
self.sfc.putPixel(
|
||||
@intCast(x),
|
||||
@intCast(y),
|
||||
x + @as(i32, @intCast(self.padding_x)),
|
||||
y + @as(i32, @intCast(self.padding_y)),
|
||||
.{ .alpha8 = .{ .a = @intFromEnum(color) } },
|
||||
);
|
||||
}
|
||||
|
||||
/// Draw and fill a rectangle. This is the main primitive for drawing
|
||||
/// lines as well (which are just generally skinny rectangles...)
|
||||
pub fn rect(self: *Canvas, v: Rect(u32), color: Color) void {
|
||||
const x0 = v.x;
|
||||
const x1 = v.x + v.width;
|
||||
const y0 = v.y;
|
||||
const y1 = v.y + v.height;
|
||||
|
||||
for (y0..y1) |y| {
|
||||
for (x0..x1) |x| {
|
||||
pub fn rect(self: *Canvas, v: Rect(i32), color: Color) void {
|
||||
var y = v.y;
|
||||
while (y < v.y + v.height) : (y += 1) {
|
||||
var x = v.x;
|
||||
while (x < v.x + v.width) : (x += 1) {
|
||||
self.pixel(
|
||||
@intCast(x),
|
||||
@intCast(y),
|
||||
|
|
@ -181,96 +304,226 @@ pub const Canvas = struct {
|
|||
}
|
||||
}
|
||||
|
||||
/// Convenience wrapper for `Canvas.rect`
|
||||
pub fn box(
|
||||
self: *Canvas,
|
||||
x0: i32,
|
||||
y0: i32,
|
||||
x1: i32,
|
||||
y1: i32,
|
||||
color: Color,
|
||||
) void {
|
||||
self.rect((Box(i32){
|
||||
.p0 = .{ .x = x0, .y = y0 },
|
||||
.p1 = .{ .x = x1, .y = y1 },
|
||||
}).rect(), color);
|
||||
}
|
||||
|
||||
/// Draw and fill a quad.
|
||||
pub fn quad(self: *Canvas, q: Quad(f64), color: Color) !void {
|
||||
var path: z2d.StaticPath(6) = .{};
|
||||
path.init(); // nodes.len = 0
|
||||
|
||||
var path = self.staticPath(6); // nodes.len = 0
|
||||
path.moveTo(q.p0.x, q.p0.y); // +1, nodes.len = 1
|
||||
path.lineTo(q.p1.x, q.p1.y); // +1, nodes.len = 2
|
||||
path.lineTo(q.p2.x, q.p2.y); // +1, nodes.len = 3
|
||||
path.lineTo(q.p3.x, q.p3.y); // +1, nodes.len = 4
|
||||
path.close(); // +2, nodes.len = 6
|
||||
|
||||
try z2d.painter.fill(
|
||||
self.alloc,
|
||||
&self.sfc,
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
|
||||
} },
|
||||
path.wrapped_path.nodes.items,
|
||||
.{},
|
||||
);
|
||||
try self.fillPath(path.wrapped_path, .{}, color);
|
||||
}
|
||||
|
||||
/// Draw and fill a triangle.
|
||||
pub fn triangle(self: *Canvas, t: Triangle(f64), color: Color) !void {
|
||||
var path: z2d.StaticPath(5) = .{};
|
||||
path.init(); // nodes.len = 0
|
||||
|
||||
var path = self.staticPath(5); // nodes.len = 0
|
||||
path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1
|
||||
path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2
|
||||
path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3
|
||||
path.close(); // +2, nodes.len = 5
|
||||
try self.fillPath(path.wrapped_path, .{}, color);
|
||||
}
|
||||
|
||||
/// Stroke a line.
|
||||
pub fn line(
|
||||
self: *Canvas,
|
||||
l: Line(f64),
|
||||
thickness: f64,
|
||||
color: Color,
|
||||
) !void {
|
||||
var path = self.staticPath(2); // nodes.len = 0
|
||||
path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1
|
||||
path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2
|
||||
try self.strokePath(
|
||||
path.wrapped_path,
|
||||
.{
|
||||
.line_cap_mode = .butt,
|
||||
.line_width = thickness,
|
||||
},
|
||||
color,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a static path of the provided len and initialize it.
|
||||
/// Use this function instead of making the path manually since
|
||||
/// it ensures that the transform is applied.
|
||||
pub inline fn staticPath(
|
||||
self: *Canvas,
|
||||
comptime len: usize,
|
||||
) z2d.StaticPath(len) {
|
||||
var path: z2d.StaticPath(len) = .{};
|
||||
path.init();
|
||||
path.wrapped_path.transformation = self.transformation();
|
||||
return path;
|
||||
}
|
||||
|
||||
/// Stroke a z2d path.
|
||||
pub fn strokePath(
|
||||
self: *Canvas,
|
||||
path: z2d.Path,
|
||||
opts: z2d.painter.StrokeOpts,
|
||||
color: Color,
|
||||
) z2d.painter.StrokeError!void {
|
||||
try z2d.painter.stroke(
|
||||
self.alloc,
|
||||
&self.sfc,
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
|
||||
} },
|
||||
path.nodes.items,
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
/// Do an inner stroke on a z2d path, right now this involves a pretty
|
||||
/// heavy workaround that uses two extra surfaces; in the future, z2d
|
||||
/// should add inner and outer strokes natively.
|
||||
pub fn innerStrokePath(
|
||||
self: *Canvas,
|
||||
path: z2d.Path,
|
||||
opts: z2d.painter.StrokeOpts,
|
||||
color: Color,
|
||||
) (z2d.painter.StrokeError || z2d.painter.FillError)!void {
|
||||
// On one surface we fill the shape, this will be a mask we
|
||||
// multiply with the double-width stroke so that only the
|
||||
// part inside is used.
|
||||
var fill_sfc: z2d.Surface = try .init(
|
||||
.image_surface_alpha8,
|
||||
self.alloc,
|
||||
self.sfc.getWidth(),
|
||||
self.sfc.getHeight(),
|
||||
);
|
||||
defer fill_sfc.deinit(self.alloc);
|
||||
|
||||
// On the other we'll do the double width stroke.
|
||||
var stroke_sfc: z2d.Surface = try .init(
|
||||
.image_surface_alpha8,
|
||||
self.alloc,
|
||||
self.sfc.getWidth(),
|
||||
self.sfc.getHeight(),
|
||||
);
|
||||
defer stroke_sfc.deinit(self.alloc);
|
||||
|
||||
// Make a closed version of the path for our fill, so
|
||||
// that we can support open paths for inner stroke.
|
||||
var closed_path = path;
|
||||
closed_path.nodes = try path.nodes.clone(self.alloc);
|
||||
defer closed_path.deinit(self.alloc);
|
||||
try closed_path.close(self.alloc);
|
||||
|
||||
// Fill the shape in white to the fill surface, we use
|
||||
// white because this is a mask that we'll multiply with
|
||||
// the stroke, we want everything inside to be the stroke
|
||||
// color.
|
||||
try z2d.painter.fill(
|
||||
self.alloc,
|
||||
&fill_sfc,
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = 255 } },
|
||||
} },
|
||||
closed_path.nodes.items,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Stroke the shape with double the desired width.
|
||||
var mut_opts = opts;
|
||||
mut_opts.line_width *= 2;
|
||||
try z2d.painter.stroke(
|
||||
self.alloc,
|
||||
&stroke_sfc,
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
|
||||
} },
|
||||
path.nodes.items,
|
||||
mut_opts,
|
||||
);
|
||||
|
||||
// We multiply the stroke sfc on to the fill surface.
|
||||
// The z2d composite operation doesn't seem to work for
|
||||
// this with alpha8 surfaces, so we have to do it manually.
|
||||
for (
|
||||
std.mem.sliceAsBytes(fill_sfc.image_surface_alpha8.buf),
|
||||
std.mem.sliceAsBytes(stroke_sfc.image_surface_alpha8.buf),
|
||||
) |*d, s| {
|
||||
d.* = @intFromFloat(@round(
|
||||
255.0 *
|
||||
(@as(f64, @floatFromInt(s)) / 255.0) *
|
||||
(@as(f64, @floatFromInt(d.*)) / 255.0),
|
||||
));
|
||||
}
|
||||
|
||||
// Then we composite the result on to the main surface.
|
||||
self.sfc.composite(&fill_sfc, .src_over, 0, 0, .{});
|
||||
}
|
||||
|
||||
/// Fill a z2d path.
|
||||
pub fn fillPath(
|
||||
self: *Canvas,
|
||||
path: z2d.Path,
|
||||
opts: z2d.painter.FillOpts,
|
||||
color: Color,
|
||||
) z2d.painter.FillError!void {
|
||||
try z2d.painter.fill(
|
||||
self.alloc,
|
||||
&self.sfc,
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
|
||||
} },
|
||||
path.wrapped_path.nodes.items,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn triangle_outline(self: *Canvas, t: Triangle(f64), thickness: f64, color: Color) !void {
|
||||
var path: z2d.StaticPath(3) = .{};
|
||||
path.init(); // nodes.len = 0
|
||||
|
||||
path.moveTo(t.p0.x, t.p0.y); // +1, nodes.len = 1
|
||||
path.lineTo(t.p1.x, t.p1.y); // +1, nodes.len = 2
|
||||
path.lineTo(t.p2.x, t.p2.y); // +1, nodes.len = 3
|
||||
|
||||
try z2d.painter.stroke(
|
||||
self.alloc,
|
||||
&self.sfc,
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
|
||||
} },
|
||||
path.wrapped_path.nodes.items,
|
||||
.{
|
||||
.line_cap_mode = .round,
|
||||
.line_width = thickness,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Stroke a line.
|
||||
pub fn line(self: *Canvas, l: Line(f64), thickness: f64, color: Color) !void {
|
||||
var path: z2d.StaticPath(2) = .{};
|
||||
path.init(); // nodes.len = 0
|
||||
|
||||
path.moveTo(l.p0.x, l.p0.y); // +1, nodes.len = 1
|
||||
path.lineTo(l.p1.x, l.p1.y); // +1, nodes.len = 2
|
||||
|
||||
try z2d.painter.stroke(
|
||||
self.alloc,
|
||||
&self.sfc,
|
||||
&.{ .opaque_pattern = .{
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(color) } },
|
||||
} },
|
||||
path.wrapped_path.nodes.items,
|
||||
.{
|
||||
.line_cap_mode = .round,
|
||||
.line_width = thickness,
|
||||
},
|
||||
path.nodes.items,
|
||||
opts,
|
||||
);
|
||||
}
|
||||
|
||||
/// Invert all pixels on the canvas.
|
||||
pub fn invert(self: *Canvas) void {
|
||||
for (std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf)) |*v| {
|
||||
v.* = 255 - v.*;
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirror the canvas horizontally.
|
||||
pub fn flipHorizontal(self: *Canvas) Allocator.Error!void {
|
||||
const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf);
|
||||
const clone = try self.alloc.dupe(u8, buf);
|
||||
defer self.alloc.free(clone);
|
||||
const width: usize = @intCast(self.sfc.getWidth());
|
||||
const height: usize = @intCast(self.sfc.getHeight());
|
||||
for (0..height) |y| {
|
||||
for (0..width) |x| {
|
||||
buf[y * width + x] = clone[y * width + width - x - 1];
|
||||
}
|
||||
}
|
||||
std.mem.swap(u32, &self.clip_left, &self.clip_right);
|
||||
}
|
||||
|
||||
/// Mirror the canvas vertically.
|
||||
pub fn flipVertical(self: *Canvas) Allocator.Error!void {
|
||||
const buf = std.mem.sliceAsBytes(self.sfc.image_surface_alpha8.buf);
|
||||
const clone = try self.alloc.dupe(u8, buf);
|
||||
defer self.alloc.free(clone);
|
||||
const width: usize = @intCast(self.sfc.getWidth());
|
||||
const height: usize = @intCast(self.sfc.getHeight());
|
||||
for (0..height) |y| {
|
||||
for (0..width) |x| {
|
||||
buf[y * width + x] = clone[(height - y - 1) * width + x];
|
||||
}
|
||||
}
|
||||
std.mem.swap(u32, &self.clip_top, &self.clip_bottom);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 794 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1006 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 751 B |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1022 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 917 B |