mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge branch 'ghostty-org:main' into alt-keybindings-copy-and-paste
This commit is contained in:
22
.github/workflows/release-tip.yml
vendored
22
.github/workflows/release-tip.yml
vendored
@ -297,14 +297,14 @@ jobs:
|
||||
python3 ./dist/macos/update_appcast_tip.py
|
||||
test -f appcast_new.xml
|
||||
|
||||
# Update Blob Storage
|
||||
# Upload our binaries first
|
||||
- name: Prep R2 Storage
|
||||
run: |
|
||||
mkdir blob
|
||||
mkdir -p blob/${GHOSTTY_COMMIT_LONG}
|
||||
cp ghostty-macos-universal.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal.zip
|
||||
cp ghostty-macos-universal-dsym.zip blob/${GHOSTTY_COMMIT_LONG}/ghostty-macos-universal-dsym.zip
|
||||
cp appcast_new.xml blob/appcast.xml
|
||||
|
||||
- name: Upload to R2
|
||||
uses: ryand56/r2-upload-action@latest
|
||||
with:
|
||||
@ -315,6 +315,24 @@ jobs:
|
||||
source-dir: blob
|
||||
destination-dir: ./
|
||||
|
||||
# Now upload our appcast. This ensures that the appcast never
|
||||
# gets out of sync with the binaries.
|
||||
- name: Prep R2 Storage for Appcast
|
||||
run: |
|
||||
rm -r blob
|
||||
mkdir blob
|
||||
cp appcast_new.xml blob/appcast.xml
|
||||
|
||||
- name: Upload Appcast to R2
|
||||
uses: ryand56/r2-upload-action@latest
|
||||
with:
|
||||
r2-account-id: ${{ secrets.CF_R2_TIP_ACCOUNT_ID }}
|
||||
r2-access-key-id: ${{ secrets.CF_R2_TIP_AWS_KEY }}
|
||||
r2-secret-access-key: ${{ secrets.CF_R2_TIP_SECRET_KEY }}
|
||||
r2-bucket: ghostty-tip
|
||||
source-dir: blob
|
||||
destination-dir: ./
|
||||
|
||||
build-macos-debug-slow:
|
||||
if: |
|
||||
${{
|
||||
|
72
.github/workflows/update-colorschemes.yml
vendored
Normal file
72
.github/workflows/update-colorschemes.yml
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
name: Update iTerm2 colorschemes
|
||||
on:
|
||||
schedule:
|
||||
# Once a week
|
||||
- cron: "0 0 * * 0"
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
update-iterm2-schemes:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
permissions:
|
||||
# Needed for create-pull-request action
|
||||
contents: write
|
||||
pull-requests: write
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@v1.2.0
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@v30
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@v15
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Run zig fetch
|
||||
id: zig_fetch
|
||||
run: |
|
||||
UPSTREAM_REV="$(curl "https://api.github.com/repos/mbadolato/iTerm2-Color-Schemes/commits/master" | jq -r '.sha')"
|
||||
nix develop -c zig fetch --save="iterm2_themes" "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/$UPSTREAM_REV.tar.gz"
|
||||
echo "upstream_rev=$UPSTREAM_REV" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update zig cache hash
|
||||
run: |
|
||||
# Only proceed if build.zig.zon has changed
|
||||
if ! git diff --exit-code build.zig.zon; then
|
||||
nix develop -c ./nix/build-support/check-zig-cache-hash.sh --update
|
||||
nix develop -c ./nix/build-support/check-zig-cache-hash.sh
|
||||
fi
|
||||
|
||||
# Verify the build still works. We choose an arbitrary build type
|
||||
# as a canary instead of testing all build types.
|
||||
- name: Test Build
|
||||
run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true
|
||||
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
title: Update iTerm2 colorschemes
|
||||
base: main
|
||||
branch: iterm2_colors_action
|
||||
commit-message: "deps: Update iTerm2 color schemes"
|
||||
add-paths: |
|
||||
build.zig.zon
|
||||
nix/zigCacheHash.nix
|
||||
body: |
|
||||
Upstream revision: https://github.com/mbadolato/iTerm2-Color-Schemes/tree/${{ steps.zig_fetch.outputs.upstream_rev }}
|
||||
labels: dependencies
|
13
README.md
13
README.md
@ -286,13 +286,16 @@ if [ -n "${GHOSTTY_RESOURCES_DIR}" ]; then
|
||||
fi
|
||||
```
|
||||
|
||||
For details see <a href="https://github.com/ghostty-org/ghostty/blob/main/src/shell-integration/README.md">shell-integration/README.md</a>.
|
||||
|
||||
Each shell integration's installation instructions are documented inline:
|
||||
|
||||
| Shell | Integration |
|
||||
| ------ | ---------------------------------------------------------------------------------------------- |
|
||||
| `bash` | `${GHOSTTY_RESOURCES_DIR}/shell-integration/bash/ghostty.bash` |
|
||||
| `fish` | `${GHOSTTY_RESOURCES_DIR}/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish` |
|
||||
| `zsh` | `${GHOSTTY_RESOURCES_DIR}/shell-integration/zsh/ghostty-integration` |
|
||||
| Shell | Integration |
|
||||
| -------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `bash` | `${GHOSTTY_RESOURCES_DIR}/shell-integration/bash/ghostty.bash` |
|
||||
| `fish` | `${GHOSTTY_RESOURCES_DIR}/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish` |
|
||||
| `zsh` | `${GHOSTTY_RESOURCES_DIR}/shell-integration/zsh/ghostty-integration` |
|
||||
| `elvish` | `${GHOSTTY_RESOURCES_DIR}/shell-integration/elvish/lib/ghostty-integration.elv` |
|
||||
|
||||
### Terminfo
|
||||
|
||||
|
16
build.zig
16
build.zig
@ -12,6 +12,7 @@ const terminfo = @import("src/terminfo/main.zig");
|
||||
const config_vim = @import("src/config/vim.zig");
|
||||
const config_sublime_syntax = @import("src/config/sublime_syntax.zig");
|
||||
const fish_completions = @import("src/build/fish_completions.zig");
|
||||
const zsh_completions = @import("src/build/zsh_completions.zig");
|
||||
const build_config = @import("src/build_config.zig");
|
||||
const BuildConfig = build_config.BuildConfig;
|
||||
const WasmTarget = @import("src/os/wasm/target.zig").Target;
|
||||
@ -504,6 +505,18 @@ pub fn build(b: *std.Build) !void {
|
||||
});
|
||||
}
|
||||
|
||||
// zsh shell completions
|
||||
{
|
||||
const wf = b.addWriteFiles();
|
||||
_ = wf.add("_ghostty", zsh_completions.zsh_completions);
|
||||
|
||||
b.installDirectory(.{
|
||||
.source_dir = wf.getDirectory(),
|
||||
.install_dir = .prefix,
|
||||
.install_subdir = "share/zsh/site-functions",
|
||||
});
|
||||
}
|
||||
|
||||
// Vim plugin
|
||||
{
|
||||
const wf = b.addWriteFiles();
|
||||
@ -1107,8 +1120,7 @@ fn addDeps(
|
||||
});
|
||||
step.root_module.addImport("oniguruma", oniguruma_dep.module("oniguruma"));
|
||||
if (b.systemIntegrationOption("oniguruma", .{})) {
|
||||
// Oniguruma is compiled and distributed as libonig.so
|
||||
step.linkSystemLibrary2("onig", dynamic_link_opts);
|
||||
step.linkSystemLibrary2("oniguruma", dynamic_link_opts);
|
||||
} else {
|
||||
step.linkLibrary(oniguruma_dep.artifact("oniguruma"));
|
||||
try static_libs.append(oniguruma_dep.artifact("oniguruma").getEmittedBin());
|
||||
|
@ -49,8 +49,8 @@
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/80543b14552b7c9fef88fad826552e6ac5632abe.tar.gz",
|
||||
.hash = "1220217ae916146a4c598f8ba5bfff0ff940335d00572e337f20b4accf24fa2ca4fc",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5fd82e34a349e36a5b3422d8225c4e044c8b3b4b.tar.gz",
|
||||
.hash = "122083713c189f1ceab516efd494123386f3a29132a68a6896b651319a8c57d747e4",
|
||||
},
|
||||
.vaxis = .{
|
||||
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
|
||||
|
0
dist/linux/ghostty_dolphin.desktop
vendored
Executable file → Normal file
0
dist/linux/ghostty_dolphin.desktop
vendored
Executable file → Normal file
@ -947,14 +947,7 @@ extension Ghostty {
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
guard let title = String(cString: v.title!, encoding: .utf8) else { return }
|
||||
|
||||
// We must set this in a dispatchqueue to avoid a deadlock on startup on some
|
||||
// versions of macOS. I unfortunately didn't document the exact versions so
|
||||
// I don't know when its safe to remove this.
|
||||
DispatchQueue.main.async {
|
||||
surfaceView.title = title
|
||||
}
|
||||
|
||||
surfaceView.setTitle(title)
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
@ -1089,7 +1082,10 @@ extension Ghostty {
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
let backingSize = NSSize(width: Double(v.width), height: Double(v.height))
|
||||
surfaceView.cellSize = surfaceView.convertFromBacking(backingSize)
|
||||
DispatchQueue.main.async { [weak surfaceView] in
|
||||
guard let surfaceView else { return }
|
||||
surfaceView.cellSize = surfaceView.convertFromBacking(backingSize)
|
||||
}
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
|
@ -12,7 +12,7 @@ extension Ghostty {
|
||||
// The current title of the surface as defined by the pty. This can be
|
||||
// changed with escape codes. This is public because the callbacks go
|
||||
// to the app level and it is set from there.
|
||||
@Published var title: String = "👻"
|
||||
@Published private(set) var title: String = "👻"
|
||||
|
||||
// The current pwd of the surface as defined by the pty. This can be
|
||||
// changed with escape codes.
|
||||
@ -110,6 +110,9 @@ extension Ghostty {
|
||||
// This is set to non-null during keyDown to accumulate insertText contents
|
||||
private var keyTextAccumulator: [String]? = nil
|
||||
|
||||
// A small delay that is introduced before a title change to avoid flickers
|
||||
private var titleChangeTimer: Timer?
|
||||
|
||||
// We need to support being a first responder so that we can get input events
|
||||
override var acceptsFirstResponder: Bool { return true }
|
||||
|
||||
@ -339,6 +342,20 @@ extension Ghostty {
|
||||
NSCursor.setHiddenUntilMouseMoves(!visible)
|
||||
}
|
||||
|
||||
func setTitle(_ title: String) {
|
||||
// This fixes an issue where very quick changes to the title could
|
||||
// cause an unpleasant flickering. We set a timer so that we can
|
||||
// coalesce rapid changes. The timer is short enough that it still
|
||||
// feels "instant".
|
||||
titleChangeTimer?.invalidate()
|
||||
titleChangeTimer = Timer.scheduledTimer(
|
||||
withTimeInterval: 0.075,
|
||||
repeats: false
|
||||
) { [weak self] _ in
|
||||
self?.title = title
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@objc private func onUpdateRendererHealth(notification: SwiftUI.Notification) {
|
||||
|
@ -167,6 +167,9 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
||||
self.savedState = savedState
|
||||
|
||||
// We hide the dock if the window is on a screen with the dock.
|
||||
// We must hide the dock FIRST then hide the menu:
|
||||
// If you specify autoHideMenuBar, it must be accompanied by either hideDock or autoHideDock.
|
||||
// https://developer.apple.com/documentation/appkit/nsapplication/presentationoptions-swift.struct
|
||||
if (savedState.dock) {
|
||||
hideDock()
|
||||
}
|
||||
@ -176,18 +179,6 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
||||
hideMenu()
|
||||
}
|
||||
|
||||
// When this window becomes or resigns main we need to run some logic.
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(windowDidBecomeMain),
|
||||
name: NSWindow.didBecomeMainNotification,
|
||||
object: window)
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(windowDidResignMain),
|
||||
name: NSWindow.didResignMainNotification,
|
||||
object: window)
|
||||
|
||||
// When we change screens we need to redo everything.
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
@ -222,8 +213,6 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
||||
// Remove all our notifications. We remove them one by one because
|
||||
// we don't want to remove the observers that our superclass sets.
|
||||
let center = NotificationCenter.default
|
||||
center.removeObserver(self, name: NSWindow.didBecomeMainNotification, object: window)
|
||||
center.removeObserver(self, name: NSWindow.didResignMainNotification, object: window)
|
||||
center.removeObserver(self, name: NSWindow.didChangeScreenNotification, object: window)
|
||||
|
||||
// Unhide our elements
|
||||
@ -315,42 +304,6 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
||||
exit()
|
||||
}
|
||||
|
||||
@objc func windowDidBecomeMain(_ notification: Notification) {
|
||||
guard let savedState else { return }
|
||||
|
||||
// This should always be true due to how we register but just be sure
|
||||
guard let object = notification.object as? NSWindow,
|
||||
object == window else { return }
|
||||
|
||||
// This is crazy but at least on macOS 15.0, you must hide the dock
|
||||
// FIRST then hide the menu. If you do the opposite, it does not
|
||||
// work.
|
||||
|
||||
if savedState.dock {
|
||||
hideDock()
|
||||
}
|
||||
|
||||
if (properties.hideMenu) {
|
||||
hideMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func windowDidResignMain(_ notification: Notification) {
|
||||
guard let savedState else { return }
|
||||
|
||||
// This should always be true due to how we register but just be sure
|
||||
guard let object = notification.object as? NSWindow,
|
||||
object == window else { return }
|
||||
|
||||
if (properties.hideMenu) {
|
||||
unhideMenu()
|
||||
}
|
||||
|
||||
if savedState.dock {
|
||||
unhideDock()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Dock
|
||||
|
||||
private func hideDock() {
|
||||
|
@ -1,3 +1,3 @@
|
||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||
# more details.
|
||||
"sha256-c3MQJG7vwQBOaxHQ8cYP0HxdsLqlgsVmAiT1d7gq6js="
|
||||
"sha256-q9UDVryP50HfeeafgnrOd+D6K+cEy33/05K2TB5qiqw="
|
||||
|
@ -215,6 +215,9 @@ pub const SfntTag = enum(c_int) {
|
||||
pub fn DataType(comptime self: SfntTag) type {
|
||||
return switch (self) {
|
||||
.os2 => c.TT_OS2,
|
||||
.head => c.TT_Header,
|
||||
.post => c.TT_Postscript,
|
||||
.hhea => c.TT_HoriHeader,
|
||||
else => unreachable, // As-needed...
|
||||
};
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ pub const FontVariationAxisKey = font_descriptor.FontVariationAxisKey;
|
||||
pub const FontSymbolicTraits = font_descriptor.FontSymbolicTraits;
|
||||
pub const createFontDescriptorsFromURL = font_manager.createFontDescriptorsFromURL;
|
||||
pub const createFontDescriptorsFromData = font_manager.createFontDescriptorsFromData;
|
||||
pub const createFontDescriptorFromData = font_manager.createFontDescriptorFromData;
|
||||
pub const Frame = frame.Frame;
|
||||
pub const Framesetter = framesetter.Framesetter;
|
||||
pub const Line = line.Line;
|
||||
|
@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const foundation = @import("../foundation.zig");
|
||||
const FontDescriptor = @import("./font_descriptor.zig").FontDescriptor;
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
pub fn createFontDescriptorsFromURL(url: *foundation.URL) ?*foundation.Array {
|
||||
@ -14,3 +15,9 @@ pub fn createFontDescriptorsFromData(data: *foundation.Data) ?*foundation.Array
|
||||
@ptrCast(data),
|
||||
)));
|
||||
}
|
||||
|
||||
pub fn createFontDescriptorFromData(data: *foundation.Data) ?*FontDescriptor {
|
||||
return @ptrFromInt(@intFromPtr(c.CTFontManagerCreateFontDescriptorFromData(
|
||||
@ptrCast(data),
|
||||
)));
|
||||
}
|
||||
|
@ -440,9 +440,9 @@ fn isExecutable(mode: std.fs.File.Mode) bool {
|
||||
return mode & 0o0111 != 0;
|
||||
}
|
||||
|
||||
// `hostname` is present on both *nix and windows
|
||||
// `uname -n` is the *nix equivalent of `hostname.exe` on Windows
|
||||
test "expandPath: hostname" {
|
||||
const executable = if (builtin.os.tag == .windows) "hostname.exe" else "hostname";
|
||||
const executable = if (builtin.os.tag == .windows) "hostname.exe" else "uname";
|
||||
const path = (try expandPath(testing.allocator, executable)).?;
|
||||
defer testing.allocator.free(path);
|
||||
try testing.expect(path.len > executable.len);
|
||||
|
@ -245,7 +245,7 @@ const DerivedConfig = struct {
|
||||
mouse_scroll_multiplier: f64,
|
||||
mouse_shift_capture: configpkg.MouseShiftCapture,
|
||||
macos_non_native_fullscreen: configpkg.NonNativeFullscreen,
|
||||
macos_option_as_alt: configpkg.OptionAsAlt,
|
||||
macos_option_as_alt: ?configpkg.OptionAsAlt,
|
||||
vt_kam_allowed: bool,
|
||||
window_padding_top: u32,
|
||||
window_padding_bottom: u32,
|
||||
@ -1990,12 +1990,26 @@ fn encodeKey(
|
||||
// inputs there are many keybindings that result in no encoding
|
||||
// whatsoever.
|
||||
const enc: input.KeyEncoder = enc: {
|
||||
const option_as_alt: configpkg.OptionAsAlt = self.config.macos_option_as_alt orelse detect: {
|
||||
// Non-macOS doesn't use this value so ignore.
|
||||
if (comptime builtin.os.tag != .macos) break :detect .false;
|
||||
|
||||
// If we don't have alt pressed, it doesn't matter what this
|
||||
// config is so we can just say "false" and break out and avoid
|
||||
// more expensive checks below.
|
||||
if (!event.mods.alt) break :detect .false;
|
||||
|
||||
// Alt is pressed, we're on macOS. We break some encapsulation
|
||||
// here and assume libghostty for ease...
|
||||
break :detect self.rt_app.keyboardLayout().detectOptionAsAlt();
|
||||
};
|
||||
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
const t = &self.io.terminal;
|
||||
break :enc .{
|
||||
.event = event,
|
||||
.macos_option_as_alt = self.config.macos_option_as_alt,
|
||||
.macos_option_as_alt = option_as_alt,
|
||||
.alt_esc_prefix = t.modes.get(.alt_esc_prefix),
|
||||
.cursor_key_application = t.modes.get(.cursor_keys),
|
||||
.keypad_key_application = t.modes.get(.keypad_keys),
|
||||
|
@ -105,11 +105,14 @@ pub const App = struct {
|
||||
var config_clone = try config.clone(alloc);
|
||||
errdefer config_clone.deinit();
|
||||
|
||||
var keymap = try input.Keymap.init();
|
||||
errdefer keymap.deinit();
|
||||
|
||||
return .{
|
||||
.core_app = core_app,
|
||||
.config = config_clone,
|
||||
.opts = opts,
|
||||
.keymap = try input.Keymap.init(),
|
||||
.keymap = keymap,
|
||||
.keymap_state = .{},
|
||||
};
|
||||
}
|
||||
@ -161,8 +164,15 @@ pub const App = struct {
|
||||
// then we strip the alt modifier from the mods for translation.
|
||||
const translate_mods = translate_mods: {
|
||||
var translate_mods = mods;
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
const strip = switch (self.config.@"macos-option-as-alt") {
|
||||
if ((comptime builtin.target.isDarwin()) and translate_mods.alt) {
|
||||
// Note: the keyboardLayout() function is not super cheap
|
||||
// so we only want to run it if alt is already pressed hence
|
||||
// the above condition.
|
||||
const option_as_alt: configpkg.OptionAsAlt =
|
||||
self.config.@"macos-option-as-alt" orelse
|
||||
self.keyboardLayout().detectOptionAsAlt();
|
||||
|
||||
const strip = switch (option_as_alt) {
|
||||
.false => false,
|
||||
.true => mods.alt,
|
||||
.left => mods.sides.alt == .left,
|
||||
@ -382,6 +392,25 @@ pub const App = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads the keyboard layout.
|
||||
///
|
||||
/// Kind of expensive so this should be avoided if possible. When I say
|
||||
/// "kind of expensive" I mean that its not something you probably want
|
||||
/// to run on every keypress.
|
||||
pub fn keyboardLayout(self: *const App) input.KeyboardLayout {
|
||||
// We only support keyboard layout detection on macOS.
|
||||
if (comptime builtin.os.tag != .macos) return .unknown;
|
||||
|
||||
// Any layout larger than this is not something we can handle.
|
||||
var buf: [256]u8 = undefined;
|
||||
const id = self.keymap.sourceId(&buf) catch |err| {
|
||||
comptime assert(@TypeOf(err) == error{OutOfMemory});
|
||||
return .unknown;
|
||||
};
|
||||
|
||||
return input.KeyboardLayout.mapAppleId(id) orelse .unknown;
|
||||
}
|
||||
|
||||
pub fn wakeup(self: *const App) void {
|
||||
self.opts.wakeup(self.opts.userdata);
|
||||
}
|
||||
@ -1551,7 +1580,8 @@ pub const CAPI = struct {
|
||||
@truncate(@as(c_uint, @bitCast(mods_raw))),
|
||||
));
|
||||
const result = mods.translation(
|
||||
surface.core_surface.config.macos_option_as_alt,
|
||||
surface.core_surface.config.macos_option_as_alt orelse
|
||||
surface.app.keyboardLayout().detectOptionAsAlt(),
|
||||
);
|
||||
return @intCast(@as(input.Mods.Backing, @bitCast(result)));
|
||||
}
|
||||
|
@ -409,6 +409,13 @@ pub const App = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keyboardLayout(self: *const App) input.KeyboardLayout {
|
||||
_ = self;
|
||||
|
||||
// Not supported by glfw
|
||||
return .unknown;
|
||||
}
|
||||
|
||||
/// Mac-specific settings. This is only enabled when the target is
|
||||
/// Mac and the artifact is a standalone exe. We don't target libs because
|
||||
/// the embedded API doesn't do windowing.
|
||||
|
@ -123,13 +123,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
// and initializing a Vulkan context was causing a longer delay
|
||||
// on some systems.
|
||||
_ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles,vulkan-disable");
|
||||
|
||||
// Wayland-EGL on GTK 4.14 causes "Failed to create EGL context" errors.
|
||||
// This can be fixed by forcing the backend to prefer X11. This issue
|
||||
// appears to be fixed in GTK 4.16 but I wasn't able to bisect why.
|
||||
// The "*" at the end says that if X11 fails, try all remaining
|
||||
// backends.
|
||||
_ = internal_os.setenv("GDK_BACKEND", "x11,*");
|
||||
} else {
|
||||
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
|
||||
// is an environment that isn't tested well and we don't have a
|
||||
|
@ -17,13 +17,13 @@ window: *c.GtkWindow,
|
||||
view: PrimaryView,
|
||||
|
||||
data: [:0]u8,
|
||||
core_surface: CoreSurface,
|
||||
core_surface: *CoreSurface,
|
||||
pending_req: apprt.ClipboardRequest,
|
||||
|
||||
pub fn create(
|
||||
app: *App,
|
||||
data: []const u8,
|
||||
core_surface: CoreSurface,
|
||||
core_surface: *CoreSurface,
|
||||
request: apprt.ClipboardRequest,
|
||||
) !void {
|
||||
if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists;
|
||||
@ -54,7 +54,7 @@ fn init(
|
||||
self: *ClipboardConfirmation,
|
||||
app: *App,
|
||||
data: []const u8,
|
||||
core_surface: CoreSurface,
|
||||
core_surface: *CoreSurface,
|
||||
request: apprt.ClipboardRequest,
|
||||
) !void {
|
||||
// Create the window
|
||||
|
@ -1051,7 +1051,7 @@ pub fn clipboardRequest(
|
||||
}
|
||||
|
||||
pub fn setClipboardString(
|
||||
self: *const Surface,
|
||||
self: *Surface,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
confirm: bool,
|
||||
@ -1065,7 +1065,7 @@ pub fn setClipboardString(
|
||||
ClipboardConfirmationWindow.create(
|
||||
self.app,
|
||||
val,
|
||||
self.core_surface,
|
||||
&self.core_surface,
|
||||
.{ .osc_52_write = clipboard_type },
|
||||
) catch |window_err| {
|
||||
log.err("failed to create clipboard confirmation window err={}", .{window_err});
|
||||
@ -1113,7 +1113,7 @@ fn gtkClipboardRead(
|
||||
ClipboardConfirmationWindow.create(
|
||||
self.app,
|
||||
str,
|
||||
self.core_surface,
|
||||
&self.core_surface,
|
||||
req.state,
|
||||
) catch |window_err| {
|
||||
log.err("failed to create clipboard confirmation window err={}", .{window_err});
|
||||
|
@ -2,9 +2,6 @@ const std = @import("std");
|
||||
|
||||
const Config = @import("../config/Config.zig");
|
||||
const Action = @import("../cli/action.zig").Action;
|
||||
const ListFontsConfig = @import("../cli/list_fonts.zig").Config;
|
||||
const ShowConfigOptions = @import("../cli/show_config.zig").Options;
|
||||
const ListKeybindsOptions = @import("../cli/list_keybinds.zig").Options;
|
||||
|
||||
/// A fish completions configuration that contains all the available commands
|
||||
/// and options.
|
||||
@ -12,7 +9,7 @@ pub const fish_completions = comptimeGenerateFishCompletions();
|
||||
|
||||
fn comptimeGenerateFishCompletions() []const u8 {
|
||||
comptime {
|
||||
@setEvalBranchQuota(18000);
|
||||
@setEvalBranchQuota(50000);
|
||||
var counter = std.io.countingWriter(std.io.null_writer);
|
||||
try writeFishCompletions(&counter.writer());
|
||||
|
||||
@ -41,7 +38,7 @@ fn writeFishCompletions(writer: anytype) !void {
|
||||
|
||||
try writer.writeAll("complete -c ghostty -f\n");
|
||||
|
||||
try writer.writeAll("complete -c ghostty -l help -f\n");
|
||||
try writer.writeAll("complete -c ghostty -s e -l help -f\n");
|
||||
try writer.writeAll("complete -c ghostty -n \"not __fish_seen_subcommand_from $commands\" -l version -f\n");
|
||||
|
||||
for (@typeInfo(Config).Struct.fields) |field| {
|
||||
@ -100,66 +97,35 @@ fn writeFishCompletions(writer: anytype) !void {
|
||||
try writer.writeAll("\"\n");
|
||||
}
|
||||
|
||||
for (@typeInfo(ListFontsConfig).Struct.fields) |field| {
|
||||
if (field.name[0] == '_') continue;
|
||||
try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +list-fonts\" -l ");
|
||||
try writer.writeAll(field.name);
|
||||
try writer.writeAll(if (field.type != bool) " -r" else " ");
|
||||
try writer.writeAll(" -f");
|
||||
switch (@typeInfo(field.type)) {
|
||||
.Bool => try writer.writeAll(" -a \"true false\""),
|
||||
.Enum => |info| {
|
||||
try writer.writeAll(" -a \"");
|
||||
for (info.fields, 0..) |f, i| {
|
||||
if (i > 0) try writer.writeAll(" ");
|
||||
try writer.writeAll(f.name);
|
||||
}
|
||||
try writer.writeAll("\"");
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
for (@typeInfo(Action).Enum.fields) |field| {
|
||||
if (std.mem.eql(u8, "help", field.name)) continue;
|
||||
if (std.mem.eql(u8, "version", field.name)) continue;
|
||||
|
||||
for (@typeInfo(ShowConfigOptions).Struct.fields) |field| {
|
||||
if (field.name[0] == '_') continue;
|
||||
try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +show-config\" -l ");
|
||||
try writer.writeAll(field.name);
|
||||
try writer.writeAll(if (field.type != bool) " -r" else " ");
|
||||
try writer.writeAll(" -f");
|
||||
switch (@typeInfo(field.type)) {
|
||||
.Bool => try writer.writeAll(" -a \"true false\""),
|
||||
.Enum => |info| {
|
||||
try writer.writeAll(" -a \"");
|
||||
for (info.fields, 0..) |f, i| {
|
||||
if (i > 0) try writer.writeAll(" ");
|
||||
try writer.writeAll(f.name);
|
||||
}
|
||||
try writer.writeAll("\"");
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
const options = @field(Action, field.name).options();
|
||||
for (@typeInfo(options).Struct.fields) |opt| {
|
||||
if (opt.name[0] == '_') continue;
|
||||
try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +" ++ field.name ++ "\" -l ");
|
||||
try writer.writeAll(opt.name);
|
||||
try writer.writeAll(if (opt.type != bool) " -r" else "");
|
||||
|
||||
for (@typeInfo(ListKeybindsOptions).Struct.fields) |field| {
|
||||
if (field.name[0] == '_') continue;
|
||||
try writer.writeAll("complete -c ghostty -n \"__fish_seen_subcommand_from +list-keybinds\" -l ");
|
||||
try writer.writeAll(field.name);
|
||||
try writer.writeAll(if (field.type != bool) " -r" else " ");
|
||||
try writer.writeAll(" -f");
|
||||
switch (@typeInfo(field.type)) {
|
||||
.Bool => try writer.writeAll(" -a \"true false\""),
|
||||
.Enum => |info| {
|
||||
try writer.writeAll(" -a \"");
|
||||
for (info.fields, 0..) |f, i| {
|
||||
if (i > 0) try writer.writeAll(" ");
|
||||
try writer.writeAll(f.name);
|
||||
}
|
||||
try writer.writeAll("\"");
|
||||
},
|
||||
else => {},
|
||||
// special case +validate_config --config-file
|
||||
if (std.mem.eql(u8, "config-file", opt.name)) {
|
||||
try writer.writeAll(" -F");
|
||||
} else try writer.writeAll(" -f");
|
||||
|
||||
switch (@typeInfo(opt.type)) {
|
||||
.Bool => try writer.writeAll(" -a \"true false\""),
|
||||
.Enum => |info| {
|
||||
try writer.writeAll(" -a \"");
|
||||
for (info.opts, 0..) |f, i| {
|
||||
if (i > 0) try writer.writeAll(" ");
|
||||
try writer.writeAll(f.name);
|
||||
}
|
||||
try writer.writeAll("\"");
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
}
|
||||
|
@ -30,10 +30,10 @@ pub fn genConfig(writer: anytype, cli: bool) !void {
|
||||
inline for (@typeInfo(Config).Struct.fields) |field| {
|
||||
if (field.name[0] == '_') continue;
|
||||
|
||||
try writer.writeAll("`");
|
||||
try writer.writeAll("**`");
|
||||
if (cli) try writer.writeAll("--");
|
||||
try writer.writeAll(field.name);
|
||||
try writer.writeAll("`\n\n");
|
||||
try writer.writeAll("`**\n\n");
|
||||
if (@hasDecl(help_strings.Config, field.name)) {
|
||||
var iter = std.mem.splitScalar(u8, @field(help_strings.Config, field.name), '\n');
|
||||
var first = true;
|
||||
@ -60,12 +60,12 @@ pub fn genActions(writer: anytype) !void {
|
||||
const action = std.meta.stringToEnum(Action, field.name).?;
|
||||
|
||||
switch (action) {
|
||||
.help => try writer.writeAll("`--help`\n\n"),
|
||||
.version => try writer.writeAll("`--version`\n\n"),
|
||||
.help => try writer.writeAll("**`--help`**\n\n"),
|
||||
.version => try writer.writeAll("**`--version`**\n\n"),
|
||||
else => {
|
||||
try writer.writeAll("`+");
|
||||
try writer.writeAll("**`+");
|
||||
try writer.writeAll(field.name);
|
||||
try writer.writeAll("`\n\n");
|
||||
try writer.writeAll("`**\n\n");
|
||||
},
|
||||
}
|
||||
|
||||
@ -97,9 +97,9 @@ pub fn genKeybindActions(writer: anytype) !void {
|
||||
inline for (info.Union.fields) |field| {
|
||||
if (field.name[0] == '_') continue;
|
||||
|
||||
try writer.writeAll("`");
|
||||
try writer.writeAll("**`");
|
||||
try writer.writeAll(field.name);
|
||||
try writer.writeAll("`\n\n");
|
||||
try writer.writeAll("`**\n\n");
|
||||
|
||||
if (@hasDecl(help_strings.KeybindAction, field.name)) {
|
||||
var iter = std.mem.splitScalar(u8, @field(help_strings.KeybindAction, field.name), '\n');
|
||||
|
204
src/build/zsh_completions.zig
Normal file
204
src/build/zsh_completions.zig
Normal file
@ -0,0 +1,204 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Config = @import("../config/Config.zig");
|
||||
const Action = @import("../cli/action.zig").Action;
|
||||
|
||||
/// A zsh completions configuration that contains all the available commands
|
||||
/// and options.
|
||||
pub const zsh_completions = comptimeGenerateZshCompletions();
|
||||
|
||||
fn comptimeGenerateZshCompletions() []const u8 {
|
||||
comptime {
|
||||
@setEvalBranchQuota(19000);
|
||||
var counter = std.io.countingWriter(std.io.null_writer);
|
||||
try writeZshCompletions(&counter.writer());
|
||||
|
||||
var buf: [counter.bytes_written]u8 = undefined;
|
||||
var stream = std.io.fixedBufferStream(&buf);
|
||||
try writeZshCompletions(stream.writer());
|
||||
const final = buf;
|
||||
return final[0..stream.getWritten().len];
|
||||
}
|
||||
}
|
||||
|
||||
fn writeZshCompletions(writer: anytype) !void {
|
||||
try writer.writeAll(
|
||||
\\#compdef ghostty
|
||||
\\
|
||||
\\_fonts () {
|
||||
\\ local font_list=$(ghostty +list-fonts | grep -Z '^[A-Z]')
|
||||
\\ local fonts=(${(f)font_list})
|
||||
\\ _describe -t fonts 'fonts' fonts
|
||||
\\}
|
||||
\\
|
||||
\\_themes() {
|
||||
\\ local theme_list=$(ghostty +list-themes | sed -E 's/^(.*) \(.*$/\1/')
|
||||
\\ local themes=(${(f)theme_list})
|
||||
\\ _describe -t themes 'themes' themes
|
||||
\\}
|
||||
\\
|
||||
);
|
||||
|
||||
try writer.writeAll("_config() {\n");
|
||||
try writer.writeAll(" _arguments \\\n");
|
||||
try writer.writeAll(" \"--help\" \\\n");
|
||||
try writer.writeAll(" \"--version\" \\\n");
|
||||
for (@typeInfo(Config).Struct.fields) |field| {
|
||||
if (field.name[0] == '_') continue;
|
||||
try writer.writeAll(" \"--");
|
||||
try writer.writeAll(field.name);
|
||||
try writer.writeAll("=-:::");
|
||||
|
||||
if (std.mem.startsWith(u8, field.name, "font-family"))
|
||||
try writer.writeAll("_fonts")
|
||||
else if (std.mem.eql(u8, "theme", field.name))
|
||||
try writer.writeAll("_themes")
|
||||
else if (std.mem.eql(u8, "working-directory", field.name))
|
||||
try writer.writeAll("{_files -/}")
|
||||
else if (field.type == Config.RepeatablePath)
|
||||
try writer.writeAll("_files") // todo check if this is needed
|
||||
else {
|
||||
try writer.writeAll("(");
|
||||
switch (@typeInfo(field.type)) {
|
||||
.Bool => try writer.writeAll("true false"),
|
||||
.Enum => |info| {
|
||||
for (info.fields, 0..) |f, i| {
|
||||
if (i > 0) try writer.writeAll(" ");
|
||||
try writer.writeAll(f.name);
|
||||
}
|
||||
},
|
||||
.Struct => |info| {
|
||||
if (!@hasDecl(field.type, "parseCLI") and info.layout == .@"packed") {
|
||||
for (info.fields, 0..) |f, i| {
|
||||
if (i > 0) try writer.writeAll(" ");
|
||||
try writer.writeAll(f.name);
|
||||
try writer.writeAll(" no-");
|
||||
try writer.writeAll(f.name);
|
||||
}
|
||||
} else {
|
||||
//resize-overlay-duration
|
||||
//keybind
|
||||
//window-padding-x ...-y
|
||||
//link
|
||||
//palette
|
||||
//background
|
||||
//foreground
|
||||
//font-variation*
|
||||
//font-feature
|
||||
try writer.writeAll(" ");
|
||||
}
|
||||
},
|
||||
else => try writer.writeAll(" "),
|
||||
}
|
||||
try writer.writeAll(")");
|
||||
}
|
||||
|
||||
try writer.writeAll("\" \\\n");
|
||||
}
|
||||
try writer.writeAll("\n}\n\n");
|
||||
|
||||
try writer.writeAll(
|
||||
\\_ghostty() {
|
||||
\\ typeset -A opt_args
|
||||
\\ local context state line
|
||||
\\ local opt=('-e' '--help' '--version')
|
||||
\\
|
||||
\\ _arguments -C \
|
||||
\\ '1:actions:->actions' \
|
||||
\\ '*:: :->rest' \
|
||||
\\
|
||||
\\ if [[ "$line[1]" == "--help" || "$line[1]" == "--version" || "$line[1]" == "-e" ]]; then
|
||||
\\ return
|
||||
\\ fi
|
||||
\\
|
||||
\\ if [[ "$line[1]" == -* ]]; then
|
||||
\\ _config
|
||||
\\ return
|
||||
\\ fi
|
||||
\\
|
||||
\\ case "$state" in
|
||||
\\ (actions)
|
||||
\\ local actions; actions=(
|
||||
\\
|
||||
);
|
||||
|
||||
{
|
||||
// how to get 'commands'
|
||||
var count: usize = 0;
|
||||
const padding = " ";
|
||||
for (@typeInfo(Action).Enum.fields) |field| {
|
||||
if (std.mem.eql(u8, "help", field.name)) continue;
|
||||
if (std.mem.eql(u8, "version", field.name)) continue;
|
||||
|
||||
try writer.writeAll(padding ++ "'+");
|
||||
try writer.writeAll(field.name);
|
||||
try writer.writeAll("'\n");
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
try writer.writeAll(
|
||||
\\ )
|
||||
\\ _describe '' opt
|
||||
\\ _describe -t action 'action' actions
|
||||
\\ ;;
|
||||
\\ (rest)
|
||||
\\ if [[ "$line[2]" == "--help" ]]; then
|
||||
\\ return
|
||||
\\ fi
|
||||
\\
|
||||
\\ local help=('--help')
|
||||
\\ _describe '' help
|
||||
\\
|
||||
\\ case $line[1] in
|
||||
\\
|
||||
);
|
||||
{
|
||||
const padding = " ";
|
||||
for (@typeInfo(Action).Enum.fields) |field| {
|
||||
if (std.mem.eql(u8, "help", field.name)) continue;
|
||||
if (std.mem.eql(u8, "version", field.name)) continue;
|
||||
|
||||
const options = @field(Action, field.name).options();
|
||||
// assumes options will never be created with only <_name> members
|
||||
if (@typeInfo(options).Struct.fields.len == 0) continue;
|
||||
|
||||
try writer.writeAll(padding ++ "(+" ++ field.name ++ ")\n");
|
||||
try writer.writeAll(padding ++ " _arguments \\\n");
|
||||
for (@typeInfo(options).Struct.fields) |opt| {
|
||||
if (opt.name[0] == '_') continue;
|
||||
|
||||
try writer.writeAll(padding ++ " '--");
|
||||
try writer.writeAll(opt.name);
|
||||
try writer.writeAll("=-:::");
|
||||
switch (@typeInfo(opt.type)) {
|
||||
.Bool => try writer.writeAll("(true false)"),
|
||||
.Enum => |info| {
|
||||
try writer.writeAll("(");
|
||||
for (info.opts, 0..) |f, i| {
|
||||
if (i > 0) try writer.writeAll(" ");
|
||||
try writer.writeAll(f.name);
|
||||
}
|
||||
try writer.writeAll(")");
|
||||
},
|
||||
else => {
|
||||
if (std.mem.eql(u8, "config-file", opt.name)) {
|
||||
try writer.writeAll("_files");
|
||||
} else try writer.writeAll("( )");
|
||||
},
|
||||
}
|
||||
try writer.writeAll("' \\\n");
|
||||
}
|
||||
try writer.writeAll(padding ++ ";;\n");
|
||||
}
|
||||
}
|
||||
try writer.writeAll(
|
||||
\\ esac
|
||||
\\ ;;
|
||||
\\ esac
|
||||
\\}
|
||||
\\
|
||||
\\_ghostty "$@"
|
||||
\\
|
||||
);
|
||||
}
|
@ -163,6 +163,26 @@ pub const Action = enum {
|
||||
return "cli/" ++ filename ++ ".zig";
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the options of action. Supports generating shell completions
|
||||
/// without duplicating the mapping from Action to relevant Option
|
||||
/// @import(..) declaration.
|
||||
pub fn options(comptime self: Action) type {
|
||||
comptime {
|
||||
return switch (self) {
|
||||
.version => version.Options,
|
||||
.help => help.Options,
|
||||
.@"list-fonts" => list_fonts.Options,
|
||||
.@"list-keybinds" => list_keybinds.Options,
|
||||
.@"list-themes" => list_themes.Options,
|
||||
.@"list-colors" => list_colors.Options,
|
||||
.@"list-actions" => list_actions.Options,
|
||||
.@"show-config" => show_config.Options,
|
||||
.@"validate-config" => validate_config.Options,
|
||||
.@"crash-report" => crash_report.Options,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "parse action none" {
|
||||
|
@ -7,7 +7,7 @@ const font = @import("../font/main.zig");
|
||||
|
||||
const log = std.log.scoped(.list_fonts);
|
||||
|
||||
pub const Config = struct {
|
||||
pub const Options = struct {
|
||||
/// This is set by the CLI parser for deinit.
|
||||
_arena: ?ArenaAllocator = null,
|
||||
|
||||
@ -23,13 +23,13 @@ pub const Config = struct {
|
||||
bold: bool = false,
|
||||
italic: bool = false,
|
||||
|
||||
pub fn deinit(self: *Config) void {
|
||||
pub fn deinit(self: *Options) void {
|
||||
if (self._arena) |arena| arena.deinit();
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
/// Enables "-h" and "--help" to work.
|
||||
pub fn help(self: Config) !void {
|
||||
pub fn help(self: Options) !void {
|
||||
_ = self;
|
||||
return Action.help_error;
|
||||
}
|
||||
@ -59,9 +59,9 @@ pub fn run(alloc: Allocator) !u8 {
|
||||
}
|
||||
|
||||
fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 {
|
||||
var config: Config = .{};
|
||||
var config: Options = .{};
|
||||
defer config.deinit();
|
||||
try args.parse(Config, alloc_gpa, &config, argsIter);
|
||||
try args.parse(Options, alloc_gpa, &config, argsIter);
|
||||
|
||||
// Use an arena for all our memory allocs
|
||||
var arena = ArenaAllocator.init(alloc_gpa);
|
||||
|
@ -7,6 +7,8 @@ const xev = @import("xev");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const gtk = if (build_config.app_runtime == .gtk) @import("../apprt/gtk/c.zig").c else void;
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
/// The `version` command is used to display information about Ghostty.
|
||||
pub fn run(alloc: Allocator) !u8 {
|
||||
_ = alloc;
|
||||
|
@ -314,7 +314,7 @@ const c = @cImport({
|
||||
|
||||
/// A theme to use. This can be a built-in theme name, a custom theme
|
||||
/// name, or an absolute path to a custom theme file. Ghostty also supports
|
||||
/// specifying a different them to use for light and dark mode. Each
|
||||
/// specifying a different theme to use for light and dark mode. Each
|
||||
/// option is documented below.
|
||||
///
|
||||
/// If the theme is an absolute pathname, Ghostty will attempt to load that
|
||||
@ -1574,20 +1574,41 @@ keybind: Keybinds = .{},
|
||||
/// editor, etc.
|
||||
@"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .visible,
|
||||
|
||||
/// If `true`, the *Option* key will be treated as *Alt*. This makes terminal
|
||||
/// sequences expecting *Alt* to work properly, but will break Unicode input
|
||||
/// sequences on macOS if you use them via the *Alt* key. You may set this to
|
||||
/// `false` to restore the macOS *Alt* key unicode sequences but this will break
|
||||
/// terminal sequences expecting *Alt* to work.
|
||||
/// macOS doesn't have a distinct "alt" key and instead has the "option"
|
||||
/// key which behaves slightly differently. On macOS by default, the
|
||||
/// option key plus a character will sometimes produces a Unicode character.
|
||||
/// For example, on US standard layouts option-b produces "∫". This may be
|
||||
/// undesirable if you want to use "option" as an "alt" key for keybindings
|
||||
/// in terminal programs or shells.
|
||||
///
|
||||
/// The values `left` or `right` enable this for the left or right *Option*
|
||||
/// key, respectively.
|
||||
/// This configuration lets you change the behavior so that option is treated
|
||||
/// as alt.
|
||||
///
|
||||
/// The default behavior (unset) will depend on your active keyboard
|
||||
/// layout. If your keyboard layout is one of the keyboard layouts listed
|
||||
/// below, then the default value is "true". Otherwise, the default
|
||||
/// value is "false". Keyboard layouts with a default value of "true" are:
|
||||
///
|
||||
/// - U.S. Standard
|
||||
/// - U.S. International
|
||||
///
|
||||
/// Note that if an *Option*-sequence doesn't produce a printable character, it
|
||||
/// will be treated as *Alt* regardless of this setting. (i.e. `alt+ctrl+a`).
|
||||
///
|
||||
/// Explicit values that can be set:
|
||||
///
|
||||
/// If `true`, the *Option* key will be treated as *Alt*. This makes terminal
|
||||
/// sequences expecting *Alt* to work properly, but will break Unicode input
|
||||
/// sequences on macOS if you use them via the *Alt* key.
|
||||
///
|
||||
/// You may set this to `false` to restore the macOS *Alt* key unicode
|
||||
/// sequences but this will break terminal sequences expecting *Alt* to work.
|
||||
///
|
||||
/// The values `left` or `right` enable this for the left or right *Option*
|
||||
/// key, respectively.
|
||||
///
|
||||
/// This does not work with GLFW builds.
|
||||
@"macos-option-as-alt": OptionAsAlt = .false,
|
||||
@"macos-option-as-alt": ?OptionAsAlt = null,
|
||||
|
||||
/// Whether to enable the macOS window shadow. The default value is true.
|
||||
/// With some window managers and window transparency settings, you may
|
||||
@ -4227,14 +4248,9 @@ pub const Keybinds = struct {
|
||||
}
|
||||
}
|
||||
|
||||
try formatter.formatEntry(
|
||||
[]const u8,
|
||||
std.fmt.bufPrint(
|
||||
&buf,
|
||||
"{}{}",
|
||||
.{ k, v },
|
||||
) catch return error.OutOfMemory,
|
||||
);
|
||||
var buffer_stream = std.io.fixedBufferStream(&buf);
|
||||
std.fmt.format(buffer_stream.writer(), "{}", .{k}) catch return error.OutOfMemory;
|
||||
try v.formatEntries(&buffer_stream, formatter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4268,6 +4284,56 @@ pub const Keybinds = struct {
|
||||
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
try std.testing.expectEqualSlices(u8, "a = shift+a=csi:hello\n", buf.items);
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/ghostty-org/ghostty/issues/2734
|
||||
test "formatConfig multiple items" {
|
||||
const testing = std.testing;
|
||||
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var list: Keybinds = .{};
|
||||
try list.parseCLI(alloc, "ctrl+z>1=goto_tab:1");
|
||||
try list.parseCLI(alloc, "ctrl+z>2=goto_tab:2");
|
||||
try list.formatEntry(formatterpkg.entryFormatter("keybind", buf.writer()));
|
||||
|
||||
const want =
|
||||
\\keybind = ctrl+z>1=goto_tab:1
|
||||
\\keybind = ctrl+z>2=goto_tab:2
|
||||
\\
|
||||
;
|
||||
try std.testing.expectEqualStrings(want, buf.items);
|
||||
}
|
||||
|
||||
test "formatConfig multiple items nested" {
|
||||
const testing = std.testing;
|
||||
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var list: Keybinds = .{};
|
||||
try list.parseCLI(alloc, "ctrl+a>ctrl+b>n=new_window");
|
||||
try list.parseCLI(alloc, "ctrl+a>ctrl+b>w=close_window");
|
||||
try list.parseCLI(alloc, "ctrl+a>ctrl+c>t=new_tab");
|
||||
try list.parseCLI(alloc, "ctrl+b>ctrl+d>a=previous_tab");
|
||||
try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer()));
|
||||
|
||||
// NB: This does not currently retain the order of the keybinds.
|
||||
const want =
|
||||
\\a = ctrl+a>ctrl+b>w=close_window
|
||||
\\a = ctrl+a>ctrl+b>n=new_window
|
||||
\\a = ctrl+a>ctrl+c>t=new_tab
|
||||
\\a = ctrl+b>ctrl+d>a=previous_tab
|
||||
\\
|
||||
;
|
||||
try std.testing.expectEqualStrings(want, buf.items);
|
||||
}
|
||||
};
|
||||
|
||||
/// See "font-codepoint-map" for documentation.
|
||||
|
@ -520,7 +520,14 @@ test "getIndex box glyph" {
|
||||
|
||||
var r: CodepointResolver = .{
|
||||
.collection = c,
|
||||
.sprite = .{ .width = 18, .height = 36, .thickness = 2 },
|
||||
.sprite = .{
|
||||
.metrics = font.Metrics.calc(.{
|
||||
.cell_width = 18.0,
|
||||
.ascent = 30.0,
|
||||
.descent = -6.0,
|
||||
.line_gap = 0.0,
|
||||
}),
|
||||
},
|
||||
};
|
||||
defer r.deinit(alloc);
|
||||
|
||||
|
@ -122,13 +122,7 @@ fn reloadMetrics(self: *SharedGrid) !void {
|
||||
self.metrics = face.metrics;
|
||||
|
||||
// Setup our sprite font.
|
||||
self.resolver.sprite = .{
|
||||
.width = self.metrics.cell_width,
|
||||
.height = self.metrics.cell_height,
|
||||
.thickness = self.metrics.underline_thickness,
|
||||
.underline_position = self.metrics.underline_position,
|
||||
.strikethrough_position = self.metrics.strikethrough_position,
|
||||
};
|
||||
self.resolver.sprite = .{ .metrics = self.metrics };
|
||||
}
|
||||
|
||||
/// Returns the grid cell size.
|
||||
|
@ -6,21 +6,28 @@ const std = @import("std");
|
||||
cell_width: u32,
|
||||
cell_height: u32,
|
||||
|
||||
/// For monospace grids, the recommended y-value from the bottom to set
|
||||
/// the baseline for font rendering. This is chosen so that things such
|
||||
/// as the bottom of a "g" or "y" do not drop below the cell.
|
||||
/// Distance in pixels from the bottom of the cell to the text baseline.
|
||||
cell_baseline: u32,
|
||||
|
||||
/// The position of the underline from the top of the cell and the
|
||||
/// thickness in pixels.
|
||||
/// Distance in pixels from the top of the cell to the top of the underline.
|
||||
underline_position: u32,
|
||||
/// Thickness in pixels of the underline.
|
||||
underline_thickness: u32,
|
||||
|
||||
/// The position and thickness of a strikethrough. Same units/style
|
||||
/// as the underline fields.
|
||||
/// Distance in pixels from the top of the cell to the top of the strikethrough.
|
||||
strikethrough_position: u32,
|
||||
/// Thickness in pixels of the strikethrough.
|
||||
strikethrough_thickness: u32,
|
||||
|
||||
/// Distance in pixels from the top of the cell to the top of the overline.
|
||||
/// Can be negative to adjust the position above the top of the cell.
|
||||
overline_position: i32,
|
||||
/// Thickness in pixels of the overline.
|
||||
overline_thickness: u32,
|
||||
|
||||
/// Thickness in pixels of box drawing characters.
|
||||
box_thickness: u32,
|
||||
|
||||
/// The thickness in pixels of the cursor sprite. This has a default value
|
||||
/// because it is not determined by fonts but rather by user configuration.
|
||||
cursor_thickness: u32 = 1,
|
||||
@ -30,6 +37,146 @@ cursor_thickness: u32 = 1,
|
||||
original_cell_width: ?u32 = null,
|
||||
original_cell_height: ?u32 = null,
|
||||
|
||||
/// Minimum acceptable values for some fields to prevent modifiers
|
||||
/// from being able to, for example, cause 0-thickness underlines.
|
||||
const Minimums = struct {
|
||||
const cell_width = 1;
|
||||
const cell_height = 1;
|
||||
const underline_thickness = 1;
|
||||
const strikethrough_thickness = 1;
|
||||
const overline_thickness = 1;
|
||||
const box_thickness = 1;
|
||||
const cursor_thickness = 1;
|
||||
};
|
||||
|
||||
const CalcOpts = struct {
|
||||
cell_width: f64,
|
||||
|
||||
/// The typographic ascent metric from the font.
|
||||
/// This represents the maximum vertical position of the highest ascender.
|
||||
///
|
||||
/// Relative to the baseline, in px, +Y=up
|
||||
ascent: f64,
|
||||
|
||||
/// The typographic descent metric from the font.
|
||||
/// This represents the minimum vertical position of the lowest descender.
|
||||
///
|
||||
/// Relative to the baseline, in px, +Y=up
|
||||
///
|
||||
/// Note:
|
||||
/// As this value is generally below the baseline, it is typically negative.
|
||||
descent: f64,
|
||||
|
||||
/// The typographic line gap (aka "leading") metric from the font.
|
||||
/// This represents the additional space to be added between lines in
|
||||
/// addition to the space defined by the ascent and descent metrics.
|
||||
///
|
||||
/// Positive value in px
|
||||
line_gap: f64,
|
||||
|
||||
/// The TOP of the underline stroke.
|
||||
///
|
||||
/// Relative to the baseline, in px, +Y=up
|
||||
underline_position: ?f64 = null,
|
||||
|
||||
/// The thickness of the underline stroke in px.
|
||||
underline_thickness: ?f64 = null,
|
||||
|
||||
/// The TOP of the strikethrough stroke.
|
||||
///
|
||||
/// Relative to the baseline, in px, +Y=up
|
||||
strikethrough_position: ?f64 = null,
|
||||
|
||||
/// The thickness of the strikethrough stroke in px.
|
||||
strikethrough_thickness: ?f64 = null,
|
||||
|
||||
/// The height of capital letters in the font, either derived from
|
||||
/// a provided cap height metric or measured from the height of the
|
||||
/// capital H glyph.
|
||||
cap_height: ?f64 = null,
|
||||
|
||||
/// The height of lowercase letters in the font, either derived from
|
||||
/// a provided ex height metric or measured from the height of the
|
||||
/// lowercase x glyph.
|
||||
ex_height: ?f64 = null,
|
||||
};
|
||||
|
||||
/// Calculate our metrics based on values extracted from a font.
|
||||
///
|
||||
/// Try to pass values with as much precision as possible,
|
||||
/// do not round them before using them for this function.
|
||||
///
|
||||
/// For any nullable options that are not provided, estimates will be used.
|
||||
pub fn calc(opts: CalcOpts) Metrics {
|
||||
// We use the ceiling of the provided cell width and height to ensure
|
||||
// that the cell is large enough for the provided size, since we cast
|
||||
// it to an integer later.
|
||||
const cell_width = @ceil(opts.cell_width);
|
||||
const cell_height = @ceil(opts.ascent - opts.descent + opts.line_gap);
|
||||
|
||||
// We split our line gap in two parts, and put half of it on the top
|
||||
// of the cell and the other half on the bottom, so that our text never
|
||||
// bumps up against either edge of the cell vertically.
|
||||
const half_line_gap = opts.line_gap / 2;
|
||||
|
||||
// Unlike all our other metrics, `cell_baseline` is relative to the
|
||||
// BOTTOM of the cell.
|
||||
const cell_baseline = @round(half_line_gap - opts.descent);
|
||||
|
||||
// We calculate a top_to_baseline to make following calculations simpler.
|
||||
const top_to_baseline = cell_height - cell_baseline;
|
||||
|
||||
// If we don't have a provided cap height,
|
||||
// we estimate it as 75% of the ascent.
|
||||
const cap_height = opts.cap_height orelse opts.ascent * 0.75;
|
||||
|
||||
// If we don't have a provided ex height,
|
||||
// we estimate it as 75% of the cap height.
|
||||
const ex_height = opts.ex_height orelse cap_height * 0.75;
|
||||
|
||||
// If we don't have a provided underline thickness,
|
||||
// we estimate it as 15% of the ex height.
|
||||
const underline_thickness = @max(1, @ceil(opts.underline_thickness orelse 0.15 * ex_height));
|
||||
|
||||
// If we don't have a provided strikethrough thickness
|
||||
// then we just use the underline thickness for it.
|
||||
const strikethrough_thickness = @max(1, @ceil(opts.strikethrough_thickness orelse underline_thickness));
|
||||
|
||||
// If we don't have a provided underline position then
|
||||
// we place it 1 underline-thickness below the baseline.
|
||||
const underline_position = @round(top_to_baseline -
|
||||
(opts.underline_position orelse
|
||||
-underline_thickness));
|
||||
|
||||
// If we don't have a provided strikethrough position
|
||||
// then we center the strikethrough stroke at half the
|
||||
// ex height, so that it's perfectly centered on lower
|
||||
// case text.
|
||||
const strikethrough_position = @round(top_to_baseline -
|
||||
(opts.strikethrough_position orelse
|
||||
ex_height * 0.5 + strikethrough_thickness * 0.5));
|
||||
|
||||
var result: Metrics = .{
|
||||
.cell_width = @intFromFloat(cell_width),
|
||||
.cell_height = @intFromFloat(cell_height),
|
||||
.cell_baseline = @intFromFloat(cell_baseline),
|
||||
.underline_position = @intFromFloat(underline_position),
|
||||
.underline_thickness = @intFromFloat(underline_thickness),
|
||||
.strikethrough_position = @intFromFloat(strikethrough_position),
|
||||
.strikethrough_thickness = @intFromFloat(strikethrough_thickness),
|
||||
.overline_position = 0,
|
||||
.overline_thickness = @intFromFloat(underline_thickness),
|
||||
.box_thickness = @intFromFloat(underline_thickness),
|
||||
};
|
||||
|
||||
// Ensure all metrics are within their allowable range.
|
||||
result.clamp();
|
||||
|
||||
// std.log.debug("metrics={}", .{result});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Apply a set of modifiers.
|
||||
pub fn apply(self: *Metrics, mods: ModifierSet) void {
|
||||
var it = mods.iterator();
|
||||
@ -80,6 +227,21 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent modifiers from pushing metrics out of their allowable range.
|
||||
self.clamp();
|
||||
}
|
||||
|
||||
/// Clamp all metrics to their allowable range.
|
||||
fn clamp(self: *Metrics) void {
|
||||
inline for (std.meta.fields(Metrics)) |field| {
|
||||
if (@hasDecl(Minimums, field.name)) {
|
||||
@field(self, field.name) = @max(
|
||||
@field(self, field.name),
|
||||
@field(Minimums, field.name),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of modifiers to apply to metrics. We use a hash map because
|
||||
@ -152,23 +314,26 @@ pub const Modifier = union(enum) {
|
||||
}
|
||||
|
||||
/// Apply a modifier to a numeric value.
|
||||
pub fn apply(self: Modifier, v: u32) u32 {
|
||||
pub fn apply(self: Modifier, v: anytype) @TypeOf(v) {
|
||||
const T = @TypeOf(v);
|
||||
const signed = @typeInfo(T).Int.signedness == .signed;
|
||||
return switch (self) {
|
||||
.percent => |p| percent: {
|
||||
const p_clamped: f64 = @max(0, p);
|
||||
const v_f64: f64 = @floatFromInt(v);
|
||||
const applied_f64: f64 = @round(v_f64 * p_clamped);
|
||||
const applied_u32: u32 = @intFromFloat(applied_f64);
|
||||
break :percent applied_u32;
|
||||
const applied_T: T = @intFromFloat(applied_f64);
|
||||
break :percent applied_T;
|
||||
},
|
||||
|
||||
.absolute => |abs| absolute: {
|
||||
const v_i64: i64 = @intCast(v);
|
||||
const abs_i64: i64 = @intCast(abs);
|
||||
const applied_i64: i64 = @max(0, v_i64 +| abs_i64);
|
||||
const applied_u32: u32 = std.math.cast(u32, applied_i64) orelse
|
||||
std.math.maxInt(u32);
|
||||
break :absolute applied_u32;
|
||||
const applied_i64: i64 = v_i64 +| abs_i64;
|
||||
const clamped_i64: i64 = if (signed) applied_i64 else @max(0, applied_i64);
|
||||
const applied_T: T = std.math.cast(T, clamped_i64) orelse
|
||||
std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64)));
|
||||
break :absolute applied_T;
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -215,7 +380,7 @@ pub const Key = key: {
|
||||
var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined;
|
||||
var count: usize = 0;
|
||||
for (field_infos, 0..) |field, i| {
|
||||
if (field.type != u32) continue;
|
||||
if (field.type != u32 and field.type != i32) continue;
|
||||
enumFields[i] = .{ .name = field.name, .value = i };
|
||||
count += 1;
|
||||
}
|
||||
@ -242,6 +407,9 @@ fn init() Metrics {
|
||||
.underline_thickness = 0,
|
||||
.strikethrough_position = 0,
|
||||
.strikethrough_thickness = 0,
|
||||
.overline_position = 0,
|
||||
.overline_thickness = 0,
|
||||
.box_thickness = 0,
|
||||
};
|
||||
}
|
||||
|
||||
@ -337,12 +505,12 @@ test "Modifier: percent" {
|
||||
|
||||
{
|
||||
const m: Modifier = .{ .percent = 0.8 };
|
||||
const v: u32 = m.apply(100);
|
||||
const v: u32 = m.apply(@as(u32, 100));
|
||||
try testing.expectEqual(@as(u32, 80), v);
|
||||
}
|
||||
{
|
||||
const m: Modifier = .{ .percent = 1.8 };
|
||||
const v: u32 = m.apply(100);
|
||||
const v: u32 = m.apply(@as(u32, 100));
|
||||
try testing.expectEqual(@as(u32, 180), v);
|
||||
}
|
||||
}
|
||||
@ -352,17 +520,17 @@ test "Modifier: absolute" {
|
||||
|
||||
{
|
||||
const m: Modifier = .{ .absolute = -100 };
|
||||
const v: u32 = m.apply(100);
|
||||
const v: u32 = m.apply(@as(u32, 100));
|
||||
try testing.expectEqual(@as(u32, 0), v);
|
||||
}
|
||||
{
|
||||
const m: Modifier = .{ .absolute = -120 };
|
||||
const v: u32 = m.apply(100);
|
||||
const v: u32 = m.apply(@as(u32, 100));
|
||||
try testing.expectEqual(@as(u32, 0), v);
|
||||
}
|
||||
{
|
||||
const m: Modifier = .{ .absolute = 100 };
|
||||
const v: u32 = m.apply(100);
|
||||
const v: u32 = m.apply(@as(u32, 100));
|
||||
try testing.expectEqual(@as(u32, 200), v);
|
||||
}
|
||||
}
|
||||
|
@ -55,12 +55,10 @@ pub const Face = struct {
|
||||
const data = try macos.foundation.Data.createWithBytesNoCopy(source);
|
||||
defer data.release();
|
||||
|
||||
const arr = macos.text.createFontDescriptorsFromData(data) orelse
|
||||
const desc = macos.text.createFontDescriptorFromData(data) orelse
|
||||
return error.FontInitFailure;
|
||||
defer arr.release();
|
||||
if (arr.getCount() == 0) return error.FontInitFailure;
|
||||
defer desc.release();
|
||||
|
||||
const desc = arr.getValueAtIndex(macos.text.FontDescriptor, 0);
|
||||
const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12);
|
||||
defer ct_font.release();
|
||||
|
||||
@ -532,11 +530,177 @@ pub const Face = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn calcMetrics(ct_font: *macos.text.Font) !font.face.Metrics {
|
||||
const CalcMetricsError = error{
|
||||
CopyTableError,
|
||||
InvalidHeadTable,
|
||||
InvalidPostTable,
|
||||
InvalidOS2Table,
|
||||
OS2VersionNotSupported,
|
||||
InvalidHheaTable,
|
||||
};
|
||||
|
||||
fn calcMetrics(ct_font: *macos.text.Font) CalcMetricsError!font.face.Metrics {
|
||||
// Read the 'head' table out of the font data.
|
||||
const head: opentype.Head = head: {
|
||||
const tag = macos.text.FontTableTag.init("head");
|
||||
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
|
||||
defer data.release();
|
||||
const ptr = data.getPointer();
|
||||
const len = data.getLength();
|
||||
break :head opentype.Head.init(ptr[0..len]) catch |err| {
|
||||
return switch (err) {
|
||||
error.EndOfStream,
|
||||
=> error.InvalidHeadTable,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Read the 'post' table out of the font data.
|
||||
const post: opentype.Post = post: {
|
||||
const tag = macos.text.FontTableTag.init("post");
|
||||
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
|
||||
defer data.release();
|
||||
const ptr = data.getPointer();
|
||||
const len = data.getLength();
|
||||
break :post opentype.Post.init(ptr[0..len]) catch |err| {
|
||||
return switch (err) {
|
||||
error.EndOfStream => error.InvalidPostTable,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Read the 'OS/2' table out of the font data.
|
||||
const os2: opentype.OS2 = os2: {
|
||||
const tag = macos.text.FontTableTag.init("OS/2");
|
||||
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
|
||||
defer data.release();
|
||||
const ptr = data.getPointer();
|
||||
const len = data.getLength();
|
||||
break :os2 opentype.OS2.init(ptr[0..len]) catch |err| {
|
||||
return switch (err) {
|
||||
error.EndOfStream => error.InvalidOS2Table,
|
||||
error.OS2VersionNotSupported => error.OS2VersionNotSupported,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// Read the 'hhea' table out of the font data.
|
||||
const hhea: opentype.Hhea = hhea: {
|
||||
const tag = macos.text.FontTableTag.init("hhea");
|
||||
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
|
||||
defer data.release();
|
||||
const ptr = data.getPointer();
|
||||
const len = data.getLength();
|
||||
break :hhea opentype.Hhea.init(ptr[0..len]) catch |err| {
|
||||
return switch (err) {
|
||||
error.EndOfStream => error.InvalidHheaTable,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const units_per_em: f64 = @floatFromInt(head.unitsPerEm);
|
||||
const px_per_em: f64 = ct_font.getSize();
|
||||
const px_per_unit: f64 = px_per_em / units_per_em;
|
||||
|
||||
const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: {
|
||||
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
||||
const os2_descent: f64 = @floatFromInt(os2.sTypoDescender);
|
||||
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap);
|
||||
|
||||
// If the font says to use typo metrics, trust it.
|
||||
if (os2.fsSelection.use_typo_metrics) {
|
||||
break :vertical_metrics .{
|
||||
os2_ascent * px_per_unit,
|
||||
os2_descent * px_per_unit,
|
||||
os2_line_gap * px_per_unit,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise we prefer the height metrics from 'hhea' if they
|
||||
// are available, or else OS/2 sTypo* metrics, and if all else
|
||||
// fails then we use OS/2 usWin* metrics.
|
||||
//
|
||||
// This is not "standard" behavior, but it's our best bet to
|
||||
// account for fonts being... just weird. It's pretty much what
|
||||
// FreeType does to get its generic ascent and descent metrics.
|
||||
|
||||
if (hhea.ascender != 0 or hhea.descender != 0) {
|
||||
const hhea_ascent: f64 = @floatFromInt(hhea.ascender);
|
||||
const hhea_descent: f64 = @floatFromInt(hhea.descender);
|
||||
const hhea_line_gap: f64 = @floatFromInt(hhea.lineGap);
|
||||
break :vertical_metrics .{
|
||||
hhea_ascent * px_per_unit,
|
||||
hhea_descent * px_per_unit,
|
||||
hhea_line_gap * px_per_unit,
|
||||
};
|
||||
}
|
||||
|
||||
if (os2_ascent != 0 or os2_descent != 0) {
|
||||
break :vertical_metrics .{
|
||||
os2_ascent * px_per_unit,
|
||||
os2_descent * px_per_unit,
|
||||
os2_line_gap * px_per_unit,
|
||||
};
|
||||
}
|
||||
|
||||
const win_ascent: f64 = @floatFromInt(os2.usWinAscent);
|
||||
const win_descent: f64 = @floatFromInt(os2.usWinDescent);
|
||||
break :vertical_metrics .{
|
||||
win_ascent * px_per_unit,
|
||||
// usWinDescent is *positive* -> down unlike sTypoDescender
|
||||
// and hhea.Descender, so we flip its sign to fix this.
|
||||
-win_descent * px_per_unit,
|
||||
0.0,
|
||||
};
|
||||
};
|
||||
|
||||
// Some fonts have degenerate 'post' tables where the underline
|
||||
// thickness (and often position) are 0. We consider them null
|
||||
// if this is the case and use our own fallbacks when we calculate.
|
||||
const has_broken_underline = post.underlineThickness == 0;
|
||||
|
||||
// If the underline position isn't 0 then we do use it,
|
||||
// even if the thickness is't properly specified.
|
||||
const underline_position: ?f64 = if (has_broken_underline and post.underlinePosition == 0)
|
||||
null
|
||||
else
|
||||
@as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit;
|
||||
|
||||
const underline_thickness = if (has_broken_underline)
|
||||
null
|
||||
else
|
||||
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
|
||||
|
||||
// Similar logic to the underline above.
|
||||
const has_broken_strikethrough = os2.yStrikeoutSize == 0;
|
||||
|
||||
const strikethrough_position: ?f64 = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
|
||||
null
|
||||
else
|
||||
@as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
|
||||
|
||||
const strikethrough_thickness: ?f64 = if (has_broken_strikethrough)
|
||||
null
|
||||
else
|
||||
@as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit;
|
||||
|
||||
// We fall back to whatever CoreText does if
|
||||
// the OS/2 table doesn't specify a cap height.
|
||||
const cap_height: f64 = if (os2.sCapHeight) |sCapHeight|
|
||||
@as(f64, @floatFromInt(sCapHeight)) * px_per_unit
|
||||
else
|
||||
ct_font.getCapHeight();
|
||||
|
||||
// Ditto for ex height.
|
||||
const ex_height: f64 = if (os2.sxHeight) |sxHeight|
|
||||
@as(f64, @floatFromInt(sxHeight)) * px_per_unit
|
||||
else
|
||||
ct_font.getXHeight();
|
||||
|
||||
// Cell width is calculated by calculating the widest width of the
|
||||
// visible ASCII characters. Usually 'M' is widest but we just take
|
||||
// whatever is widest.
|
||||
const cell_width: f32 = cell_width: {
|
||||
const cell_width: f64 = cell_width: {
|
||||
// Build a comptime array of all the ASCII chars
|
||||
const unichars = comptime unichars: {
|
||||
const len = 127 - 32;
|
||||
@ -564,93 +728,29 @@ pub const Face = struct {
|
||||
max = @max(advances[i].width, max);
|
||||
}
|
||||
|
||||
break :cell_width @floatCast(@ceil(max));
|
||||
break :cell_width max;
|
||||
};
|
||||
|
||||
// Calculate the layout metrics for height/ascent by just asking
|
||||
// the font. I also tried Kitty's approach at one point which is to
|
||||
// use the CoreText layout engine but this led to some glyphs being
|
||||
// set incorrectly.
|
||||
const layout_metrics: struct {
|
||||
height: f32,
|
||||
ascent: f32,
|
||||
leading: f32,
|
||||
} = metrics: {
|
||||
const ascent = ct_font.getAscent();
|
||||
const descent = ct_font.getDescent();
|
||||
|
||||
// Leading is the value between lines at the TOP of a line.
|
||||
// Because we are rendering a fixed size terminal grid, we
|
||||
// want the leading to be split equally between the top and bottom.
|
||||
const leading = ct_font.getLeading();
|
||||
|
||||
// We ceil the metrics below because we don't want to cut off any
|
||||
// potential used pixels. This tends to only make a one pixel
|
||||
// difference but at small font sizes this can be noticeable.
|
||||
break :metrics .{
|
||||
.height = @floatCast(@ceil(ascent + descent + leading)),
|
||||
.ascent = @floatCast(@ceil(ascent + (leading / 2))),
|
||||
.leading = @floatCast(leading),
|
||||
};
|
||||
};
|
||||
|
||||
// All of these metrics are based on our layout above.
|
||||
const cell_height = @ceil(layout_metrics.height);
|
||||
const cell_baseline = @ceil(layout_metrics.height - layout_metrics.ascent);
|
||||
|
||||
const underline_thickness = @ceil(@as(f32, @floatCast(ct_font.getUnderlineThickness())));
|
||||
const strikethrough_thickness = underline_thickness;
|
||||
|
||||
const strikethrough_position = strikethrough_position: {
|
||||
// This is the height of lower case letters in our font.
|
||||
const ex_height = ct_font.getXHeight();
|
||||
|
||||
// We want to position the strikethrough so that it's
|
||||
// vertically centered on any lower case text. This is
|
||||
// a fairly standard choice for strikethrough positioning.
|
||||
//
|
||||
// Because our `strikethrough_position` is relative to the
|
||||
// top of the cell we start with the ascent metric, which
|
||||
// is the distance from the top down to the baseline, then
|
||||
// we subtract half of the ex height to go back up to the
|
||||
// correct height that should evenly split lowercase text.
|
||||
const pos = layout_metrics.ascent -
|
||||
ex_height * 0.5 -
|
||||
strikethrough_thickness * 0.5;
|
||||
|
||||
break :strikethrough_position @ceil(pos);
|
||||
};
|
||||
|
||||
// Underline position reported is usually something like "-1" to
|
||||
// represent the amount under the baseline. We add this to our real
|
||||
// baseline to get the actual value from the bottom (+y is up).
|
||||
// The final underline position is +y from the TOP (confusing)
|
||||
// so we have to subtract from the cell height.
|
||||
const underline_position = @ceil(layout_metrics.ascent -
|
||||
@as(f32, @floatCast(ct_font.getUnderlinePosition())));
|
||||
|
||||
// Note: is this useful?
|
||||
// const units_per_em = ct_font.getUnitsPerEm();
|
||||
// const units_per_point = @intToFloat(f64, units_per_em) / ct_font.getSize();
|
||||
|
||||
const result = font.face.Metrics{
|
||||
.cell_width = @intFromFloat(cell_width),
|
||||
.cell_height = @intFromFloat(cell_height),
|
||||
.cell_baseline = @intFromFloat(cell_baseline),
|
||||
.underline_position = @intFromFloat(underline_position),
|
||||
.underline_thickness = @intFromFloat(underline_thickness),
|
||||
.strikethrough_position = @intFromFloat(strikethrough_position),
|
||||
.strikethrough_thickness = @intFromFloat(strikethrough_thickness),
|
||||
};
|
||||
|
||||
// std.log.warn("font size size={d}", .{ct_font.getSize()});
|
||||
// std.log.warn("font metrics={}", .{result});
|
||||
|
||||
return result;
|
||||
return font.face.Metrics.calc(.{
|
||||
.cell_width = cell_width,
|
||||
.ascent = ascent,
|
||||
.descent = descent,
|
||||
.line_gap = line_gap,
|
||||
.underline_position = underline_position,
|
||||
.underline_thickness = underline_thickness,
|
||||
.strikethrough_position = strikethrough_position,
|
||||
.strikethrough_thickness = strikethrough_thickness,
|
||||
.cap_height = cap_height,
|
||||
.ex_height = ex_height,
|
||||
});
|
||||
}
|
||||
|
||||
/// Copy the font table data for the given tag.
|
||||
pub fn copyTable(self: Face, alloc: Allocator, tag: *const [4]u8) !?[]u8 {
|
||||
pub fn copyTable(
|
||||
self: Face,
|
||||
alloc: Allocator,
|
||||
tag: *const [4]u8,
|
||||
) Allocator.Error!?[]u8 {
|
||||
const data = self.font.copyTable(macos.text.FontTableTag.init(tag)) orelse
|
||||
return null;
|
||||
defer data.release();
|
||||
@ -678,7 +778,9 @@ const ColorState = struct {
|
||||
svg: ?opentype.SVG,
|
||||
svg_data: ?*macos.foundation.Data,
|
||||
|
||||
pub fn init(f: *macos.text.Font) !ColorState {
|
||||
pub const Error = error{InvalidSVGTable};
|
||||
|
||||
pub fn init(f: *macos.text.Font) Error!ColorState {
|
||||
// sbix is true if the table exists in the font data at all.
|
||||
// In the future we probably want to actually parse it and
|
||||
// check for glyphs.
|
||||
@ -699,8 +801,16 @@ const ColorState = struct {
|
||||
errdefer data.release();
|
||||
const ptr = data.getPointer();
|
||||
const len = data.getLength();
|
||||
const svg = opentype.SVG.init(ptr[0..len]) catch |err| {
|
||||
return switch (err) {
|
||||
error.EndOfStream,
|
||||
error.SVGVersionNotSupported,
|
||||
=> error.InvalidSVGTable,
|
||||
};
|
||||
};
|
||||
|
||||
break :svg .{
|
||||
.svg = try opentype.SVG.init(ptr[0..len]),
|
||||
.svg = svg,
|
||||
.data = data,
|
||||
};
|
||||
};
|
||||
@ -907,3 +1017,58 @@ test "glyphIndex colored vs text" {
|
||||
try testing.expect(face.isColorGlyph(glyph));
|
||||
}
|
||||
}
|
||||
|
||||
test "coretext: metrics" {
|
||||
const testFont = font.embedded.inconsolata;
|
||||
const alloc = std.testing.allocator;
|
||||
|
||||
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
|
||||
defer atlas.deinit(alloc);
|
||||
|
||||
var ct_font = try Face.init(
|
||||
undefined,
|
||||
testFont,
|
||||
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
|
||||
);
|
||||
defer ct_font.deinit();
|
||||
|
||||
try std.testing.expectEqual(font.face.Metrics{
|
||||
.cell_width = 8,
|
||||
// The cell height is 17 px because the calculation is
|
||||
//
|
||||
// ascender - descender + gap
|
||||
//
|
||||
// which, for inconsolata is
|
||||
//
|
||||
// 859 - -190 + 0
|
||||
//
|
||||
// font units, at 1000 units per em that works out to 1.049 em,
|
||||
// and 1em should be the point size * dpi scale, so 12 * (96/72)
|
||||
// which is 16, and 16 * 1.049 = 16.784, which finally is rounded
|
||||
// to 17.
|
||||
.cell_height = 17,
|
||||
.cell_baseline = 3,
|
||||
.underline_position = 17,
|
||||
.underline_thickness = 1,
|
||||
.strikethrough_position = 10,
|
||||
.strikethrough_thickness = 1,
|
||||
.overline_position = 0,
|
||||
.overline_thickness = 1,
|
||||
.box_thickness = 1,
|
||||
}, ct_font.metrics);
|
||||
|
||||
// Resize should change metrics
|
||||
try ct_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } });
|
||||
try std.testing.expectEqual(font.face.Metrics{
|
||||
.cell_width = 16,
|
||||
.cell_height = 34,
|
||||
.cell_baseline = 6,
|
||||
.underline_position = 34,
|
||||
.underline_thickness = 2,
|
||||
.strikethrough_position = 19,
|
||||
.strikethrough_thickness = 2,
|
||||
.overline_position = 0,
|
||||
.overline_thickness = 2,
|
||||
.box_thickness = 2,
|
||||
}, ct_font.metrics);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ const font = @import("../main.zig");
|
||||
const Glyph = font.Glyph;
|
||||
const Library = font.Library;
|
||||
const convert = @import("freetype_convert.zig");
|
||||
const opentype = @import("../opentype.zig");
|
||||
const fastmem = @import("../../fastmem.zig");
|
||||
const quirks = @import("../../quirks.zig");
|
||||
const config = @import("../../config.zig");
|
||||
@ -85,7 +86,7 @@ pub const Face = struct {
|
||||
.lib = lib.lib,
|
||||
.face = face,
|
||||
.hb_font = hb_font,
|
||||
.metrics = calcMetrics(face, opts.metric_modifiers),
|
||||
.metrics = try calcMetrics(face, opts.metric_modifiers),
|
||||
.load_flags = opts.freetype_load_flags,
|
||||
};
|
||||
result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result);
|
||||
@ -185,7 +186,7 @@ pub const Face = struct {
|
||||
/// for clearing any glyph caches, font atlas data, etc.
|
||||
pub fn setSize(self: *Face, opts: font.face.Options) !void {
|
||||
try setSize_(self.face, opts.size);
|
||||
self.metrics = calcMetrics(self.face, opts.metric_modifiers);
|
||||
self.metrics = try calcMetrics(self.face, opts.metric_modifiers);
|
||||
}
|
||||
|
||||
fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {
|
||||
@ -258,7 +259,7 @@ pub const Face = struct {
|
||||
try self.face.setVarDesignCoordinates(coords);
|
||||
|
||||
// We need to recalculate font metrics which may have changed.
|
||||
self.metrics = calcMetrics(self.face, opts.metric_modifiers);
|
||||
self.metrics = try calcMetrics(self.face, opts.metric_modifiers);
|
||||
}
|
||||
|
||||
/// Returns the glyph index for the given Unicode code point. If this
|
||||
@ -593,6 +594,15 @@ pub const Face = struct {
|
||||
return @floatFromInt(v >> 6);
|
||||
}
|
||||
|
||||
fn f26dot6ToF64(v: freetype.c.FT_F26Dot6) f64 {
|
||||
return @as(opentype.sfnt.F26Dot6, @bitCast(@as(u32, @intCast(v)))).to(f64);
|
||||
}
|
||||
|
||||
const CalcMetricsError = error{
|
||||
CopyTableError,
|
||||
MissingOS2Table,
|
||||
};
|
||||
|
||||
/// Calculate the metrics associated with a face. This is not public because
|
||||
/// the metrics are calculated for every face and cached since they're
|
||||
/// frequently required for renderers and take up next to little memory space
|
||||
@ -605,138 +615,195 @@ pub const Face = struct {
|
||||
fn calcMetrics(
|
||||
face: freetype.Face,
|
||||
modifiers: ?*const font.face.Metrics.ModifierSet,
|
||||
) font.face.Metrics {
|
||||
) CalcMetricsError!font.face.Metrics {
|
||||
const size_metrics = face.handle.*.size.*.metrics;
|
||||
|
||||
// Cell width is calculated by preferring to use 'M' as the width of a
|
||||
// cell since 'M' is generally the widest ASCII character. If loading 'M'
|
||||
// fails then we use the max advance of the font face size metrics.
|
||||
const cell_width: f32 = cell_width: {
|
||||
if (face.getCharIndex('M')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||
break :cell_width f26dot6ToFloat(face.handle.*.glyph.*.advance.x);
|
||||
} else |_| {
|
||||
// Ignore the error since we just fall back to max_advance below
|
||||
// This code relies on this assumption, and it should always be
|
||||
// true since we don't do any non-uniform scaling on the font ever.
|
||||
assert(size_metrics.x_ppem == size_metrics.y_ppem);
|
||||
|
||||
// Read the 'head' table out of the font data.
|
||||
const head = face.getSfntTable(.head) orelse return error.CopyTableError;
|
||||
|
||||
// Read the 'post' table out of the font data.
|
||||
const post = face.getSfntTable(.post) orelse return error.CopyTableError;
|
||||
|
||||
// Read the 'OS/2' table out of the font data.
|
||||
const os2 = face.getSfntTable(.os2) orelse return error.CopyTableError;
|
||||
|
||||
// Read the 'hhea' table out of the font data.
|
||||
const hhea = face.getSfntTable(.hhea) orelse return error.CopyTableError;
|
||||
|
||||
// Some fonts don't actually have an OS/2 table, which
|
||||
// we need in order to do the metrics calculations, in
|
||||
// such cases FreeType sets the version to 0xFFFF
|
||||
if (os2.version == 0xFFFF) return error.MissingOS2Table;
|
||||
|
||||
const units_per_em = head.Units_Per_EM;
|
||||
const px_per_em: f64 = @floatFromInt(size_metrics.y_ppem);
|
||||
const px_per_unit = px_per_em / @as(f64, @floatFromInt(units_per_em));
|
||||
|
||||
const ascent: f64, const descent: f64, const line_gap: f64 = vertical_metrics: {
|
||||
const os2_ascent: f64 = @floatFromInt(os2.sTypoAscender);
|
||||
const os2_descent: f64 = @floatFromInt(os2.sTypoDescender);
|
||||
const os2_line_gap: f64 = @floatFromInt(os2.sTypoLineGap);
|
||||
|
||||
// If the font says to use typo metrics, trust it.
|
||||
// (The USE_TYPO_METRICS bit is bit 7)
|
||||
if (os2.fsSelection & (1 << 7) != 0) {
|
||||
break :vertical_metrics .{
|
||||
os2_ascent * px_per_unit,
|
||||
os2_descent * px_per_unit,
|
||||
os2_line_gap * px_per_unit,
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise we prefer the height metrics from 'hhea' if they
|
||||
// are available, or else OS/2 sTypo* metrics, and if all else
|
||||
// fails then we use OS/2 usWin* metrics.
|
||||
//
|
||||
// This is not "standard" behavior, but it's our best bet to
|
||||
// account for fonts being... just weird. It's pretty much what
|
||||
// FreeType does to get its generic ascent and descent metrics.
|
||||
|
||||
if (hhea.Ascender != 0 or hhea.Descender != 0) {
|
||||
const hhea_ascent: f64 = @floatFromInt(hhea.Ascender);
|
||||
const hhea_descent: f64 = @floatFromInt(hhea.Descender);
|
||||
const hhea_line_gap: f64 = @floatFromInt(hhea.Line_Gap);
|
||||
break :vertical_metrics .{
|
||||
hhea_ascent * px_per_unit,
|
||||
hhea_descent * px_per_unit,
|
||||
hhea_line_gap * px_per_unit,
|
||||
};
|
||||
}
|
||||
|
||||
if (os2_ascent != 0 or os2_descent != 0) {
|
||||
break :vertical_metrics .{
|
||||
os2_ascent * px_per_unit,
|
||||
os2_descent * px_per_unit,
|
||||
os2_line_gap * px_per_unit,
|
||||
};
|
||||
}
|
||||
|
||||
const win_ascent: f64 = @floatFromInt(os2.usWinAscent);
|
||||
const win_descent: f64 = @floatFromInt(os2.usWinDescent);
|
||||
break :vertical_metrics .{
|
||||
win_ascent * px_per_unit,
|
||||
// usWinDescent is *positive* -> down unlike sTypoDescender
|
||||
// and hhea.Descender, so we flip its sign to fix this.
|
||||
-win_descent * px_per_unit,
|
||||
0.0,
|
||||
};
|
||||
};
|
||||
|
||||
// Some fonts have degenerate 'post' tables where the underline
|
||||
// thickness (and often position) are 0. We consider them null
|
||||
// if this is the case and use our own fallbacks when we calculate.
|
||||
const has_broken_underline = post.underlineThickness == 0;
|
||||
|
||||
// If the underline position isn't 0 then we do use it,
|
||||
// even if the thickness is't properly specified.
|
||||
const underline_position = if (has_broken_underline and post.underlinePosition == 0)
|
||||
null
|
||||
else
|
||||
@as(f64, @floatFromInt(post.underlinePosition)) * px_per_unit;
|
||||
|
||||
const underline_thickness = if (has_broken_underline)
|
||||
null
|
||||
else
|
||||
@as(f64, @floatFromInt(post.underlineThickness)) * px_per_unit;
|
||||
|
||||
// Similar logic to the underline above.
|
||||
const has_broken_strikethrough = os2.yStrikeoutSize == 0;
|
||||
|
||||
const strikethrough_position = if (has_broken_strikethrough and os2.yStrikeoutPosition == 0)
|
||||
null
|
||||
else
|
||||
@as(f64, @floatFromInt(os2.yStrikeoutPosition)) * px_per_unit;
|
||||
|
||||
const strikethrough_thickness = if (has_broken_strikethrough)
|
||||
null
|
||||
else
|
||||
@as(f64, @floatFromInt(os2.yStrikeoutSize)) * px_per_unit;
|
||||
|
||||
// Cell width is calculated by calculating the widest width of the
|
||||
// visible ASCII characters. Usually 'M' is widest but we just take
|
||||
// whatever is widest.
|
||||
//
|
||||
// If we fail to load any visible ASCII we just use max_advance from
|
||||
// the metrics provided by FreeType.
|
||||
const cell_width: f64 = cell_width: {
|
||||
var max: f64 = 0.0;
|
||||
var c: u8 = ' ';
|
||||
while (c < 127) : (c += 1) {
|
||||
if (face.getCharIndex(c)) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||
max = @max(
|
||||
f26dot6ToF64(face.handle.*.glyph.*.advance.x),
|
||||
max,
|
||||
);
|
||||
} else |_| {}
|
||||
}
|
||||
}
|
||||
|
||||
break :cell_width f26dot6ToFloat(size_metrics.max_advance);
|
||||
// If we couldn't get any widths, just use FreeType's max_advance.
|
||||
if (max == 0.0) {
|
||||
break :cell_width f26dot6ToF64(size_metrics.max_advance);
|
||||
}
|
||||
|
||||
break :cell_width max;
|
||||
};
|
||||
|
||||
// Ex height is calculated by measuring the height of the `x` glyph.
|
||||
// If that fails then we just pretend it's 65% of the ascent height.
|
||||
const ex_height: f32 = ex_height: {
|
||||
// The OS/2 table does not include sCapHeight or sxHeight in version 1.
|
||||
const has_os2_height_metrics = os2.version >= 2;
|
||||
|
||||
// We use the cap height specified by the font if it's
|
||||
// available, otherwise we try to measure the `H` glyph.
|
||||
const cap_height: ?f64 = cap_height: {
|
||||
if (has_os2_height_metrics) {
|
||||
break :cap_height @as(f64, @floatFromInt(os2.sCapHeight)) * px_per_unit;
|
||||
}
|
||||
if (face.getCharIndex('H')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||
break :cap_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
break :cap_height null;
|
||||
};
|
||||
|
||||
// We use the ex height specified by the font if it's
|
||||
// available, otherwise we try to measure the `x` glyph.
|
||||
const ex_height: ?f64 = ex_height: {
|
||||
if (has_os2_height_metrics) {
|
||||
break :ex_height @as(f64, @floatFromInt(os2.sxHeight)) * px_per_unit;
|
||||
}
|
||||
if (face.getCharIndex('x')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||
break :ex_height f26dot6ToFloat(face.handle.*.glyph.*.metrics.height);
|
||||
} else |_| {
|
||||
// Ignore the error since we just fall back to 65% of the ascent below
|
||||
}
|
||||
break :ex_height f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
break :ex_height f26dot6ToFloat(size_metrics.ascender) * 0.65;
|
||||
break :ex_height null;
|
||||
};
|
||||
|
||||
// Cell height is calculated as the maximum of multiple things in order
|
||||
// to handle edge cases in fonts: (1) the height as reported in metadata
|
||||
// by the font designer (2) the maximum glyph height as measured in the
|
||||
// font and (3) the height from the ascender to an underscore.
|
||||
const cell_height: f32 = cell_height: {
|
||||
// The height as reported by the font designer.
|
||||
const face_height = f26dot6ToFloat(size_metrics.height);
|
||||
var result = font.face.Metrics.calc(.{
|
||||
.cell_width = cell_width,
|
||||
|
||||
// The maximum height a glyph can take in the font
|
||||
const max_glyph_height = f26dot6ToFloat(size_metrics.ascender) -
|
||||
f26dot6ToFloat(size_metrics.descender);
|
||||
.ascent = ascent,
|
||||
.descent = descent,
|
||||
.line_gap = line_gap,
|
||||
|
||||
// The height of the underscore character
|
||||
const underscore_height = underscore: {
|
||||
if (face.getCharIndex('_')) |glyph_index| {
|
||||
if (face.loadGlyph(glyph_index, .{ .render = true })) {
|
||||
var res: f32 = f26dot6ToFloat(size_metrics.ascender);
|
||||
res -= @floatFromInt(face.handle.*.glyph.*.bitmap_top);
|
||||
res += @floatFromInt(face.handle.*.glyph.*.bitmap.rows);
|
||||
break :underscore res;
|
||||
} else |_| {
|
||||
// Ignore the error since we just fall back below
|
||||
}
|
||||
}
|
||||
.underline_position = underline_position,
|
||||
.underline_thickness = underline_thickness,
|
||||
|
||||
break :underscore 0;
|
||||
};
|
||||
.strikethrough_position = strikethrough_position,
|
||||
.strikethrough_thickness = strikethrough_thickness,
|
||||
|
||||
break :cell_height @max(
|
||||
face_height,
|
||||
@max(max_glyph_height, underscore_height),
|
||||
);
|
||||
};
|
||||
.cap_height = cap_height,
|
||||
.ex_height = ex_height,
|
||||
});
|
||||
|
||||
// The baseline is the descender amount for the font. This is the maximum
|
||||
// that a font may go down. We switch signs because our coordinate system
|
||||
// is reversed.
|
||||
const cell_baseline = -1 * f26dot6ToFloat(size_metrics.descender);
|
||||
|
||||
const underline_thickness = @max(@as(f32, 1), fontUnitsToPxY(
|
||||
face,
|
||||
face.handle.*.underline_thickness,
|
||||
));
|
||||
|
||||
// The underline position. This is a value from the top where the
|
||||
// underline should go.
|
||||
const underline_position: f32 = underline_pos: {
|
||||
// From the FreeType docs:
|
||||
// > `underline_position`
|
||||
// > The position, in font units, of the underline line for
|
||||
// > this face. It is the center of the underlining stem.
|
||||
|
||||
const declared_px = @as(f32, @floatFromInt(freetype.mulFix(
|
||||
face.handle.*.underline_position,
|
||||
@intCast(face.handle.*.size.*.metrics.y_scale),
|
||||
))) / 64;
|
||||
|
||||
// We use the declared underline position if its available.
|
||||
const declared = @ceil(cell_height - cell_baseline - declared_px - underline_thickness * 0.5);
|
||||
if (declared > 0)
|
||||
break :underline_pos declared;
|
||||
|
||||
// If we have no declared underline position, we go slightly under the
|
||||
// cell height (mainly: non-scalable fonts, i.e. emoji)
|
||||
break :underline_pos cell_height - 1;
|
||||
};
|
||||
|
||||
// The strikethrough position. We use the position provided by the
|
||||
// font if it exists otherwise we calculate a best guess.
|
||||
const strikethrough: struct {
|
||||
pos: f32,
|
||||
thickness: f32,
|
||||
} = if (face.getSfntTable(.os2)) |os2| st: {
|
||||
const thickness = @max(@as(f32, 1), fontUnitsToPxY(face, os2.yStrikeoutSize));
|
||||
|
||||
const pos = @as(f32, @floatFromInt(freetype.mulFix(
|
||||
os2.yStrikeoutPosition,
|
||||
@as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)),
|
||||
))) / 64;
|
||||
|
||||
break :st .{
|
||||
.pos = @ceil(cell_height - cell_baseline - pos),
|
||||
.thickness = thickness,
|
||||
};
|
||||
} else .{
|
||||
// Exactly 50% of the ex height so that our strikethrough is
|
||||
// centered through lowercase text. This is a common choice.
|
||||
.pos = @ceil(cell_height - cell_baseline - ex_height * 0.5 - underline_thickness * 0.5),
|
||||
.thickness = underline_thickness,
|
||||
};
|
||||
|
||||
var result = font.face.Metrics{
|
||||
.cell_width = @intFromFloat(cell_width),
|
||||
.cell_height = @intFromFloat(cell_height),
|
||||
.cell_baseline = @intFromFloat(cell_baseline),
|
||||
.underline_position = @intFromFloat(underline_position),
|
||||
.underline_thickness = @intFromFloat(underline_thickness),
|
||||
.strikethrough_position = @intFromFloat(strikethrough.pos),
|
||||
.strikethrough_thickness = @intFromFloat(strikethrough.thickness),
|
||||
};
|
||||
if (modifiers) |m| result.apply(m.*);
|
||||
|
||||
// std.log.warn("font metrics={}", .{result});
|
||||
@ -744,13 +811,6 @@ pub const Face = struct {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Convert freetype "font units" to pixels using the Y scale.
|
||||
fn fontUnitsToPxY(face: freetype.Face, x: i32) f32 {
|
||||
const mul = freetype.mulFix(x, @as(i32, @intCast(face.handle.*.size.*.metrics.y_scale)));
|
||||
const div = @as(f32, @floatFromInt(mul)) / 64;
|
||||
return @ceil(div);
|
||||
}
|
||||
|
||||
/// Copy the font table data for the given tag.
|
||||
pub fn copyTable(self: Face, alloc: Allocator, tag: *const [4]u8) !?[]u8 {
|
||||
return try self.face.loadSfntTable(alloc, freetype.Tag.init(tag));
|
||||
@ -828,6 +888,9 @@ test "color emoji" {
|
||||
.underline_thickness = 0,
|
||||
.strikethrough_position = 0,
|
||||
.strikethrough_thickness = 0,
|
||||
.overline_position = 0,
|
||||
.overline_thickness = 0,
|
||||
.box_thickness = 0,
|
||||
},
|
||||
});
|
||||
try testing.expectEqual(@as(u32, 24), glyph.height);
|
||||
@ -853,24 +916,42 @@ test "metrics" {
|
||||
|
||||
try testing.expectEqual(font.face.Metrics{
|
||||
.cell_width = 8,
|
||||
.cell_height = 1.8e1,
|
||||
.cell_baseline = 4,
|
||||
.underline_position = 18,
|
||||
// The cell height is 17 px because the calculation is
|
||||
//
|
||||
// ascender - descender + gap
|
||||
//
|
||||
// which, for inconsolata is
|
||||
//
|
||||
// 859 - -190 + 0
|
||||
//
|
||||
// font units, at 1000 units per em that works out to 1.049 em,
|
||||
// and 1em should be the point size * dpi scale, so 12 * (96/72)
|
||||
// which is 16, and 16 * 1.049 = 16.784, which finally is rounded
|
||||
// to 17.
|
||||
.cell_height = 17,
|
||||
.cell_baseline = 3,
|
||||
.underline_position = 17,
|
||||
.underline_thickness = 1,
|
||||
.strikethrough_position = 10,
|
||||
.strikethrough_thickness = 1,
|
||||
.overline_position = 0,
|
||||
.overline_thickness = 1,
|
||||
.box_thickness = 1,
|
||||
}, ft_font.metrics);
|
||||
|
||||
// Resize should change metrics
|
||||
try ft_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } });
|
||||
try testing.expectEqual(font.face.Metrics{
|
||||
.cell_width = 16,
|
||||
.cell_height = 35,
|
||||
.cell_baseline = 7,
|
||||
.underline_position = 35,
|
||||
.cell_height = 34,
|
||||
.cell_baseline = 6,
|
||||
.underline_position = 34,
|
||||
.underline_thickness = 2,
|
||||
.strikethrough_position = 20,
|
||||
.strikethrough_position = 19,
|
||||
.strikethrough_thickness = 2,
|
||||
.overline_position = 0,
|
||||
.overline_thickness = 2,
|
||||
.box_thickness = 2,
|
||||
}, ft_font.metrics);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,16 @@
|
||||
pub const sfnt = @import("opentype/sfnt.zig");
|
||||
|
||||
const svg = @import("opentype/svg.zig");
|
||||
const os2 = @import("opentype/os2.zig");
|
||||
const post = @import("opentype/post.zig");
|
||||
const hhea = @import("opentype/hhea.zig");
|
||||
const head = @import("opentype/head.zig");
|
||||
|
||||
pub const SVG = svg.SVG;
|
||||
pub const OS2 = os2.OS2;
|
||||
pub const Post = post.Post;
|
||||
pub const Hhea = hhea.Hhea;
|
||||
pub const Head = head.Head;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
180
src/font/opentype/head.zig
Normal file
180
src/font/opentype/head.zig
Normal file
@ -0,0 +1,180 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const sfnt = @import("sfnt.zig");
|
||||
|
||||
/// Font Header Table
|
||||
///
|
||||
/// References:
|
||||
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/head
|
||||
///
|
||||
/// Field names are in camelCase to match names in spec.
|
||||
pub const Head = extern struct {
|
||||
/// Major version number of the font header table — set to 1.
|
||||
majorVersion: sfnt.uint16 align(1),
|
||||
|
||||
/// Minor version number of the font header table — set to 0.
|
||||
minorVersion: sfnt.uint16 align(1),
|
||||
|
||||
/// Set by font manufacturer.
|
||||
fontRevision: sfnt.Fixed align(1),
|
||||
|
||||
/// To compute: set it to 0, sum the entire font as uint32, then store
|
||||
/// 0xB1B0AFBA - sum. If the font is used as a component in a font
|
||||
/// collection file, the value of this field will be invalidated by
|
||||
/// changes to the file structure and font table directory, and must
|
||||
/// be ignored.
|
||||
checksumAdjustment: sfnt.uint32 align(1),
|
||||
|
||||
/// Set to 0x5F0F3CF5.
|
||||
magicNumber: sfnt.uint32 align(1),
|
||||
|
||||
/// Bit 0: Baseline for font at y=0.
|
||||
///
|
||||
/// Bit 1: Left sidebearing point at x=0
|
||||
/// (relevant only for TrueType rasterizers)
|
||||
///
|
||||
/// Bit 2: Instructions may depend on point size.
|
||||
///
|
||||
/// Bit 3: Force ppem to integer values for all internal scaler math; may
|
||||
/// use fractional ppem sizes if this bit is clear. It is strongly
|
||||
/// recommended that this be set in hinted fonts.
|
||||
///
|
||||
/// Bit 4: Instructions may alter advance width
|
||||
/// (the advance widths might not scale linearly).
|
||||
///
|
||||
/// Bit 5: This bit is not used in OpenType, and should not be set in order
|
||||
/// to ensure compatible behavior on all platforms. If set, it may
|
||||
/// result in different behavior for vertical layout in some
|
||||
/// platforms.
|
||||
///
|
||||
/// (See Apple’s specification for details
|
||||
/// regarding behavior in Apple platforms.)
|
||||
///
|
||||
/// Bits 6 – 10: These bits are not used in OpenType and should always be
|
||||
/// cleared.
|
||||
///
|
||||
/// (See Apple’s specification for details
|
||||
/// regarding legacy use in Apple platforms.)
|
||||
///
|
||||
/// Bit 11: Font data is “lossless” as a result of having been
|
||||
/// subjected to optimizing transformation and/or compression
|
||||
/// (such as compression mechanisms defined by ISO/IEC 14496-18,
|
||||
/// MicroType® Express, WOFF 2.0, or similar) where the original
|
||||
/// font functionality and features are retained but the binary
|
||||
/// compatibility between input and output font files is not
|
||||
/// guaranteed. As a result of the applied transform, the DSIG
|
||||
/// table may also be invalidated.
|
||||
///
|
||||
/// Bit 12: Font converted (produce compatible metrics).
|
||||
///
|
||||
/// Bit 13: Font optimized for ClearType®. Note, fonts that rely on embedded
|
||||
/// bitmaps (EBDT) for rendering should not be considered optimized
|
||||
/// for ClearType, and therefore should keep this bit cleared.
|
||||
///
|
||||
/// Bit 14: Last Resort font. If set, indicates that the glyphs encoded in
|
||||
/// the 'cmap' subtables are simply generic symbolic representations
|
||||
/// of code point ranges and do not truly represent support for
|
||||
/// those code points. If unset, indicates that the glyphs encoded
|
||||
/// in the 'cmap' subtables represent proper support for those code
|
||||
/// points.
|
||||
///
|
||||
/// Bit 15: Reserved, set to 0.
|
||||
flags: sfnt.uint16 align(1),
|
||||
|
||||
/// Set to a value from 16 to 16384. Any value in this range is valid.
|
||||
///
|
||||
/// In fonts that have TrueType outlines, a power of 2 is recommended
|
||||
/// as this allows performance optimization in some rasterizers.
|
||||
unitsPerEm: sfnt.uint16 align(1),
|
||||
|
||||
/// Number of seconds since 12:00 midnight that started
|
||||
/// January 1st, 1904, in GMT/UTC time zone.
|
||||
created: sfnt.LONGDATETIME align(1),
|
||||
|
||||
/// Number of seconds since 12:00 midnight that started
|
||||
/// January 1st, 1904, in GMT/UTC time zone.
|
||||
modified: sfnt.LONGDATETIME align(1),
|
||||
|
||||
/// Minimum x coordinate across all glyph bounding boxes.
|
||||
xMin: sfnt.int16 align(1),
|
||||
|
||||
/// Minimum y coordinate across all glyph bounding boxes.
|
||||
yMin: sfnt.int16 align(1),
|
||||
|
||||
/// Maximum x coordinate across all glyph bounding boxes.
|
||||
xMax: sfnt.int16 align(1),
|
||||
|
||||
/// Maximum y coordinate across all glyph bounding boxes.
|
||||
yMax: sfnt.int16 align(1),
|
||||
|
||||
/// Bit 0: Bold (if set to 1);
|
||||
/// Bit 1: Italic (if set to 1)
|
||||
/// Bit 2: Underline (if set to 1)
|
||||
/// Bit 3: Outline (if set to 1)
|
||||
/// Bit 4: Shadow (if set to 1)
|
||||
/// Bit 5: Condensed (if set to 1)
|
||||
/// Bit 6: Extended (if set to 1)
|
||||
/// Bits 7 – 15: Reserved (set to 0).
|
||||
macStyle: sfnt.uint16 align(1),
|
||||
|
||||
/// Smallest readable size in pixels.
|
||||
lowestRecPPEM: sfnt.uint16 align(1),
|
||||
|
||||
/// Deprecated (Set to 2).
|
||||
/// 0: Fully mixed directional glyphs;
|
||||
/// 1: Only strongly left to right;
|
||||
/// 2: Like 1 but also contains neutrals;
|
||||
/// -1: Only strongly right to left;
|
||||
/// -2: Like -1 but also contains neutrals.
|
||||
fontDirectionHint: sfnt.int16 align(1),
|
||||
|
||||
/// 0 for short offsets (Offset16), 1 for long (Offset32).
|
||||
indexToLocFormat: sfnt.int16 align(1),
|
||||
|
||||
/// 0 for current format.
|
||||
glyphDataFormat: sfnt.int16 align(1),
|
||||
|
||||
/// Parse the table from raw data.
|
||||
pub fn init(data: []const u8) error{EndOfStream}!Head {
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
return try reader.readStructEndian(Head, .big);
|
||||
}
|
||||
};
|
||||
|
||||
test "head" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const test_font = @import("../embedded.zig").julia_mono;
|
||||
|
||||
const font = try sfnt.SFNT.init(test_font, alloc);
|
||||
defer font.deinit(alloc);
|
||||
|
||||
const table = font.getTable("head").?;
|
||||
|
||||
const head = try Head.init(table);
|
||||
|
||||
try testing.expectEqualDeep(
|
||||
Head{
|
||||
.majorVersion = 1,
|
||||
.minorVersion = 0,
|
||||
.fontRevision = sfnt.Fixed.from(0.05499267578125),
|
||||
.checksumAdjustment = 1007668681,
|
||||
.magicNumber = 1594834165,
|
||||
.flags = 7,
|
||||
.unitsPerEm = 2000,
|
||||
.created = 3797757830,
|
||||
.modified = 3797760444,
|
||||
.xMin = -1000,
|
||||
.yMin = -1058,
|
||||
.xMax = 3089,
|
||||
.yMax = 2400,
|
||||
.macStyle = 0,
|
||||
.lowestRecPPEM = 7,
|
||||
.fontDirectionHint = 2,
|
||||
.indexToLocFormat = 1,
|
||||
.glyphDataFormat = 0,
|
||||
},
|
||||
head,
|
||||
);
|
||||
}
|
117
src/font/opentype/hhea.zig
Normal file
117
src/font/opentype/hhea.zig
Normal file
@ -0,0 +1,117 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const sfnt = @import("sfnt.zig");
|
||||
|
||||
/// Horizontal Header Table
|
||||
///
|
||||
/// References:
|
||||
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/hhea
|
||||
///
|
||||
/// Field names are in camelCase to match names in spec.
|
||||
pub const Hhea = extern struct {
|
||||
/// Major version number of the horizontal header table — set to 1.
|
||||
majorVersion: sfnt.uint16 align(1),
|
||||
|
||||
/// Minor version number of the horizontal header table — set to 0.
|
||||
minorVersion: sfnt.uint16 align(1),
|
||||
|
||||
/// Typographic ascent—see remarks below.
|
||||
ascender: sfnt.FWORD align(1),
|
||||
|
||||
/// Typographic descent—see remarks below.
|
||||
descender: sfnt.FWORD align(1),
|
||||
|
||||
/// Typographic line gap.
|
||||
///
|
||||
/// Negative lineGap values are treated as zero
|
||||
/// in some legacy platform implementations.
|
||||
lineGap: sfnt.FWORD align(1),
|
||||
|
||||
/// Maximum advance width value in 'hmtx' table.
|
||||
advanceWidthMax: sfnt.UFWORD align(1),
|
||||
|
||||
/// Minimum left sidebearing value in 'hmtx' table for
|
||||
/// glyphs with contours (empty glyphs should be ignored).
|
||||
minLeftSideBearing: sfnt.FWORD align(1),
|
||||
|
||||
/// Minimum right sidebearing value; calculated as
|
||||
/// min(aw - (lsb + xMax - xMin)) for glyphs with
|
||||
/// contours (empty glyphs should be ignored).
|
||||
minRightSideBearing: sfnt.FWORD align(1),
|
||||
|
||||
/// Max(lsb + (xMax - xMin)).
|
||||
xMaxExtent: sfnt.FWORD align(1),
|
||||
|
||||
/// Used to calculate the slope of the cursor (rise/run); 1 for vertical.
|
||||
caretSlopeRise: sfnt.int16 align(1),
|
||||
|
||||
/// 0 for vertical.
|
||||
caretSlopeRun: sfnt.int16 align(1),
|
||||
|
||||
/// The amount by which a slanted highlight on a glyph needs to be shifted
|
||||
/// to produce the best appearance. Set to 0 for non-slanted fonts
|
||||
caretOffset: sfnt.int16 align(1),
|
||||
|
||||
/// set to 0
|
||||
_reserved0: sfnt.int16 align(1),
|
||||
|
||||
/// set to 0
|
||||
_reserved1: sfnt.int16 align(1),
|
||||
|
||||
/// set to 0
|
||||
_reserved2: sfnt.int16 align(1),
|
||||
|
||||
/// set to 0
|
||||
_reserved3: sfnt.int16 align(1),
|
||||
|
||||
/// 0 for current format.
|
||||
metricDataFormat: sfnt.int16 align(1),
|
||||
|
||||
/// Number of hMetric entries in 'hmtx' table
|
||||
numberOfHMetrics: sfnt.uint16 align(1),
|
||||
|
||||
/// Parse the table from raw data.
|
||||
pub fn init(data: []const u8) !Hhea {
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
|
||||
return try reader.readStructEndian(Hhea, .big);
|
||||
}
|
||||
};
|
||||
|
||||
test "hhea" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const test_font = @import("../embedded.zig").julia_mono;
|
||||
|
||||
const font = try sfnt.SFNT.init(test_font, alloc);
|
||||
defer font.deinit(alloc);
|
||||
|
||||
const table = font.getTable("hhea").?;
|
||||
|
||||
const hhea = try Hhea.init(table);
|
||||
|
||||
try testing.expectEqualDeep(
|
||||
Hhea{
|
||||
.majorVersion = 1,
|
||||
.minorVersion = 0,
|
||||
.ascender = 1900,
|
||||
.descender = -450,
|
||||
.lineGap = 0,
|
||||
.advanceWidthMax = 1200,
|
||||
.minLeftSideBearing = -1000,
|
||||
.minRightSideBearing = -1889,
|
||||
.xMaxExtent = 3089,
|
||||
.caretSlopeRise = 1,
|
||||
.caretSlopeRun = 0,
|
||||
.caretOffset = 0,
|
||||
._reserved0 = 0,
|
||||
._reserved1 = 0,
|
||||
._reserved2 = 0,
|
||||
._reserved3 = 0,
|
||||
.metricDataFormat = 0,
|
||||
.numberOfHMetrics = 2,
|
||||
},
|
||||
hhea,
|
||||
);
|
||||
}
|
584
src/font/opentype/os2.zig
Normal file
584
src/font/opentype/os2.zig
Normal file
@ -0,0 +1,584 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const sfnt = @import("sfnt.zig");
|
||||
|
||||
pub const FSSelection = packed struct(sfnt.uint16) {
|
||||
/// Font contains italic or oblique glyphs, otherwise they are upright.
|
||||
italic: bool = false,
|
||||
|
||||
/// Glyphs are underscored.
|
||||
underscore: bool = false,
|
||||
|
||||
/// Glyphs have their foreground and background reversed.
|
||||
negative: bool = false,
|
||||
|
||||
/// Outline (hollow) glyphs, otherwise they are solid.
|
||||
outlined: bool = false,
|
||||
|
||||
/// Glyphs are overstruck.
|
||||
strikeout: bool = false,
|
||||
|
||||
/// Glyphs are emboldened.
|
||||
bold: bool = false,
|
||||
|
||||
/// Glyphs are in the standard weight/style for the font.
|
||||
regular: bool = false,
|
||||
|
||||
/// If set, it is strongly recommended that applications use
|
||||
/// OS/2.sTypoAscender - OS/2.sTypoDescender + OS/2.sTypoLineGap
|
||||
/// as the default line spacing for this font.
|
||||
use_typo_metrics: bool = false,
|
||||
|
||||
/// The font has 'name' table strings consistent with a weight/width/slope
|
||||
/// family without requiring use of name IDs 21 and 22.
|
||||
wws: bool = false,
|
||||
|
||||
/// Font contains oblique glyphs.
|
||||
oblique: bool = false,
|
||||
|
||||
_reserved: u6 = 0,
|
||||
};
|
||||
|
||||
/// OS/2 and Windows Metrics Table
|
||||
///
|
||||
/// References:
|
||||
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/os2
|
||||
///
|
||||
/// Field names are in camelCase to match names in spec.
|
||||
pub const OS2v5 = extern struct {
|
||||
version: sfnt.uint16 align(1),
|
||||
xAvgCharWidth: sfnt.FWORD align(1),
|
||||
usWeightClass: sfnt.uint16 align(1),
|
||||
usWidthClass: sfnt.uint16 align(1),
|
||||
fsType: sfnt.uint16 align(1),
|
||||
ySubscriptXSize: sfnt.FWORD align(1),
|
||||
ySubscriptYSize: sfnt.FWORD align(1),
|
||||
ySubscriptXOffset: sfnt.FWORD align(1),
|
||||
ySubscriptYOffset: sfnt.FWORD align(1),
|
||||
ySuperscriptXSize: sfnt.FWORD align(1),
|
||||
ySuperscriptYSize: sfnt.FWORD align(1),
|
||||
ySuperscriptXOffset: sfnt.FWORD align(1),
|
||||
ySuperscriptYOffset: sfnt.FWORD align(1),
|
||||
yStrikeoutSize: sfnt.FWORD align(1),
|
||||
yStrikeoutPosition: sfnt.FWORD align(1),
|
||||
sFamilyClass: sfnt.int16 align(1),
|
||||
panose: [10]sfnt.uint8 align(1),
|
||||
ulUnicodeRange1: sfnt.uint32 align(1),
|
||||
ulUnicodeRange2: sfnt.uint32 align(1),
|
||||
ulUnicodeRange3: sfnt.uint32 align(1),
|
||||
ulUnicodeRange4: sfnt.uint32 align(1),
|
||||
achVendID: sfnt.Tag align(1),
|
||||
fsSelection: FSSelection align(1),
|
||||
usFirstCharIndex: sfnt.uint16 align(1),
|
||||
usLastCharIndex: sfnt.uint16 align(1),
|
||||
sTypoAscender: sfnt.FWORD align(1),
|
||||
sTypoDescender: sfnt.FWORD align(1),
|
||||
sTypoLineGap: sfnt.FWORD align(1),
|
||||
usWinAscent: sfnt.UFWORD align(1),
|
||||
usWinDescent: sfnt.UFWORD align(1),
|
||||
ulCodePageRange1: sfnt.uint32 align(1),
|
||||
ulCodePageRange2: sfnt.uint32 align(1),
|
||||
sxHeight: sfnt.FWORD align(1),
|
||||
sCapHeight: sfnt.FWORD align(1),
|
||||
usDefaultChar: sfnt.uint16 align(1),
|
||||
usBreakChar: sfnt.uint16 align(1),
|
||||
usMaxContext: sfnt.uint16 align(1),
|
||||
usLowerOpticalPointSize: sfnt.uint16 align(1),
|
||||
usUpperOpticalPointSize: sfnt.uint16 align(1),
|
||||
};
|
||||
|
||||
pub const OS2v4_3_2 = extern struct {
|
||||
version: sfnt.uint16 align(1),
|
||||
xAvgCharWidth: sfnt.FWORD align(1),
|
||||
usWeightClass: sfnt.uint16 align(1),
|
||||
usWidthClass: sfnt.uint16 align(1),
|
||||
fsType: sfnt.uint16 align(1),
|
||||
ySubscriptXSize: sfnt.FWORD align(1),
|
||||
ySubscriptYSize: sfnt.FWORD align(1),
|
||||
ySubscriptXOffset: sfnt.FWORD align(1),
|
||||
ySubscriptYOffset: sfnt.FWORD align(1),
|
||||
ySuperscriptXSize: sfnt.FWORD align(1),
|
||||
ySuperscriptYSize: sfnt.FWORD align(1),
|
||||
ySuperscriptXOffset: sfnt.FWORD align(1),
|
||||
ySuperscriptYOffset: sfnt.FWORD align(1),
|
||||
yStrikeoutSize: sfnt.FWORD align(1),
|
||||
yStrikeoutPosition: sfnt.FWORD align(1),
|
||||
sFamilyClass: sfnt.int16 align(1),
|
||||
panose: [10]sfnt.uint8 align(1),
|
||||
ulUnicodeRange1: sfnt.uint32 align(1),
|
||||
ulUnicodeRange2: sfnt.uint32 align(1),
|
||||
ulUnicodeRange3: sfnt.uint32 align(1),
|
||||
ulUnicodeRange4: sfnt.uint32 align(1),
|
||||
achVendID: sfnt.Tag align(1),
|
||||
fsSelection: FSSelection align(1),
|
||||
usFirstCharIndex: sfnt.uint16 align(1),
|
||||
usLastCharIndex: sfnt.uint16 align(1),
|
||||
sTypoAscender: sfnt.FWORD align(1),
|
||||
sTypoDescender: sfnt.FWORD align(1),
|
||||
sTypoLineGap: sfnt.FWORD align(1),
|
||||
usWinAscent: sfnt.UFWORD align(1),
|
||||
usWinDescent: sfnt.UFWORD align(1),
|
||||
ulCodePageRange1: sfnt.uint32 align(1),
|
||||
ulCodePageRange2: sfnt.uint32 align(1),
|
||||
sxHeight: sfnt.FWORD align(1),
|
||||
sCapHeight: sfnt.FWORD align(1),
|
||||
usDefaultChar: sfnt.uint16 align(1),
|
||||
usBreakChar: sfnt.uint16 align(1),
|
||||
usMaxContext: sfnt.uint16 align(1),
|
||||
};
|
||||
|
||||
pub const OS2v1 = extern struct {
|
||||
version: sfnt.uint16 align(1),
|
||||
xAvgCharWidth: sfnt.FWORD align(1),
|
||||
usWeightClass: sfnt.uint16 align(1),
|
||||
usWidthClass: sfnt.uint16 align(1),
|
||||
fsType: sfnt.uint16 align(1),
|
||||
ySubscriptXSize: sfnt.FWORD align(1),
|
||||
ySubscriptYSize: sfnt.FWORD align(1),
|
||||
ySubscriptXOffset: sfnt.FWORD align(1),
|
||||
ySubscriptYOffset: sfnt.FWORD align(1),
|
||||
ySuperscriptXSize: sfnt.FWORD align(1),
|
||||
ySuperscriptYSize: sfnt.FWORD align(1),
|
||||
ySuperscriptXOffset: sfnt.FWORD align(1),
|
||||
ySuperscriptYOffset: sfnt.FWORD align(1),
|
||||
yStrikeoutSize: sfnt.FWORD align(1),
|
||||
yStrikeoutPosition: sfnt.FWORD align(1),
|
||||
sFamilyClass: sfnt.int16 align(1),
|
||||
panose: [10]sfnt.uint8 align(1),
|
||||
ulUnicodeRange1: sfnt.uint32 align(1),
|
||||
ulUnicodeRange2: sfnt.uint32 align(1),
|
||||
ulUnicodeRange3: sfnt.uint32 align(1),
|
||||
ulUnicodeRange4: sfnt.uint32 align(1),
|
||||
achVendID: sfnt.Tag align(1),
|
||||
fsSelection: FSSelection align(1),
|
||||
usFirstCharIndex: sfnt.uint16 align(1),
|
||||
usLastCharIndex: sfnt.uint16 align(1),
|
||||
sTypoAscender: sfnt.FWORD align(1),
|
||||
sTypoDescender: sfnt.FWORD align(1),
|
||||
sTypoLineGap: sfnt.FWORD align(1),
|
||||
usWinAscent: sfnt.UFWORD align(1),
|
||||
usWinDescent: sfnt.UFWORD align(1),
|
||||
ulCodePageRange1: sfnt.uint32 align(1),
|
||||
ulCodePageRange2: sfnt.uint32 align(1),
|
||||
};
|
||||
|
||||
pub const OS2v0 = extern struct {
|
||||
version: sfnt.uint16 align(1),
|
||||
xAvgCharWidth: sfnt.FWORD align(1),
|
||||
usWeightClass: sfnt.uint16 align(1),
|
||||
usWidthClass: sfnt.uint16 align(1),
|
||||
fsType: sfnt.uint16 align(1),
|
||||
ySubscriptXSize: sfnt.FWORD align(1),
|
||||
ySubscriptYSize: sfnt.FWORD align(1),
|
||||
ySubscriptXOffset: sfnt.FWORD align(1),
|
||||
ySubscriptYOffset: sfnt.FWORD align(1),
|
||||
ySuperscriptXSize: sfnt.FWORD align(1),
|
||||
ySuperscriptYSize: sfnt.FWORD align(1),
|
||||
ySuperscriptXOffset: sfnt.FWORD align(1),
|
||||
ySuperscriptYOffset: sfnt.FWORD align(1),
|
||||
yStrikeoutSize: sfnt.FWORD align(1),
|
||||
yStrikeoutPosition: sfnt.FWORD align(1),
|
||||
sFamilyClass: sfnt.int16 align(1),
|
||||
panose: [10]sfnt.uint8 align(1),
|
||||
ulUnicodeRange1: sfnt.uint32 align(1),
|
||||
ulUnicodeRange2: sfnt.uint32 align(1),
|
||||
ulUnicodeRange3: sfnt.uint32 align(1),
|
||||
ulUnicodeRange4: sfnt.uint32 align(1),
|
||||
achVendID: sfnt.Tag align(1),
|
||||
fsSelection: FSSelection align(1),
|
||||
usFirstCharIndex: sfnt.uint16 align(1),
|
||||
usLastCharIndex: sfnt.uint16 align(1),
|
||||
sTypoAscender: sfnt.FWORD align(1),
|
||||
sTypoDescender: sfnt.FWORD align(1),
|
||||
sTypoLineGap: sfnt.FWORD align(1),
|
||||
usWinAscent: sfnt.UFWORD align(1),
|
||||
usWinDescent: sfnt.UFWORD align(1),
|
||||
};
|
||||
|
||||
/// Generic OS/2 table with optional fields
|
||||
/// for those that don't exist in all versions.
|
||||
///
|
||||
/// References:
|
||||
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/os2
|
||||
///
|
||||
/// Field names are in camelCase to match names in spec.
|
||||
pub const OS2 = struct {
|
||||
/// The version number for the OS/2 table: 0x0000 to 0x0005.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#version
|
||||
version: u16,
|
||||
/// The Average Character Width field specifies the arithmetic average of the escapement (width) of all non-zero width glyphs in the font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#xavgcharwidth
|
||||
xAvgCharWidth: i16,
|
||||
/// Indicates the visual weight (degree of blackness or thickness of strokes) of the characters in the font. Values from 1 to 1000 are valid.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usweightclass
|
||||
usWeightClass: u16,
|
||||
/// Indicates a relative change from the normal aspect ratio (width to height ratio) as specified by a font designer for the glyphs in a font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uswidthclass
|
||||
usWidthClass: u16,
|
||||
/// Indicates font embedding licensing rights for the font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#fstype
|
||||
fsType: u16,
|
||||
/// The recommended horizontal size in font design units for subscripts for this font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptxsize
|
||||
ySubscriptXSize: i16,
|
||||
/// The recommended vertical size in font design units for subscripts for this font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptysize
|
||||
ySubscriptYSize: i16,
|
||||
/// The recommended horizontal offset in font design units for subscripts for this font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptxoffset
|
||||
ySubscriptXOffset: i16,
|
||||
/// The recommended vertical offset in font design units from the baseline for subscripts for this font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptyoffset
|
||||
ySubscriptYOffset: i16,
|
||||
/// The recommended horizontal size in font design units for superscripts for this font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptxsize
|
||||
ySuperscriptXSize: i16,
|
||||
/// The recommended vertical size in font design units for superscripts for this font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptysize
|
||||
ySuperscriptYSize: i16,
|
||||
/// The recommended horizontal offset in font design units for superscripts for this font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptxoffset
|
||||
ySuperscriptXOffset: i16,
|
||||
/// The recommended vertical offset in font design units from the baseline for superscripts for this font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptyoffset
|
||||
ySuperscriptYOffset: i16,
|
||||
/// Thickness of the strikeout stroke in font design units. Should be > 0.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ystrikeoutsize
|
||||
yStrikeoutSize: i16,
|
||||
/// The position of the top of the strikeout stroke relative to the baseline in font design units.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ystrikeoutposition
|
||||
yStrikeoutPosition: i16,
|
||||
/// This field provides a classification of font-family design.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#sfamilyclass
|
||||
sFamilyClass: i16,
|
||||
/// This 10-byte array of numbers is used to describe the visual characteristics of a given typeface.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#panose
|
||||
panose: [10]u8,
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange
|
||||
ulUnicodeRange1: u32,
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange
|
||||
ulUnicodeRange2: u32,
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange
|
||||
ulUnicodeRange3: u32,
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange
|
||||
ulUnicodeRange4: u32,
|
||||
/// The four character identifier for the vendor of the given type face.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#achvendid
|
||||
achVendID: [4]u8,
|
||||
/// Contains information concerning the nature of the font patterns.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#fsselection
|
||||
fsSelection: FSSelection,
|
||||
/// The minimum Unicode index (character code) in this font, according to the 'cmap' subtable for platform ID 3 and platform-specific encoding ID 0 or 1.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usfirstcharindex
|
||||
usFirstCharIndex: u16,
|
||||
/// The maximum Unicode index (character code) in this font, according to the 'cmap' subtable for platform ID 3 and encoding ID 0 or 1.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uslastcharindex
|
||||
usLastCharIndex: u16,
|
||||
/// The typographic ascender for this font. This field should be combined with the sTypoDescender and sTypoLineGap values to determine default line spacing.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#stypoascender
|
||||
sTypoAscender: i16,
|
||||
/// The typographic descender for this font. This field should be combined with the sTypoAscender and sTypoLineGap values to determine default line spacing.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#stypodescender
|
||||
sTypoDescender: i16,
|
||||
/// The typographic line gap for this font. This field should be combined with the sTypoAscender and sTypoDescender values to determine default line spacing.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#stypolinegap
|
||||
sTypoLineGap: i16,
|
||||
/// The “Windows ascender” metric. This should be used to specify the height above the baseline for a clipping region.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uswinascent
|
||||
usWinAscent: u16,
|
||||
/// The “Windows descender” metric. This should be used to specify the vertical extent below the baseline for a clipping region.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uswindescent
|
||||
usWinDescent: u16,
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulcodepagerange
|
||||
ulCodePageRange1: ?u32 = null,
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulcodepagerange
|
||||
ulCodePageRange2: ?u32 = null,
|
||||
/// This metric specifies the distance between the baseline and the approximate height of non-ascending lowercase letters measured in font design units.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#sxheight
|
||||
sxHeight: ?i16 = null,
|
||||
/// This metric specifies the distance between the baseline and the approximate height of uppercase letters measured in font design units.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#scapheight
|
||||
sCapHeight: ?i16 = null,
|
||||
/// This is the Unicode code point, in UTF-16 encoding, of a character that can be used for a default glyph if a requested character is not supported in the font.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usdefaultchar
|
||||
usDefaultChar: ?u16 = null,
|
||||
/// This is the Unicode code point, in UTF-16 encoding, of a character that can be used as a default break character.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usbreakchar
|
||||
usBreakChar: ?u16 = null,
|
||||
/// The maximum length of a target glyph context for any feature in this font. For example, a font which has only a pair kerning feature should set this field to 2.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usmaxcontext
|
||||
usMaxContext: ?u16 = null,
|
||||
/// This field is used for fonts with multiple optical styles.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usloweropticalpointsize
|
||||
usLowerOpticalPointSize: ?u16 = null,
|
||||
/// This field is used for fonts with multiple optical styles.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usupperopticalpointsize
|
||||
usUpperOpticalPointSize: ?u16 = null,
|
||||
|
||||
/// Parse the table from raw data.
|
||||
pub fn init(data: []const u8) error{
|
||||
EndOfStream,
|
||||
OS2VersionNotSupported,
|
||||
}!OS2 {
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
|
||||
const version = try reader.readInt(sfnt.uint16, .big);
|
||||
|
||||
// Return to the start, cause the version is part of the struct.
|
||||
try fbs.seekTo(0);
|
||||
|
||||
switch (version) {
|
||||
5 => {
|
||||
const table = try reader.readStructEndian(OS2v5, .big);
|
||||
return .{
|
||||
.version = table.version,
|
||||
.xAvgCharWidth = table.xAvgCharWidth,
|
||||
.usWeightClass = table.usWeightClass,
|
||||
.usWidthClass = table.usWidthClass,
|
||||
.fsType = table.fsType,
|
||||
.ySubscriptXSize = table.ySubscriptXSize,
|
||||
.ySubscriptYSize = table.ySubscriptYSize,
|
||||
.ySubscriptXOffset = table.ySubscriptXOffset,
|
||||
.ySubscriptYOffset = table.ySubscriptYOffset,
|
||||
.ySuperscriptXSize = table.ySuperscriptXSize,
|
||||
.ySuperscriptYSize = table.ySuperscriptYSize,
|
||||
.ySuperscriptXOffset = table.ySuperscriptXOffset,
|
||||
.ySuperscriptYOffset = table.ySuperscriptYOffset,
|
||||
.yStrikeoutSize = table.yStrikeoutSize,
|
||||
.yStrikeoutPosition = table.yStrikeoutPosition,
|
||||
.sFamilyClass = table.sFamilyClass,
|
||||
.panose = table.panose,
|
||||
.ulUnicodeRange1 = table.ulUnicodeRange1,
|
||||
.ulUnicodeRange2 = table.ulUnicodeRange2,
|
||||
.ulUnicodeRange3 = table.ulUnicodeRange3,
|
||||
.ulUnicodeRange4 = table.ulUnicodeRange4,
|
||||
.achVendID = table.achVendID,
|
||||
.fsSelection = table.fsSelection,
|
||||
.usFirstCharIndex = table.usFirstCharIndex,
|
||||
.usLastCharIndex = table.usLastCharIndex,
|
||||
.sTypoAscender = table.sTypoAscender,
|
||||
.sTypoDescender = table.sTypoDescender,
|
||||
.sTypoLineGap = table.sTypoLineGap,
|
||||
.usWinAscent = table.usWinAscent,
|
||||
.usWinDescent = table.usWinDescent,
|
||||
.ulCodePageRange1 = table.ulCodePageRange1,
|
||||
.ulCodePageRange2 = table.ulCodePageRange2,
|
||||
.sxHeight = table.sxHeight,
|
||||
.sCapHeight = table.sCapHeight,
|
||||
.usDefaultChar = table.usDefaultChar,
|
||||
.usBreakChar = table.usBreakChar,
|
||||
.usMaxContext = table.usMaxContext,
|
||||
.usLowerOpticalPointSize = table.usLowerOpticalPointSize,
|
||||
.usUpperOpticalPointSize = table.usUpperOpticalPointSize,
|
||||
};
|
||||
},
|
||||
4, 3, 2 => {
|
||||
const table = try reader.readStructEndian(OS2v4_3_2, .big);
|
||||
return .{
|
||||
.version = table.version,
|
||||
.xAvgCharWidth = table.xAvgCharWidth,
|
||||
.usWeightClass = table.usWeightClass,
|
||||
.usWidthClass = table.usWidthClass,
|
||||
.fsType = table.fsType,
|
||||
.ySubscriptXSize = table.ySubscriptXSize,
|
||||
.ySubscriptYSize = table.ySubscriptYSize,
|
||||
.ySubscriptXOffset = table.ySubscriptXOffset,
|
||||
.ySubscriptYOffset = table.ySubscriptYOffset,
|
||||
.ySuperscriptXSize = table.ySuperscriptXSize,
|
||||
.ySuperscriptYSize = table.ySuperscriptYSize,
|
||||
.ySuperscriptXOffset = table.ySuperscriptXOffset,
|
||||
.ySuperscriptYOffset = table.ySuperscriptYOffset,
|
||||
.yStrikeoutSize = table.yStrikeoutSize,
|
||||
.yStrikeoutPosition = table.yStrikeoutPosition,
|
||||
.sFamilyClass = table.sFamilyClass,
|
||||
.panose = table.panose,
|
||||
.ulUnicodeRange1 = table.ulUnicodeRange1,
|
||||
.ulUnicodeRange2 = table.ulUnicodeRange2,
|
||||
.ulUnicodeRange3 = table.ulUnicodeRange3,
|
||||
.ulUnicodeRange4 = table.ulUnicodeRange4,
|
||||
.achVendID = table.achVendID,
|
||||
.fsSelection = table.fsSelection,
|
||||
.usFirstCharIndex = table.usFirstCharIndex,
|
||||
.usLastCharIndex = table.usLastCharIndex,
|
||||
.sTypoAscender = table.sTypoAscender,
|
||||
.sTypoDescender = table.sTypoDescender,
|
||||
.sTypoLineGap = table.sTypoLineGap,
|
||||
.usWinAscent = table.usWinAscent,
|
||||
.usWinDescent = table.usWinDescent,
|
||||
.ulCodePageRange1 = table.ulCodePageRange1,
|
||||
.ulCodePageRange2 = table.ulCodePageRange2,
|
||||
.sxHeight = table.sxHeight,
|
||||
.sCapHeight = table.sCapHeight,
|
||||
.usDefaultChar = table.usDefaultChar,
|
||||
.usBreakChar = table.usBreakChar,
|
||||
.usMaxContext = table.usMaxContext,
|
||||
};
|
||||
},
|
||||
1 => {
|
||||
const table = try reader.readStructEndian(OS2v1, .big);
|
||||
return .{
|
||||
.version = table.version,
|
||||
.xAvgCharWidth = table.xAvgCharWidth,
|
||||
.usWeightClass = table.usWeightClass,
|
||||
.usWidthClass = table.usWidthClass,
|
||||
.fsType = table.fsType,
|
||||
.ySubscriptXSize = table.ySubscriptXSize,
|
||||
.ySubscriptYSize = table.ySubscriptYSize,
|
||||
.ySubscriptXOffset = table.ySubscriptXOffset,
|
||||
.ySubscriptYOffset = table.ySubscriptYOffset,
|
||||
.ySuperscriptXSize = table.ySuperscriptXSize,
|
||||
.ySuperscriptYSize = table.ySuperscriptYSize,
|
||||
.ySuperscriptXOffset = table.ySuperscriptXOffset,
|
||||
.ySuperscriptYOffset = table.ySuperscriptYOffset,
|
||||
.yStrikeoutSize = table.yStrikeoutSize,
|
||||
.yStrikeoutPosition = table.yStrikeoutPosition,
|
||||
.sFamilyClass = table.sFamilyClass,
|
||||
.panose = table.panose,
|
||||
.ulUnicodeRange1 = table.ulUnicodeRange1,
|
||||
.ulUnicodeRange2 = table.ulUnicodeRange2,
|
||||
.ulUnicodeRange3 = table.ulUnicodeRange3,
|
||||
.ulUnicodeRange4 = table.ulUnicodeRange4,
|
||||
.achVendID = table.achVendID,
|
||||
.fsSelection = table.fsSelection,
|
||||
.usFirstCharIndex = table.usFirstCharIndex,
|
||||
.usLastCharIndex = table.usLastCharIndex,
|
||||
.sTypoAscender = table.sTypoAscender,
|
||||
.sTypoDescender = table.sTypoDescender,
|
||||
.sTypoLineGap = table.sTypoLineGap,
|
||||
.usWinAscent = table.usWinAscent,
|
||||
.usWinDescent = table.usWinDescent,
|
||||
.ulCodePageRange1 = table.ulCodePageRange1,
|
||||
.ulCodePageRange2 = table.ulCodePageRange2,
|
||||
};
|
||||
},
|
||||
0 => {
|
||||
const table = try reader.readStructEndian(OS2v0, .big);
|
||||
return .{
|
||||
.version = table.version,
|
||||
.xAvgCharWidth = table.xAvgCharWidth,
|
||||
.usWeightClass = table.usWeightClass,
|
||||
.usWidthClass = table.usWidthClass,
|
||||
.fsType = table.fsType,
|
||||
.ySubscriptXSize = table.ySubscriptXSize,
|
||||
.ySubscriptYSize = table.ySubscriptYSize,
|
||||
.ySubscriptXOffset = table.ySubscriptXOffset,
|
||||
.ySubscriptYOffset = table.ySubscriptYOffset,
|
||||
.ySuperscriptXSize = table.ySuperscriptXSize,
|
||||
.ySuperscriptYSize = table.ySuperscriptYSize,
|
||||
.ySuperscriptXOffset = table.ySuperscriptXOffset,
|
||||
.ySuperscriptYOffset = table.ySuperscriptYOffset,
|
||||
.yStrikeoutSize = table.yStrikeoutSize,
|
||||
.yStrikeoutPosition = table.yStrikeoutPosition,
|
||||
.sFamilyClass = table.sFamilyClass,
|
||||
.panose = table.panose,
|
||||
.ulUnicodeRange1 = table.ulUnicodeRange1,
|
||||
.ulUnicodeRange2 = table.ulUnicodeRange2,
|
||||
.ulUnicodeRange3 = table.ulUnicodeRange3,
|
||||
.ulUnicodeRange4 = table.ulUnicodeRange4,
|
||||
.achVendID = table.achVendID,
|
||||
.fsSelection = table.fsSelection,
|
||||
.usFirstCharIndex = table.usFirstCharIndex,
|
||||
.usLastCharIndex = table.usLastCharIndex,
|
||||
.sTypoAscender = table.sTypoAscender,
|
||||
.sTypoDescender = table.sTypoDescender,
|
||||
.sTypoLineGap = table.sTypoLineGap,
|
||||
.usWinAscent = table.usWinAscent,
|
||||
.usWinDescent = table.usWinDescent,
|
||||
};
|
||||
},
|
||||
else => return error.OS2VersionNotSupported,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
test "OS/2" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const test_font = @import("../embedded.zig").julia_mono;
|
||||
|
||||
const font = try sfnt.SFNT.init(test_font, alloc);
|
||||
defer font.deinit(alloc);
|
||||
|
||||
const table = font.getTable("OS/2").?;
|
||||
|
||||
const os2 = try OS2.init(table);
|
||||
|
||||
try testing.expectEqualDeep(OS2{
|
||||
.version = 4,
|
||||
.xAvgCharWidth = 1200,
|
||||
.usWeightClass = 400,
|
||||
.usWidthClass = 5,
|
||||
.fsType = 0,
|
||||
.ySubscriptXSize = 1300,
|
||||
.ySubscriptYSize = 1200,
|
||||
.ySubscriptXOffset = 0,
|
||||
.ySubscriptYOffset = 150,
|
||||
.ySuperscriptXSize = 1300,
|
||||
.ySuperscriptYSize = 1200,
|
||||
.ySuperscriptXOffset = 0,
|
||||
.ySuperscriptYOffset = 700,
|
||||
.yStrikeoutSize = 100,
|
||||
.yStrikeoutPosition = 550,
|
||||
.sFamilyClass = 0,
|
||||
.panose = .{ 2, 11, 6, 9, 6, 3, 0, 2, 0, 4 },
|
||||
.ulUnicodeRange1 = 3843162111,
|
||||
.ulUnicodeRange2 = 3603300351,
|
||||
.ulUnicodeRange3 = 117760229,
|
||||
.ulUnicodeRange4 = 96510060,
|
||||
.achVendID = "corm".*,
|
||||
.fsSelection = .{
|
||||
.regular = true,
|
||||
.use_typo_metrics = true,
|
||||
},
|
||||
.usFirstCharIndex = 13,
|
||||
.usLastCharIndex = 65535,
|
||||
.sTypoAscender = 1900,
|
||||
.sTypoDescender = -450,
|
||||
.sTypoLineGap = 0,
|
||||
.usWinAscent = 2400,
|
||||
.usWinDescent = 450,
|
||||
.ulCodePageRange1 = 1613234687,
|
||||
.ulCodePageRange2 = 0,
|
||||
.sxHeight = 1100,
|
||||
.sCapHeight = 1450,
|
||||
.usDefaultChar = 0,
|
||||
.usBreakChar = 32,
|
||||
.usMaxContext = 126,
|
||||
.usLowerOpticalPointSize = null,
|
||||
.usUpperOpticalPointSize = null,
|
||||
}, os2);
|
||||
}
|
83
src/font/opentype/post.zig
Normal file
83
src/font/opentype/post.zig
Normal file
@ -0,0 +1,83 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const sfnt = @import("sfnt.zig");
|
||||
|
||||
/// PostScript Table
|
||||
///
|
||||
/// This implementation doesn't parse the
|
||||
/// extra fields in versions 2.0 and 2.5.
|
||||
///
|
||||
/// References:
|
||||
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/post
|
||||
///
|
||||
/// Field names are in camelCase to match names in spec.
|
||||
pub const Post = extern struct {
|
||||
version: sfnt.Version16Dot16 align(1),
|
||||
|
||||
/// Italic angle in counter-clockwise degrees from the vertical.
|
||||
/// Zero for upright text, negative for text that leans to the
|
||||
/// right (forward).
|
||||
italicAngle: sfnt.Fixed align(1),
|
||||
|
||||
/// Suggested y-coordinate of the top of the underline.
|
||||
underlinePosition: sfnt.FWORD align(1),
|
||||
|
||||
/// Suggested values for the underline thickness.
|
||||
/// In general, the underline thickness should match the thickness of
|
||||
/// the underscore character (U+005F LOW LINE), and should also match
|
||||
/// the strikeout thickness, which is specified in the OS/2 table.
|
||||
underlineThickness: sfnt.FWORD align(1),
|
||||
|
||||
/// Set to 0 if the font is proportionally spaced, non-zero if
|
||||
/// the font is not proportionally spaced (i.e. monospaced).
|
||||
isFixedPitch: sfnt.uint32 align(1),
|
||||
|
||||
/// Minimum memory usage when an OpenType font is downloaded.
|
||||
minMemType42: sfnt.uint32 align(1),
|
||||
|
||||
/// Maximum memory usage when an OpenType font is downloaded.
|
||||
maxMemType42: sfnt.uint32 align(1),
|
||||
|
||||
/// Minimum memory usage when an OpenType
|
||||
/// font is downloaded as a Type 1 font.
|
||||
minMemType1: sfnt.uint32 align(1),
|
||||
|
||||
/// Maximum memory usage when an OpenType
|
||||
/// font is downloaded as a Type 1 font.
|
||||
maxMemType1: sfnt.uint32 align(1),
|
||||
|
||||
/// Parse the table from raw data.
|
||||
pub fn init(data: []const u8) error{EndOfStream}!Post {
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
return try reader.readStructEndian(Post, .big);
|
||||
}
|
||||
};
|
||||
|
||||
test "post" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const test_font = @import("../embedded.zig").julia_mono;
|
||||
|
||||
const font = try sfnt.SFNT.init(test_font, alloc);
|
||||
defer font.deinit(alloc);
|
||||
|
||||
const table = font.getTable("post").?;
|
||||
|
||||
const post = try Post.init(table);
|
||||
|
||||
try testing.expectEqualDeep(
|
||||
Post{
|
||||
.version = sfnt.Version16Dot16{ .minor = 0, .major = 2 },
|
||||
.italicAngle = sfnt.Fixed.from(0.0),
|
||||
.underlinePosition = -200,
|
||||
.underlineThickness = 100,
|
||||
.isFixedPitch = 1,
|
||||
.minMemType42 = 0,
|
||||
.maxMemType42 = 0,
|
||||
.minMemType1 = 0,
|
||||
.maxMemType1 = 0,
|
||||
},
|
||||
post,
|
||||
);
|
||||
}
|
314
src/font/opentype/sfnt.zig
Normal file
314
src/font/opentype/sfnt.zig
Normal file
@ -0,0 +1,314 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// 8-bit unsigned integer.
|
||||
pub const uint8 = u8;
|
||||
|
||||
/// 8-bit signed integer.
|
||||
pub const int8 = i8;
|
||||
|
||||
/// 16-bit unsigned integer.
|
||||
pub const uint16 = u16;
|
||||
|
||||
/// 16-bit signed integer.
|
||||
pub const int16 = i16;
|
||||
|
||||
/// 24-bit unsigned integer.
|
||||
pub const uint24 = u24;
|
||||
|
||||
/// 32-bit unsigned integer.
|
||||
pub const uint32 = u32;
|
||||
|
||||
/// 32-bit signed integer.
|
||||
pub const int32 = i32;
|
||||
|
||||
/// 32-bit signed fixed-point number (16.16)
|
||||
pub const Fixed = FixedPoint(i32, 16, 16);
|
||||
|
||||
/// int16 that describes a quantity in font design units.
|
||||
pub const FWORD = i16;
|
||||
|
||||
/// uint16 that describes a quantity in font design units.
|
||||
pub const UFWORD = u16;
|
||||
|
||||
/// 16-bit signed fixed number with the low 14 bits of fraction (2.14).
|
||||
pub const F2DOT14 = FixedPoint(i16, 2, 14);
|
||||
|
||||
/// Date and time represented in number of seconds since 12:00 midnight, January 1, 1904, UTC. The value is represented as a signed 64-bit integer.
|
||||
pub const LONGDATETIME = i64;
|
||||
|
||||
/// Array of four uint8s (length = 32 bits) used to identify a table,
|
||||
/// design-variation axis, script, language system, feature, or baseline.
|
||||
pub const Tag = [4]u8;
|
||||
|
||||
/// 8-bit offset to a table, same as uint8, NULL offset = 0x00
|
||||
pub const Offset8 = u8;
|
||||
|
||||
/// Short offset to a table, same as uint16, NULL offset = 0x0000
|
||||
pub const Offset16 = u16;
|
||||
|
||||
/// 24-bit offset to a table, same as uint24, NULL offset = 0x000000
|
||||
pub const Offset24 = u24;
|
||||
|
||||
/// Long offset to a table, same as uint32, NULL offset = 0x00000000
|
||||
pub const Offset32 = u32;
|
||||
|
||||
/// Packed 32-bit value with major and minor version numbers
|
||||
pub const Version16Dot16 = packed struct(u32) {
|
||||
minor: u16,
|
||||
major: u16,
|
||||
};
|
||||
|
||||
/// 32-bit signed 26.6 fixed point numbers.
|
||||
pub const F26Dot6 = FixedPoint(i32, 26, 6);
|
||||
|
||||
fn FixedPoint(comptime T: type, int_bits: u64, frac_bits: u64) type {
|
||||
const type_info: std.builtin.Type.Int = @typeInfo(T).Int;
|
||||
comptime assert(int_bits + frac_bits == type_info.bits);
|
||||
|
||||
return packed struct(T) {
|
||||
const Self = FixedPoint(T, int_bits, frac_bits);
|
||||
const frac_factor: comptime_float = @floatFromInt(std.math.pow(
|
||||
u64,
|
||||
2,
|
||||
frac_bits,
|
||||
));
|
||||
const half = @as(T, 1) << @intCast(frac_bits - 1);
|
||||
|
||||
frac: std.meta.Int(.unsigned, frac_bits),
|
||||
int: std.meta.Int(type_info.signedness, int_bits),
|
||||
|
||||
pub fn to(self: Self, comptime FloatType: type) FloatType {
|
||||
const i: FloatType = @floatFromInt(self.int);
|
||||
const f: FloatType = @floatFromInt(self.frac);
|
||||
|
||||
return i + f / frac_factor;
|
||||
}
|
||||
|
||||
pub fn from(float: anytype) Self {
|
||||
const int = @floor(float);
|
||||
const frac = @abs(float - int);
|
||||
|
||||
return .{
|
||||
.int = @intFromFloat(int),
|
||||
.frac = @intFromFloat(@round(frac * frac_factor)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Round to the nearest integer, .5 rounds away from 0.
|
||||
pub fn round(self: Self) T {
|
||||
if (self.frac & half != 0)
|
||||
return self.int + 1
|
||||
else
|
||||
return self.int;
|
||||
}
|
||||
|
||||
pub fn format(
|
||||
self: Self,
|
||||
comptime fmt: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
_ = fmt;
|
||||
_ = options;
|
||||
|
||||
try writer.print("{d}", .{self.to(f64)});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test FixedPoint {
|
||||
const testing = std.testing;
|
||||
|
||||
const p26d6 = F26Dot6.from(26.6);
|
||||
try testing.expectEqual(F26Dot6{
|
||||
.int = 26,
|
||||
.frac = 38,
|
||||
}, p26d6);
|
||||
try testing.expectEqual(26.59375, p26d6.to(f64));
|
||||
try testing.expectEqual(27, p26d6.round());
|
||||
|
||||
const n26d6 = F26Dot6.from(-26.6);
|
||||
try testing.expectEqual(F26Dot6{
|
||||
.int = -27,
|
||||
.frac = 26,
|
||||
}, n26d6);
|
||||
try testing.expectEqual(-26.59375, n26d6.to(f64));
|
||||
try testing.expectEqual(-27, n26d6.round());
|
||||
}
|
||||
|
||||
/// Wrapper for parsing a SFNT font and accessing its tables.
|
||||
///
|
||||
/// References:
|
||||
/// - https://learn.microsoft.com/en-us/typography/opentype/spec/otff
|
||||
/// - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6.html
|
||||
pub const SFNT = struct {
|
||||
const Directory = struct {
|
||||
offset: OffsetSubtable,
|
||||
records: []TableRecord,
|
||||
|
||||
/// The static (fixed-sized) portion of the table directory
|
||||
///
|
||||
/// This struct matches the memory layout of the TrueType/OpenType
|
||||
/// TableDirectory, but does not include the TableRecord array, since
|
||||
/// that is dynamically sized, so we parse it separately.
|
||||
///
|
||||
/// In the TrueType reference manual this
|
||||
/// is referred to as the "offset subtable".
|
||||
///
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory
|
||||
const OffsetSubtable = extern struct {
|
||||
/// Indicates the type of font file we're reading.
|
||||
/// - 0x00_01_00_00 ---- TrueType
|
||||
/// - 0x74_72_75_65 'true' TrueType
|
||||
/// - 0x4F_54_54_4F 'OTTO' OpenType
|
||||
/// - 0x74_79_70_31 'typ1' PostScript
|
||||
sfnt_version: uint32 align(1),
|
||||
/// Number of tables.
|
||||
num_tables: uint16 align(1),
|
||||
/// Maximum power of 2 less than or equal to numTables, times 16 ((2**floor(log2(numTables))) * 16, where “**” is an exponentiation operator).
|
||||
search_range: uint16 align(1),
|
||||
/// Log2 of the maximum power of 2 less than or equal to numTables (log2(searchRange/16), which is equal to floor(log2(numTables))).
|
||||
entry_selector: uint16 align(1),
|
||||
/// numTables times 16, minus searchRange ((numTables * 16) - searchRange).
|
||||
range_shift: uint16 align(1),
|
||||
|
||||
pub fn format(
|
||||
self: OffsetSubtable,
|
||||
comptime fmt: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
_ = fmt;
|
||||
_ = options;
|
||||
|
||||
try writer.print(
|
||||
"OffsetSubtable('{s}'){{ .num_tables = {} }}",
|
||||
.{
|
||||
if (self.sfnt_version == 0x00_01_00_00)
|
||||
&@as([10]u8, "0x00010000".*)
|
||||
else
|
||||
&@as([4]u8, @bitCast(
|
||||
std.mem.nativeToBig(u32, self.sfnt_version),
|
||||
)),
|
||||
self.num_tables,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const TableRecord = extern struct {
|
||||
/// Table identifier.
|
||||
tag: Tag align(1),
|
||||
/// Checksum for this table.
|
||||
checksum: uint32 align(1),
|
||||
/// Offset from beginning of font file.
|
||||
offset: Offset32 align(1),
|
||||
/// Length of this table.
|
||||
length: uint32 align(1),
|
||||
|
||||
pub fn format(
|
||||
self: TableRecord,
|
||||
comptime fmt: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
_ = fmt;
|
||||
_ = options;
|
||||
|
||||
try writer.print(
|
||||
"TableRecord(\"{s}\"){{ .checksum = {}, .offset = {}, .length = {} }}",
|
||||
.{
|
||||
self.tag,
|
||||
self.checksum,
|
||||
self.offset,
|
||||
self.length,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
directory: Directory,
|
||||
|
||||
data: []const u8,
|
||||
|
||||
/// Parse a font from raw data. The struct will keep a
|
||||
/// reference to `data` and use it for future operations.
|
||||
pub fn init(data: []const u8, alloc: Allocator) !SFNT {
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
|
||||
// SFNT files use big endian, if our native endian is
|
||||
// not big we'll need to byte swap the values we read.
|
||||
const byte_swap = native_endian != .big;
|
||||
|
||||
var directory: Directory = undefined;
|
||||
|
||||
try reader.readNoEof(std.mem.asBytes(&directory.offset));
|
||||
if (byte_swap) std.mem.byteSwapAllFields(
|
||||
Directory.OffsetSubtable,
|
||||
&directory.offset,
|
||||
);
|
||||
|
||||
directory.records = try alloc.alloc(Directory.TableRecord, directory.offset.num_tables);
|
||||
|
||||
try reader.readNoEof(std.mem.sliceAsBytes(directory.records));
|
||||
if (byte_swap) for (directory.records) |*record| {
|
||||
std.mem.byteSwapAllFields(
|
||||
Directory.TableRecord,
|
||||
record,
|
||||
);
|
||||
};
|
||||
|
||||
return .{
|
||||
.directory = directory,
|
||||
.data = data,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: SFNT, alloc: Allocator) void {
|
||||
alloc.free(self.directory.records);
|
||||
}
|
||||
|
||||
/// Returns the bytes of the table with the provided tag if present.
|
||||
pub fn getTable(self: SFNT, tag: *const [4]u8) ?[]const u8 {
|
||||
for (self.directory.records) |record| {
|
||||
if (std.mem.eql(u8, tag, &record.tag)) {
|
||||
return self.data[record.offset..][0..record.length];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const native_endian = @import("builtin").target.cpu.arch.endian();
|
||||
|
||||
test "parse font" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const test_font = @import("../embedded.zig").julia_mono;
|
||||
|
||||
const sfnt = try SFNT.init(&test_font.*, alloc);
|
||||
defer sfnt.deinit(alloc);
|
||||
|
||||
try testing.expectEqual(19, sfnt.directory.offset.num_tables);
|
||||
try testing.expectEqualStrings("prep", &sfnt.directory.records[18].tag);
|
||||
}
|
||||
|
||||
test "get table" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
const test_font = @import("../embedded.zig").julia_mono;
|
||||
|
||||
const sfnt = try SFNT.init(&test_font.*, alloc);
|
||||
defer sfnt.deinit(alloc);
|
||||
|
||||
const svg = sfnt.getTable("SVG ").?;
|
||||
|
||||
try testing.expectEqual(430, svg.len);
|
||||
}
|
@ -22,7 +22,10 @@ pub const SVG = struct {
|
||||
/// All records in the table.
|
||||
records: []const [12]u8,
|
||||
|
||||
pub fn init(data: []const u8) !SVG {
|
||||
pub fn init(data: []const u8) error{
|
||||
EndOfStream,
|
||||
SVGVersionNotSupported,
|
||||
}!SVG {
|
||||
var fbs = std.io.fixedBufferStream(data);
|
||||
const reader = fbs.reader();
|
||||
|
||||
|
@ -27,14 +27,8 @@ const Sprite = @import("../sprite.zig").Sprite;
|
||||
|
||||
const log = std.log.scoped(.box_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 lines of the box. This is in pixels. If you
|
||||
/// want to do any DPI scaling, it is expected to be done earlier.
|
||||
thickness: u32,
|
||||
/// Grid metrics for the rendering.
|
||||
metrics: font.Metrics,
|
||||
|
||||
/// The thickness of a line.
|
||||
const Thickness = enum {
|
||||
@ -218,8 +212,14 @@ pub fn renderGlyph(
|
||||
atlas: *font.Atlas,
|
||||
cp: u32,
|
||||
) !font.Glyph {
|
||||
const metrics = self.metrics;
|
||||
|
||||
// Create the canvas we'll use to draw
|
||||
var canvas = try font.sprite.Canvas.init(alloc, self.width, self.height);
|
||||
var canvas = try font.sprite.Canvas.init(
|
||||
alloc,
|
||||
metrics.cell_width,
|
||||
metrics.cell_height,
|
||||
);
|
||||
defer canvas.deinit(alloc);
|
||||
|
||||
// Perform the actual drawing
|
||||
@ -231,29 +231,16 @@ pub fn renderGlyph(
|
||||
// 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));
|
||||
const offset_y = @as(i32, @intCast(metrics.cell_height));
|
||||
|
||||
return font.Glyph{
|
||||
.width = self.width,
|
||||
.height = self.height,
|
||||
.width = metrics.cell_width,
|
||||
.height = metrics.cell_height,
|
||||
.offset_x = 0,
|
||||
.offset_y = offset_y,
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = @floatFromInt(self.width),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if this codepoint should be rendered with the
|
||||
/// width/height set to unadjusted values.
|
||||
pub fn unadjustedCodepoint(cp: u32) bool {
|
||||
return switch (cp) {
|
||||
@intFromEnum(Sprite.cursor_rect),
|
||||
@intFromEnum(Sprite.cursor_hollow_rect),
|
||||
@intFromEnum(Sprite.cursor_bar),
|
||||
=> true,
|
||||
|
||||
else => false,
|
||||
.advance_x = @floatFromInt(metrics.cell_width),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1637,12 +1624,6 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void
|
||||
.right = true,
|
||||
}, .light),
|
||||
|
||||
// Not official box characters but special characters we hide
|
||||
// in the high bits of a unicode codepoint.
|
||||
@intFromEnum(Sprite.cursor_rect) => self.draw_cursor_rect(canvas),
|
||||
@intFromEnum(Sprite.cursor_hollow_rect) => self.draw_cursor_hollow_rect(canvas),
|
||||
@intFromEnum(Sprite.cursor_bar) => self.draw_cursor_bar(canvas),
|
||||
|
||||
else => return error.InvalidCodepoint,
|
||||
}
|
||||
}
|
||||
@ -1652,16 +1633,16 @@ fn draw_lines(
|
||||
canvas: *font.sprite.Canvas,
|
||||
lines: Lines,
|
||||
) void {
|
||||
const light_px = Thickness.light.height(self.thickness);
|
||||
const heavy_px = Thickness.heavy.height(self.thickness);
|
||||
const light_px = Thickness.light.height(self.metrics.box_thickness);
|
||||
const heavy_px = Thickness.heavy.height(self.metrics.box_thickness);
|
||||
|
||||
// Top of light horizontal strokes
|
||||
const h_light_top = (self.height -| light_px) / 2;
|
||||
const h_light_top = (self.metrics.cell_height -| light_px) / 2;
|
||||
// Bottom of light horizontal strokes
|
||||
const h_light_bottom = h_light_top +| light_px;
|
||||
|
||||
// Top of heavy horizontal strokes
|
||||
const h_heavy_top = (self.height -| heavy_px) / 2;
|
||||
const h_heavy_top = (self.metrics.cell_height -| heavy_px) / 2;
|
||||
// Bottom of heavy horizontal strokes
|
||||
const h_heavy_bottom = h_heavy_top +| heavy_px;
|
||||
|
||||
@ -1671,12 +1652,12 @@ fn draw_lines(
|
||||
const h_double_bottom = h_light_bottom +| light_px;
|
||||
|
||||
// Left of light vertical strokes
|
||||
const v_light_left = (self.width -| light_px) / 2;
|
||||
const v_light_left = (self.metrics.cell_width -| light_px) / 2;
|
||||
// Right of light vertical strokes
|
||||
const v_light_right = v_light_left +| light_px;
|
||||
|
||||
// Left of heavy vertical strokes
|
||||
const v_heavy_left = (self.width -| heavy_px) / 2;
|
||||
const v_heavy_left = (self.metrics.cell_width -| heavy_px) / 2;
|
||||
// Right of heavy vertical strokes
|
||||
const v_heavy_right = v_heavy_left +| heavy_px;
|
||||
|
||||
@ -1752,27 +1733,27 @@ fn draw_lines(
|
||||
|
||||
switch (lines.right) {
|
||||
.none => {},
|
||||
.light => self.rect(canvas, right_left, h_light_top, self.width, h_light_bottom),
|
||||
.heavy => self.rect(canvas, right_left, h_heavy_top, self.width, h_heavy_bottom),
|
||||
.light => self.rect(canvas, right_left, h_light_top, self.metrics.cell_width, h_light_bottom),
|
||||
.heavy => self.rect(canvas, right_left, h_heavy_top, self.metrics.cell_width, h_heavy_bottom),
|
||||
.double => {
|
||||
const top_left = if (lines.up == .double) v_light_right else right_left;
|
||||
const bottom_left = if (lines.down == .double) v_light_right else right_left;
|
||||
|
||||
self.rect(canvas, top_left, h_double_top, self.width, h_light_top);
|
||||
self.rect(canvas, bottom_left, h_light_bottom, self.width, h_double_bottom);
|
||||
self.rect(canvas, top_left, h_double_top, self.metrics.cell_width, h_light_top);
|
||||
self.rect(canvas, bottom_left, h_light_bottom, self.metrics.cell_width, h_double_bottom);
|
||||
},
|
||||
}
|
||||
|
||||
switch (lines.down) {
|
||||
.none => {},
|
||||
.light => self.rect(canvas, v_light_left, down_top, v_light_right, self.height),
|
||||
.heavy => self.rect(canvas, v_heavy_left, down_top, v_heavy_right, self.height),
|
||||
.light => self.rect(canvas, v_light_left, down_top, v_light_right, self.metrics.cell_height),
|
||||
.heavy => self.rect(canvas, v_heavy_left, down_top, v_heavy_right, self.metrics.cell_height),
|
||||
.double => {
|
||||
const left_top = if (lines.left == .double) h_light_bottom else down_top;
|
||||
const right_top = if (lines.right == .double) h_light_bottom else down_top;
|
||||
|
||||
self.rect(canvas, v_double_left, left_top, v_light_left, self.height);
|
||||
self.rect(canvas, v_light_right, right_top, v_double_right, self.height);
|
||||
self.rect(canvas, v_double_left, left_top, v_light_left, self.metrics.cell_height);
|
||||
self.rect(canvas, v_light_right, right_top, v_double_right, self.metrics.cell_height);
|
||||
},
|
||||
}
|
||||
|
||||
@ -1794,8 +1775,8 @@ fn draw_light_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
|
||||
self.draw_dash_horizontal(
|
||||
canvas,
|
||||
3,
|
||||
Thickness.light.height(self.thickness),
|
||||
@max(4, Thickness.light.height(self.thickness)),
|
||||
Thickness.light.height(self.metrics.box_thickness),
|
||||
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1803,8 +1784,8 @@ fn draw_heavy_triple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
|
||||
self.draw_dash_horizontal(
|
||||
canvas,
|
||||
3,
|
||||
Thickness.heavy.height(self.thickness),
|
||||
@max(4, Thickness.light.height(self.thickness)),
|
||||
Thickness.heavy.height(self.metrics.box_thickness),
|
||||
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1812,8 +1793,8 @@ fn draw_light_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
|
||||
self.draw_dash_vertical(
|
||||
canvas,
|
||||
3,
|
||||
Thickness.light.height(self.thickness),
|
||||
@max(4, Thickness.light.height(self.thickness)),
|
||||
Thickness.light.height(self.metrics.box_thickness),
|
||||
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1821,8 +1802,8 @@ fn draw_heavy_triple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
|
||||
self.draw_dash_vertical(
|
||||
canvas,
|
||||
3,
|
||||
Thickness.heavy.height(self.thickness),
|
||||
@max(4, Thickness.light.height(self.thickness)),
|
||||
Thickness.heavy.height(self.metrics.box_thickness),
|
||||
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1830,8 +1811,8 @@ fn draw_light_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas)
|
||||
self.draw_dash_horizontal(
|
||||
canvas,
|
||||
4,
|
||||
Thickness.light.height(self.thickness),
|
||||
@max(4, Thickness.light.height(self.thickness)),
|
||||
Thickness.light.height(self.metrics.box_thickness),
|
||||
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1839,8 +1820,8 @@ fn draw_heavy_quadruple_dash_horizontal(self: Box, canvas: *font.sprite.Canvas)
|
||||
self.draw_dash_horizontal(
|
||||
canvas,
|
||||
4,
|
||||
Thickness.heavy.height(self.thickness),
|
||||
@max(4, Thickness.light.height(self.thickness)),
|
||||
Thickness.heavy.height(self.metrics.box_thickness),
|
||||
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1848,8 +1829,8 @@ fn draw_light_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) vo
|
||||
self.draw_dash_vertical(
|
||||
canvas,
|
||||
4,
|
||||
Thickness.light.height(self.thickness),
|
||||
@max(4, Thickness.light.height(self.thickness)),
|
||||
Thickness.light.height(self.metrics.box_thickness),
|
||||
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1857,8 +1838,8 @@ fn draw_heavy_quadruple_dash_vertical(self: Box, canvas: *font.sprite.Canvas) vo
|
||||
self.draw_dash_vertical(
|
||||
canvas,
|
||||
4,
|
||||
Thickness.heavy.height(self.thickness),
|
||||
@max(4, Thickness.light.height(self.thickness)),
|
||||
Thickness.heavy.height(self.metrics.box_thickness),
|
||||
@max(4, Thickness.light.height(self.metrics.box_thickness)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1866,8 +1847,8 @@ fn draw_light_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
|
||||
self.draw_dash_horizontal(
|
||||
canvas,
|
||||
2,
|
||||
Thickness.light.height(self.thickness),
|
||||
Thickness.light.height(self.thickness),
|
||||
Thickness.light.height(self.metrics.box_thickness),
|
||||
Thickness.light.height(self.metrics.box_thickness),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1875,8 +1856,8 @@ fn draw_heavy_double_dash_horizontal(self: Box, canvas: *font.sprite.Canvas) voi
|
||||
self.draw_dash_horizontal(
|
||||
canvas,
|
||||
2,
|
||||
Thickness.heavy.height(self.thickness),
|
||||
Thickness.heavy.height(self.thickness),
|
||||
Thickness.heavy.height(self.metrics.box_thickness),
|
||||
Thickness.heavy.height(self.metrics.box_thickness),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1884,8 +1865,8 @@ fn draw_light_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
|
||||
self.draw_dash_vertical(
|
||||
canvas,
|
||||
2,
|
||||
Thickness.light.height(self.thickness),
|
||||
Thickness.heavy.height(self.thickness),
|
||||
Thickness.light.height(self.metrics.box_thickness),
|
||||
Thickness.heavy.height(self.metrics.box_thickness),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1893,26 +1874,26 @@ fn draw_heavy_double_dash_vertical(self: Box, canvas: *font.sprite.Canvas) void
|
||||
self.draw_dash_vertical(
|
||||
canvas,
|
||||
2,
|
||||
Thickness.heavy.height(self.thickness),
|
||||
Thickness.heavy.height(self.thickness),
|
||||
Thickness.heavy.height(self.metrics.box_thickness),
|
||||
Thickness.heavy.height(self.metrics.box_thickness),
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_light_diagonal_upper_right_to_lower_left(self: Box, canvas: *font.sprite.Canvas) void {
|
||||
canvas.line(.{
|
||||
.p0 = .{ .x = @floatFromInt(self.width), .y = 0 },
|
||||
.p1 = .{ .x = 0, .y = @floatFromInt(self.height) },
|
||||
}, @floatFromInt(Thickness.light.height(self.thickness)), .on) catch {};
|
||||
.p0 = .{ .x = @floatFromInt(self.metrics.cell_width), .y = 0 },
|
||||
.p1 = .{ .x = 0, .y = @floatFromInt(self.metrics.cell_height) },
|
||||
}, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {};
|
||||
}
|
||||
|
||||
fn draw_light_diagonal_upper_left_to_lower_right(self: Box, canvas: *font.sprite.Canvas) void {
|
||||
canvas.line(.{
|
||||
.p0 = .{ .x = 0, .y = 0 },
|
||||
.p1 = .{
|
||||
.x = @floatFromInt(self.width),
|
||||
.y = @floatFromInt(self.height),
|
||||
.x = @floatFromInt(self.metrics.cell_width),
|
||||
.y = @floatFromInt(self.metrics.cell_height),
|
||||
},
|
||||
}, @floatFromInt(Thickness.light.height(self.thickness)), .on) catch {};
|
||||
}, @floatFromInt(Thickness.light.height(self.metrics.box_thickness)), .on) catch {};
|
||||
}
|
||||
|
||||
fn draw_light_diagonal_cross(self: Box, canvas: *font.sprite.Canvas) void {
|
||||
@ -1938,21 +1919,21 @@ fn draw_block_shade(
|
||||
comptime height: f64,
|
||||
comptime shade: Shade,
|
||||
) void {
|
||||
const float_width: f64 = @floatFromInt(self.width);
|
||||
const float_height: f64 = @floatFromInt(self.height);
|
||||
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
|
||||
const w: u32 = @intFromFloat(@round(float_width * width));
|
||||
const h: u32 = @intFromFloat(@round(float_height * height));
|
||||
|
||||
const x = switch (alignment.horizontal) {
|
||||
.left => 0,
|
||||
.right => self.width - w,
|
||||
.center => (self.width - w) / 2,
|
||||
.right => self.metrics.cell_width - w,
|
||||
.center => (self.metrics.cell_width - w) / 2,
|
||||
};
|
||||
const y = switch (alignment.vertical) {
|
||||
.top => 0,
|
||||
.bottom => self.height - h,
|
||||
.middle => (self.height - h) / 2,
|
||||
.bottom => self.metrics.cell_height - h,
|
||||
.middle => (self.metrics.cell_height - h) / 2,
|
||||
};
|
||||
|
||||
canvas.rect(.{
|
||||
@ -1970,10 +1951,10 @@ fn draw_corner_triangle_shade(
|
||||
comptime shade: Shade,
|
||||
) void {
|
||||
const x0, const y0, const x1, const y1, const x2, const y2 = switch (corner) {
|
||||
.tl => .{ 0, 0, 0, self.height, self.width, 0 },
|
||||
.tr => .{ 0, 0, self.width, self.height, self.width, 0 },
|
||||
.bl => .{ 0, 0, 0, self.height, self.width, self.height },
|
||||
.br => .{ 0, self.height, self.width, self.height, self.width, 0 },
|
||||
.tl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, 0 },
|
||||
.tr => .{ 0, 0, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 },
|
||||
.bl => .{ 0, 0, 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height },
|
||||
.br => .{ 0, self.metrics.cell_height, self.metrics.cell_width, self.metrics.cell_height, self.metrics.cell_width, 0 },
|
||||
};
|
||||
|
||||
canvas.triangle(.{
|
||||
@ -1984,26 +1965,26 @@ fn draw_corner_triangle_shade(
|
||||
}
|
||||
|
||||
fn draw_full_block(self: Box, canvas: *font.sprite.Canvas) void {
|
||||
self.rect(canvas, 0, 0, self.width, self.height);
|
||||
self.rect(canvas, 0, 0, self.metrics.cell_width, self.metrics.cell_height);
|
||||
}
|
||||
|
||||
fn draw_vertical_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void {
|
||||
const x = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.width)) / 8)));
|
||||
const w = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.width)) / 8)));
|
||||
self.rect(canvas, x, 0, x + w, self.height);
|
||||
const x = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_width)) / 8)));
|
||||
const w = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 8)));
|
||||
self.rect(canvas, x, 0, x + w, self.metrics.cell_height);
|
||||
}
|
||||
|
||||
fn draw_checkerboard_fill(self: Box, canvas: *font.sprite.Canvas, parity: u1) void {
|
||||
const float_width: f64 = @floatFromInt(self.width);
|
||||
const float_height: f64 = @floatFromInt(self.height);
|
||||
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
const x_size: usize = 4;
|
||||
const y_size: usize = @intFromFloat(@round(4 * (float_height / float_width)));
|
||||
for (0..x_size) |x| {
|
||||
const x0 = (self.width * x) / x_size;
|
||||
const x1 = (self.width * (x + 1)) / x_size;
|
||||
const x0 = (self.metrics.cell_width * x) / x_size;
|
||||
const x1 = (self.metrics.cell_width * (x + 1)) / x_size;
|
||||
for (0..y_size) |y| {
|
||||
const y0 = (self.height * y) / y_size;
|
||||
const y1 = (self.height * (y + 1)) / y_size;
|
||||
const y0 = (self.metrics.cell_height * y) / y_size;
|
||||
const y1 = (self.metrics.cell_height * (y + 1)) / y_size;
|
||||
if ((x + y) % 2 == parity) {
|
||||
canvas.rect(.{
|
||||
.x = @intCast(x0),
|
||||
@ -2017,11 +1998,11 @@ fn draw_checkerboard_fill(self: Box, canvas: *font.sprite.Canvas, parity: u1) vo
|
||||
}
|
||||
|
||||
fn draw_upper_left_to_lower_right_fill(self: Box, canvas: *font.sprite.Canvas) void {
|
||||
const thick_px = Thickness.light.height(self.thickness);
|
||||
const line_count = self.width / (2 * thick_px);
|
||||
const thick_px = Thickness.light.height(self.metrics.box_thickness);
|
||||
const line_count = self.metrics.cell_width / (2 * thick_px);
|
||||
|
||||
const float_width: f64 = @floatFromInt(self.width);
|
||||
const float_height: f64 = @floatFromInt(self.height);
|
||||
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
const float_thick: f64 = @floatFromInt(thick_px);
|
||||
const stride = @round(float_width / @as(f64, @floatFromInt(line_count)));
|
||||
|
||||
@ -2037,11 +2018,11 @@ fn draw_upper_left_to_lower_right_fill(self: Box, canvas: *font.sprite.Canvas) v
|
||||
}
|
||||
|
||||
fn draw_upper_right_to_lower_left_fill(self: Box, canvas: *font.sprite.Canvas) void {
|
||||
const thick_px = Thickness.light.height(self.thickness);
|
||||
const line_count = self.width / (2 * thick_px);
|
||||
const thick_px = Thickness.light.height(self.metrics.box_thickness);
|
||||
const line_count = self.metrics.cell_width / (2 * thick_px);
|
||||
|
||||
const float_width: f64 = @floatFromInt(self.width);
|
||||
const float_height: f64 = @floatFromInt(self.height);
|
||||
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
const float_thick: f64 = @floatFromInt(thick_px);
|
||||
const stride = @round(float_width / @as(f64, @floatFromInt(line_count)));
|
||||
|
||||
@ -2061,13 +2042,13 @@ fn draw_corner_diagonal_lines(
|
||||
canvas: *font.sprite.Canvas,
|
||||
comptime corners: Quads,
|
||||
) void {
|
||||
const thick_px = Thickness.light.height(self.thickness);
|
||||
const thick_px = Thickness.light.height(self.metrics.box_thickness);
|
||||
|
||||
const float_width: f64 = @floatFromInt(self.width);
|
||||
const float_height: f64 = @floatFromInt(self.height);
|
||||
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
const float_thick: f64 = @floatFromInt(thick_px);
|
||||
const center_x: f64 = @floatFromInt(self.width / 2 + self.width % 2);
|
||||
const center_y: f64 = @floatFromInt(self.height / 2 + self.height % 2);
|
||||
const center_x: f64 = @floatFromInt(self.metrics.cell_width / 2 + self.metrics.cell_width % 2);
|
||||
const center_y: f64 = @floatFromInt(self.metrics.cell_height / 2 + self.metrics.cell_height % 2);
|
||||
|
||||
if (corners.tl) canvas.line(.{
|
||||
.p0 = .{ .x = center_x, .y = 0 },
|
||||
@ -2096,8 +2077,8 @@ fn draw_cell_diagonal(
|
||||
comptime from: Alignment,
|
||||
comptime to: Alignment,
|
||||
) void {
|
||||
const float_width: f64 = @floatFromInt(self.width);
|
||||
const float_height: f64 = @floatFromInt(self.height);
|
||||
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
|
||||
const x0: f64 = switch (from.horizontal) {
|
||||
.left => 0,
|
||||
@ -2134,16 +2115,16 @@ fn draw_fading_line(
|
||||
comptime to: Edge,
|
||||
comptime thickness: Thickness,
|
||||
) void {
|
||||
const thick_px = thickness.height(self.thickness);
|
||||
const float_width: f64 = @floatFromInt(self.width);
|
||||
const float_height: f64 = @floatFromInt(self.height);
|
||||
const thick_px = thickness.height(self.metrics.box_thickness);
|
||||
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
|
||||
// Top of horizontal strokes
|
||||
const h_top = (self.height -| thick_px) / 2;
|
||||
const h_top = (self.metrics.cell_height -| thick_px) / 2;
|
||||
// Bottom of horizontal strokes
|
||||
const h_bottom = h_top +| thick_px;
|
||||
// Left of vertical strokes
|
||||
const v_left = (self.width -| thick_px) / 2;
|
||||
const v_left = (self.metrics.cell_width -| thick_px) / 2;
|
||||
// Right of vertical strokes
|
||||
const v_right = v_left +| thick_px;
|
||||
|
||||
@ -2163,7 +2144,7 @@ fn draw_fading_line(
|
||||
|
||||
switch (to) {
|
||||
.top, .bottom => {
|
||||
for (0..self.height) |y| {
|
||||
for (0..self.metrics.cell_height) |y| {
|
||||
for (v_left..v_right) |x| {
|
||||
canvas.pixel(
|
||||
@intCast(x),
|
||||
@ -2175,7 +2156,7 @@ fn draw_fading_line(
|
||||
}
|
||||
},
|
||||
.left, .right => {
|
||||
for (0..self.width) |x| {
|
||||
for (0..self.metrics.cell_width) |x| {
|
||||
for (h_top..h_bottom) |y| {
|
||||
canvas.pixel(
|
||||
@intCast(x),
|
||||
@ -2195,17 +2176,17 @@ fn draw_branch_node(
|
||||
node: BranchNode,
|
||||
comptime thickness: Thickness,
|
||||
) void {
|
||||
const thick_px = thickness.height(self.thickness);
|
||||
const float_width: f64 = @floatFromInt(self.width);
|
||||
const float_height: f64 = @floatFromInt(self.height);
|
||||
const thick_px = thickness.height(self.metrics.box_thickness);
|
||||
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
const float_thick: f64 = @floatFromInt(thick_px);
|
||||
|
||||
// Top of horizontal strokes
|
||||
const h_top = (self.height -| thick_px) / 2;
|
||||
const h_top = (self.metrics.cell_height -| thick_px) / 2;
|
||||
// Bottom of horizontal strokes
|
||||
const h_bottom = h_top +| thick_px;
|
||||
// Left of vertical strokes
|
||||
const v_left = (self.width -| thick_px) / 2;
|
||||
const v_left = (self.metrics.cell_width -| thick_px) / 2;
|
||||
// Right of vertical strokes
|
||||
const v_right = v_left +| thick_px;
|
||||
|
||||
@ -2240,9 +2221,9 @@ fn draw_branch_node(
|
||||
if (node.up)
|
||||
self.rect(canvas, v_left, 0, v_right, @intFromFloat(@ceil(cy - r)));
|
||||
if (node.right)
|
||||
self.rect(canvas, @intFromFloat(@floor(cx + r)), h_top, self.width, h_bottom);
|
||||
self.rect(canvas, @intFromFloat(@floor(cx + r)), h_top, self.metrics.cell_width, h_bottom);
|
||||
if (node.down)
|
||||
self.rect(canvas, v_left, @intFromFloat(@floor(cy + r)), v_right, self.height);
|
||||
self.rect(canvas, v_left, @intFromFloat(@floor(cy + r)), v_right, self.metrics.cell_height);
|
||||
if (node.left)
|
||||
self.rect(canvas, 0, h_top, @intFromFloat(@ceil(cx - r)), h_bottom);
|
||||
|
||||
@ -2263,8 +2244,8 @@ fn draw_circle(
|
||||
comptime position: Alignment,
|
||||
comptime filled: bool,
|
||||
) void {
|
||||
const float_width: f64 = @floatFromInt(self.width);
|
||||
const float_height: f64 = @floatFromInt(self.height);
|
||||
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
|
||||
const x: f64 = switch (position.horizontal) {
|
||||
.left => 0,
|
||||
@ -2285,7 +2266,7 @@ fn draw_circle(
|
||||
.pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } },
|
||||
},
|
||||
},
|
||||
.line_width = @floatFromInt(Thickness.light.height(self.thickness)),
|
||||
.line_width = @floatFromInt(Thickness.light.height(self.metrics.box_thickness)),
|
||||
};
|
||||
|
||||
var path = z2d.Path.init(canvas.alloc);
|
||||
@ -2311,7 +2292,7 @@ fn draw_line(
|
||||
) !void {
|
||||
canvas.line(
|
||||
.{ .p0 = p0, .p1 = p1 },
|
||||
@floatFromInt(thickness.height(self.thickness)),
|
||||
@floatFromInt(thickness.height(self.metrics.box_thickness)),
|
||||
.on,
|
||||
) catch {};
|
||||
}
|
||||
@ -2320,8 +2301,8 @@ fn draw_shade(self: Box, canvas: *font.sprite.Canvas, v: u16) void {
|
||||
canvas.rect((font.sprite.Box(u32){
|
||||
.p0 = .{ .x = 0, .y = 0 },
|
||||
.p1 = .{
|
||||
.x = self.width,
|
||||
.y = self.height,
|
||||
.x = self.metrics.cell_width,
|
||||
.y = self.metrics.cell_height,
|
||||
},
|
||||
}).rect(), @as(font.sprite.Color, @enumFromInt(v)));
|
||||
}
|
||||
@ -2339,12 +2320,12 @@ fn draw_dark_shade(self: Box, canvas: *font.sprite.Canvas) void {
|
||||
}
|
||||
|
||||
fn draw_horizontal_one_eighth_block_n(self: Box, canvas: *font.sprite.Canvas, n: u32) void {
|
||||
const h = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.height)) / 8)));
|
||||
const h = @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 8)));
|
||||
const y = @min(
|
||||
self.height -| h,
|
||||
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.height)) / 8))),
|
||||
self.metrics.cell_height -| h,
|
||||
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(n)) * @as(f64, @floatFromInt(self.metrics.cell_height)) / 8))),
|
||||
);
|
||||
self.rect(canvas, 0, y, self.width, y + h);
|
||||
self.rect(canvas, 0, y, self.metrics.cell_width, y + h);
|
||||
}
|
||||
|
||||
fn draw_horizontal_one_eighth_1358_block(self: Box, canvas: *font.sprite.Canvas) void {
|
||||
@ -2355,24 +2336,24 @@ fn draw_horizontal_one_eighth_1358_block(self: Box, canvas: *font.sprite.Canvas)
|
||||
}
|
||||
|
||||
fn draw_quadrant(self: Box, canvas: *font.sprite.Canvas, comptime quads: Quads) void {
|
||||
const center_x = self.width / 2 + self.width % 2;
|
||||
const center_y = self.height / 2 + self.height % 2;
|
||||
const center_x = self.metrics.cell_width / 2 + self.metrics.cell_width % 2;
|
||||
const center_y = self.metrics.cell_height / 2 + self.metrics.cell_height % 2;
|
||||
|
||||
if (quads.tl) self.rect(canvas, 0, 0, center_x, center_y);
|
||||
if (quads.tr) self.rect(canvas, center_x, 0, self.width, center_y);
|
||||
if (quads.bl) self.rect(canvas, 0, center_y, center_x, self.height);
|
||||
if (quads.br) self.rect(canvas, center_x, center_y, self.width, self.height);
|
||||
if (quads.tr) self.rect(canvas, center_x, 0, self.metrics.cell_width, center_y);
|
||||
if (quads.bl) self.rect(canvas, 0, center_y, center_x, self.metrics.cell_height);
|
||||
if (quads.br) self.rect(canvas, center_x, center_y, self.metrics.cell_width, self.metrics.cell_height);
|
||||
}
|
||||
|
||||
fn draw_braille(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
|
||||
var w: u32 = @min(self.width / 4, self.height / 8);
|
||||
var x_spacing: u32 = self.width / 4;
|
||||
var y_spacing: u32 = self.height / 8;
|
||||
var w: u32 = @min(self.metrics.cell_width / 4, self.metrics.cell_height / 8);
|
||||
var x_spacing: u32 = self.metrics.cell_width / 4;
|
||||
var y_spacing: u32 = self.metrics.cell_height / 8;
|
||||
var x_margin: u32 = x_spacing / 2;
|
||||
var y_margin: u32 = y_spacing / 2;
|
||||
|
||||
var x_px_left: u32 = self.width - 2 * x_margin - x_spacing - 2 * w;
|
||||
var y_px_left: u32 = self.height - 2 * y_margin - 3 * y_spacing - 4 * w;
|
||||
var x_px_left: u32 = self.metrics.cell_width - 2 * x_margin - x_spacing - 2 * w;
|
||||
var y_px_left: u32 = self.metrics.cell_height - 2 * y_margin - 3 * y_spacing - 4 * w;
|
||||
|
||||
// First, try hard to ensure the DOT width is non-zero
|
||||
if (x_px_left >= 2 and y_px_left >= 4 and w == 0) {
|
||||
@ -2419,8 +2400,8 @@ fn draw_braille(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
|
||||
}
|
||||
|
||||
assert(x_px_left <= 1 or y_px_left <= 1);
|
||||
assert(2 * x_margin + 2 * w + x_spacing <= self.width);
|
||||
assert(2 * y_margin + 4 * w + 3 * y_spacing <= self.height);
|
||||
assert(2 * x_margin + 2 * w + x_spacing <= self.metrics.cell_width);
|
||||
assert(2 * y_margin + 4 * w + 3 * y_spacing <= self.metrics.cell_height);
|
||||
|
||||
const x = [2]u32{ x_margin, x_margin + w + x_spacing };
|
||||
const y = y: {
|
||||
@ -2479,25 +2460,25 @@ fn draw_sextant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
|
||||
const y_thirds = self.yThirds();
|
||||
|
||||
if (sex.tl) self.rect(canvas, 0, 0, x_halfs[0], y_thirds[0]);
|
||||
if (sex.tr) self.rect(canvas, x_halfs[1], 0, self.width, y_thirds[0]);
|
||||
if (sex.tr) self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_thirds[0]);
|
||||
if (sex.ml) self.rect(canvas, 0, y_thirds[0], x_halfs[0], y_thirds[1]);
|
||||
if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[0], self.width, y_thirds[1]);
|
||||
if (sex.bl) self.rect(canvas, 0, y_thirds[1], x_halfs[0], self.height);
|
||||
if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[1], self.width, self.height);
|
||||
if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[0], self.metrics.cell_width, y_thirds[1]);
|
||||
if (sex.bl) self.rect(canvas, 0, y_thirds[1], x_halfs[0], self.metrics.cell_height);
|
||||
if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[1], self.metrics.cell_width, self.metrics.cell_height);
|
||||
}
|
||||
|
||||
fn xHalfs(self: Box) [2]u32 {
|
||||
return .{
|
||||
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.width)) / 2))),
|
||||
@as(u32, @intFromFloat(@as(f64, @floatFromInt(self.width)) / 2)),
|
||||
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2))),
|
||||
@as(u32, @intFromFloat(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2)),
|
||||
};
|
||||
}
|
||||
|
||||
fn yThirds(self: Box) [2]u32 {
|
||||
return switch (@mod(self.height, 3)) {
|
||||
0 => .{ self.height / 3, 2 * self.height / 3 },
|
||||
1 => .{ self.height / 3, 2 * self.height / 3 + 1 },
|
||||
2 => .{ self.height / 3 + 1, 2 * self.height / 3 },
|
||||
return switch (@mod(self.metrics.cell_height, 3)) {
|
||||
0 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 },
|
||||
1 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 + 1 },
|
||||
2 => .{ self.metrics.cell_height / 3 + 1, 2 * self.metrics.cell_height / 3 },
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
@ -2511,10 +2492,10 @@ fn draw_smooth_mosaic(
|
||||
const top: f64 = 0.0;
|
||||
const upper: f64 = @floatFromInt(y_thirds[0]);
|
||||
const lower: f64 = @floatFromInt(y_thirds[1]);
|
||||
const bottom: f64 = @floatFromInt(self.height);
|
||||
const bottom: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
const left: f64 = 0.0;
|
||||
const center: f64 = @round(@as(f64, @floatFromInt(self.width)) / 2);
|
||||
const right: f64 = @floatFromInt(self.width);
|
||||
const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2);
|
||||
const right: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
|
||||
var path = z2d.Path.init(canvas.alloc);
|
||||
defer path.deinit();
|
||||
@ -2549,11 +2530,11 @@ fn draw_edge_triangle(
|
||||
comptime edge: Edge,
|
||||
) !void {
|
||||
const upper: f64 = 0.0;
|
||||
const middle: f64 = @round(@as(f64, @floatFromInt(self.height)) / 2);
|
||||
const lower: f64 = @floatFromInt(self.height);
|
||||
const middle: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_height)) / 2);
|
||||
const lower: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
const left: f64 = 0.0;
|
||||
const center: f64 = @round(@as(f64, @floatFromInt(self.width)) / 2);
|
||||
const right: f64 = @floatFromInt(self.width);
|
||||
const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2);
|
||||
const right: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
|
||||
var path = z2d.Path.init(canvas.alloc);
|
||||
defer path.deinit();
|
||||
@ -2588,12 +2569,12 @@ fn draw_arc(
|
||||
comptime corner: Corner,
|
||||
comptime thickness: Thickness,
|
||||
) !void {
|
||||
const thick_px = thickness.height(self.thickness);
|
||||
const float_width: f64 = @floatFromInt(self.width);
|
||||
const float_height: f64 = @floatFromInt(self.height);
|
||||
const thick_px = thickness.height(self.metrics.box_thickness);
|
||||
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
|
||||
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
|
||||
const float_thick: f64 = @floatFromInt(thick_px);
|
||||
const center_x: f64 = @as(f64, @floatFromInt((self.width -| thick_px) / 2)) + float_thick / 2;
|
||||
const center_y: f64 = @as(f64, @floatFromInt((self.height -| thick_px) / 2)) + float_thick / 2;
|
||||
const center_x: f64 = @as(f64, @floatFromInt((self.metrics.cell_width -| thick_px) / 2)) + float_thick / 2;
|
||||
const center_y: f64 = @as(f64, @floatFromInt((self.metrics.cell_height -| thick_px) / 2)) + float_thick / 2;
|
||||
|
||||
const r = @min(float_width, float_height) / 2;
|
||||
|
||||
@ -2703,23 +2684,23 @@ fn draw_dash_horizontal(
|
||||
// We need at least 1 pixel for each gap and each dash, if we don't
|
||||
// have that then we can't draw our dashed line correctly so we just
|
||||
// draw a solid line and return.
|
||||
if (self.width < count + gap_count) {
|
||||
if (self.metrics.cell_width < count + gap_count) {
|
||||
self.hline_middle(canvas, .light);
|
||||
return;
|
||||
}
|
||||
|
||||
// We never want the gaps to take up more than 50% of the space,
|
||||
// because if they do the dashes are too small and look wrong.
|
||||
const gap_width = @min(desired_gap, self.width / (2 * count));
|
||||
const gap_width = @min(desired_gap, self.metrics.cell_width / (2 * count));
|
||||
const total_gap_width = gap_count * gap_width;
|
||||
const total_dash_width = self.width - total_gap_width;
|
||||
const total_dash_width = self.metrics.cell_width - total_gap_width;
|
||||
const dash_width = total_dash_width / count;
|
||||
const remaining = total_dash_width % count;
|
||||
|
||||
assert(dash_width * count + gap_width * gap_count + remaining == self.width);
|
||||
assert(dash_width * count + gap_width * gap_count + remaining == self.metrics.cell_width);
|
||||
|
||||
// Our dashes should be centered vertically.
|
||||
const y: u32 = (self.height -| thick_px) / 2;
|
||||
const y: u32 = (self.metrics.cell_height -| thick_px) / 2;
|
||||
|
||||
// We start at half a gap from the left edge, in order to center
|
||||
// our dashes properly.
|
||||
@ -2782,23 +2763,23 @@ fn draw_dash_vertical(
|
||||
// We need at least 1 pixel for each gap and each dash, if we don't
|
||||
// have that then we can't draw our dashed line correctly so we just
|
||||
// draw a solid line and return.
|
||||
if (self.height < count + gap_count) {
|
||||
if (self.metrics.cell_height < count + gap_count) {
|
||||
self.vline_middle(canvas, .light);
|
||||
return;
|
||||
}
|
||||
|
||||
// We never want the gaps to take up more than 50% of the space,
|
||||
// because if they do the dashes are too small and look wrong.
|
||||
const gap_height = @min(desired_gap, self.height / (2 * count));
|
||||
const gap_height = @min(desired_gap, self.metrics.cell_height / (2 * count));
|
||||
const total_gap_height = gap_count * gap_height;
|
||||
const total_dash_height = self.height - total_gap_height;
|
||||
const total_dash_height = self.metrics.cell_height - total_gap_height;
|
||||
const dash_height = total_dash_height / count;
|
||||
const remaining = total_dash_height % count;
|
||||
|
||||
assert(dash_height * count + gap_height * gap_count + remaining == self.height);
|
||||
assert(dash_height * count + gap_height * gap_count + remaining == self.metrics.cell_height);
|
||||
|
||||
// Our dashes should be centered horizontally.
|
||||
const x: u32 = (self.width -| thick_px) / 2;
|
||||
const x: u32 = (self.metrics.cell_width -| thick_px) / 2;
|
||||
|
||||
// We start at the top of the cell.
|
||||
var y: u32 = 0;
|
||||
@ -2823,33 +2804,14 @@ fn draw_dash_vertical(
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_cursor_rect(self: Box, canvas: *font.sprite.Canvas) void {
|
||||
self.rect(canvas, 0, 0, self.width, self.height);
|
||||
}
|
||||
|
||||
fn draw_cursor_hollow_rect(self: Box, canvas: *font.sprite.Canvas) void {
|
||||
const thick_px = Thickness.super_light.height(self.thickness);
|
||||
|
||||
self.vline(canvas, 0, self.height, 0, thick_px);
|
||||
self.vline(canvas, 0, self.height, self.width -| thick_px, thick_px);
|
||||
self.hline(canvas, 0, self.width, 0, thick_px);
|
||||
self.hline(canvas, 0, self.width, self.height -| thick_px, thick_px);
|
||||
}
|
||||
|
||||
fn draw_cursor_bar(self: Box, canvas: *font.sprite.Canvas) void {
|
||||
const thick_px = Thickness.light.height(self.thickness);
|
||||
|
||||
self.vline(canvas, 0, self.height, 0, thick_px);
|
||||
}
|
||||
|
||||
fn vline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void {
|
||||
const thick_px = thickness.height(self.thickness);
|
||||
self.vline(canvas, 0, self.height, (self.width -| thick_px) / 2, thick_px);
|
||||
const thick_px = thickness.height(self.metrics.box_thickness);
|
||||
self.vline(canvas, 0, self.metrics.cell_height, (self.metrics.cell_width -| thick_px) / 2, thick_px);
|
||||
}
|
||||
|
||||
fn hline_middle(self: Box, canvas: *font.sprite.Canvas, thickness: Thickness) void {
|
||||
const thick_px = thickness.height(self.thickness);
|
||||
self.hline(canvas, 0, self.width, (self.height -| thick_px) / 2, thick_px);
|
||||
const thick_px = thickness.height(self.metrics.box_thickness);
|
||||
self.hline(canvas, 0, self.metrics.cell_width, (self.metrics.cell_height -| thick_px) / 2, thick_px);
|
||||
}
|
||||
|
||||
fn vline(
|
||||
@ -2861,11 +2823,11 @@ fn vline(
|
||||
thickness_px: u32,
|
||||
) void {
|
||||
canvas.rect((font.sprite.Box(u32){ .p0 = .{
|
||||
.x = @min(@max(x, 0), self.width),
|
||||
.y = @min(@max(y1, 0), self.height),
|
||||
.x = @min(@max(x, 0), self.metrics.cell_width),
|
||||
.y = @min(@max(y1, 0), self.metrics.cell_height),
|
||||
}, .p1 = .{
|
||||
.x = @min(@max(x + thickness_px, 0), self.width),
|
||||
.y = @min(@max(y2, 0), self.height),
|
||||
.x = @min(@max(x + thickness_px, 0), self.metrics.cell_width),
|
||||
.y = @min(@max(y2, 0), self.metrics.cell_height),
|
||||
} }).rect(), .on);
|
||||
}
|
||||
|
||||
@ -2878,11 +2840,11 @@ fn hline(
|
||||
thickness_px: u32,
|
||||
) void {
|
||||
canvas.rect((font.sprite.Box(u32){ .p0 = .{
|
||||
.x = @min(@max(x1, 0), self.width),
|
||||
.y = @min(@max(y, 0), self.height),
|
||||
.x = @min(@max(x1, 0), self.metrics.cell_width),
|
||||
.y = @min(@max(y, 0), self.metrics.cell_height),
|
||||
}, .p1 = .{
|
||||
.x = @min(@max(x2, 0), self.width),
|
||||
.y = @min(@max(y + thickness_px, 0), self.height),
|
||||
.x = @min(@max(x2, 0), self.metrics.cell_width),
|
||||
.y = @min(@max(y + thickness_px, 0), self.metrics.cell_height),
|
||||
} }).rect(), .on);
|
||||
}
|
||||
|
||||
@ -2895,11 +2857,11 @@ fn rect(
|
||||
y2: u32,
|
||||
) void {
|
||||
canvas.rect((font.sprite.Box(u32){ .p0 = .{
|
||||
.x = @min(@max(x1, 0), self.width),
|
||||
.y = @min(@max(y1, 0), self.height),
|
||||
.x = @min(@max(x1, 0), self.metrics.cell_width),
|
||||
.y = @min(@max(y1, 0), self.metrics.cell_height),
|
||||
}, .p1 = .{
|
||||
.x = @min(@max(x2, 0), self.width),
|
||||
.y = @min(@max(y2, 0), self.height),
|
||||
.x = @min(@max(x2, 0), self.metrics.cell_width),
|
||||
.y = @min(@max(y2, 0), self.metrics.cell_height),
|
||||
} }).rect(), .on);
|
||||
}
|
||||
|
||||
@ -2913,14 +2875,21 @@ test "all" {
|
||||
var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale);
|
||||
defer atlas_grayscale.deinit(alloc);
|
||||
|
||||
const face: Box = .{ .width = 18, .height = 36, .thickness = 2 };
|
||||
const face: Box = .{
|
||||
.metrics = font.Metrics.calc(.{
|
||||
.cell_width = 18.0,
|
||||
.ascent = 30.0,
|
||||
.descent = -6.0,
|
||||
.line_gap = 0.0,
|
||||
}),
|
||||
};
|
||||
const glyph = try face.renderGlyph(
|
||||
alloc,
|
||||
&atlas_grayscale,
|
||||
cp,
|
||||
);
|
||||
try testing.expectEqual(@as(u32, face.width), glyph.width);
|
||||
try testing.expectEqual(@as(u32, face.height), glyph.height);
|
||||
try testing.expectEqual(@as(u32, face.metrics.cell_width), glyph.width);
|
||||
try testing.expectEqual(@as(u32, face.metrics.cell_height), glyph.height);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3037,18 +3006,28 @@ test "render all sprites" {
|
||||
var atlas_grayscale = try font.Atlas.init(alloc, 1024, .grayscale);
|
||||
defer atlas_grayscale.deinit(alloc);
|
||||
|
||||
// Even cell size and thickness
|
||||
// Even cell size and thickness (18 x 36)
|
||||
try (Box{
|
||||
.width = 18,
|
||||
.height = 36,
|
||||
.thickness = 2,
|
||||
.metrics = font.Metrics.calc(.{
|
||||
.cell_width = 18.0,
|
||||
.ascent = 30.0,
|
||||
.descent = -6.0,
|
||||
.line_gap = 0.0,
|
||||
.underline_thickness = 2.0,
|
||||
.strikethrough_thickness = 2.0,
|
||||
}),
|
||||
}).testRenderAll(alloc, &atlas_grayscale);
|
||||
|
||||
// Odd cell size and thickness
|
||||
// Odd cell size and thickness (9 x 15)
|
||||
try (Box{
|
||||
.width = 9,
|
||||
.height = 15,
|
||||
.thickness = 1,
|
||||
.metrics = font.Metrics.calc(.{
|
||||
.cell_width = 9.0,
|
||||
.ascent = 12.0,
|
||||
.descent = -3.0,
|
||||
.line_gap = 0.0,
|
||||
.underline_thickness = 1.0,
|
||||
.strikethrough_thickness = 1.0,
|
||||
}),
|
||||
}).testRenderAll(alloc, &atlas_grayscale);
|
||||
|
||||
const ground_truth = @embedFile("./testdata/Box.ppm");
|
||||
|
@ -21,25 +21,12 @@ 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 log = std.log.scoped(.font_sprite);
|
||||
|
||||
/// The cell width and height.
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
/// Base thickness value for lines of sprites. This is in pixels. If you
|
||||
/// want to do any DPI scaling, it is expected to be done earlier.
|
||||
thickness: u32 = 1,
|
||||
|
||||
/// The position of the underline.
|
||||
underline_position: u32 = 0,
|
||||
|
||||
/// The position of the strikethrough.
|
||||
// NOTE(mitchellh): We don't use a dedicated strikethrough thickness
|
||||
// setting yet but fonts can in theory set this. If this becomes an
|
||||
// issue in practice we can add it here.
|
||||
strikethrough_position: u32 = 0,
|
||||
/// Grid metrics for rendering sprites.
|
||||
metrics: font.Metrics,
|
||||
|
||||
/// Returns true if the codepoint exists in our sprite font.
|
||||
pub fn hasCodepoint(self: Face, cp: u32, p: ?font.Presentation) bool {
|
||||
@ -65,10 +52,12 @@ pub fn renderGlyph(
|
||||
}
|
||||
}
|
||||
|
||||
const metrics = opts.grid_metrics orelse self.metrics;
|
||||
|
||||
// We adjust our sprite width based on the cell width.
|
||||
const width = switch (opts.cell_width orelse 1) {
|
||||
0, 1 => self.width,
|
||||
else => |width| self.width * width,
|
||||
0, 1 => metrics.cell_width,
|
||||
else => |width| metrics.cell_width * width,
|
||||
};
|
||||
|
||||
// It should be impossible for this to be null and we assert that
|
||||
@ -86,58 +75,16 @@ pub fn renderGlyph(
|
||||
|
||||
// Safe to ".?" because of the above assertion.
|
||||
return switch (kind) {
|
||||
.box => box: {
|
||||
const thickness = switch (cp) {
|
||||
@intFromEnum(Sprite.cursor_rect),
|
||||
@intFromEnum(Sprite.cursor_hollow_rect),
|
||||
@intFromEnum(Sprite.cursor_bar),
|
||||
=> if (opts.grid_metrics) |m| m.cursor_thickness else self.thickness,
|
||||
else => self.thickness,
|
||||
};
|
||||
|
||||
const f: Box, const y_offset: u32 = face: {
|
||||
// Expected, usual values.
|
||||
var f: Box = .{
|
||||
.width = width,
|
||||
.height = self.height,
|
||||
.thickness = thickness,
|
||||
};
|
||||
|
||||
// If the codepoint is unadjusted then we want to adjust
|
||||
// (heh) the width/height to the proper size and also record
|
||||
// an offset to apply to our final glyph so it renders in the
|
||||
// correct place because renderGlyph assumes full size.
|
||||
var y_offset: u32 = 0;
|
||||
if (Box.unadjustedCodepoint(cp)) unadjust: {
|
||||
const metrics = opts.grid_metrics orelse break :unadjust;
|
||||
const height = metrics.original_cell_height orelse break :unadjust;
|
||||
|
||||
// If our height shrunk, then we use the original adjusted
|
||||
// height because we don't want to overflow the cell.
|
||||
if (height >= self.height) break :unadjust;
|
||||
|
||||
// The offset is divided by two because it is vertically
|
||||
// centered.
|
||||
y_offset = (self.height - height) / 2;
|
||||
f.height = height;
|
||||
}
|
||||
|
||||
break :face .{ f, y_offset };
|
||||
};
|
||||
|
||||
var g = try f.renderGlyph(alloc, atlas, cp);
|
||||
g.offset_y += @intCast(y_offset);
|
||||
break :box g;
|
||||
},
|
||||
.box => (Box{ .metrics = metrics }).renderGlyph(alloc, atlas, cp),
|
||||
|
||||
.underline => try underline.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
@enumFromInt(cp),
|
||||
width,
|
||||
self.height,
|
||||
self.underline_position,
|
||||
self.thickness,
|
||||
metrics.cell_height,
|
||||
metrics.underline_position,
|
||||
metrics.underline_thickness,
|
||||
),
|
||||
|
||||
.strikethrough => try underline.renderGlyph(
|
||||
@ -145,30 +92,67 @@ pub fn renderGlyph(
|
||||
atlas,
|
||||
@enumFromInt(cp),
|
||||
width,
|
||||
self.height,
|
||||
self.strikethrough_position,
|
||||
self.thickness,
|
||||
metrics.cell_height,
|
||||
metrics.strikethrough_position,
|
||||
metrics.strikethrough_thickness,
|
||||
),
|
||||
|
||||
.overline => try underline.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
@enumFromInt(cp),
|
||||
width,
|
||||
self.height,
|
||||
0,
|
||||
self.thickness,
|
||||
),
|
||||
.overline => overline: {
|
||||
var g = try underline.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
@enumFromInt(cp),
|
||||
width,
|
||||
metrics.cell_height,
|
||||
0,
|
||||
metrics.overline_thickness,
|
||||
);
|
||||
|
||||
// We have to manually subtract the overline position
|
||||
// on the rendered glyph since it can be negative.
|
||||
g.offset_y -= metrics.overline_position;
|
||||
|
||||
break :overline g;
|
||||
},
|
||||
|
||||
.powerline => powerline: {
|
||||
const f: Powerline = .{
|
||||
.width = width,
|
||||
.height = self.height,
|
||||
.thickness = self.thickness,
|
||||
.width = metrics.cell_width,
|
||||
.height = metrics.cell_height,
|
||||
.thickness = metrics.box_thickness,
|
||||
};
|
||||
|
||||
break :powerline try f.renderGlyph(alloc, atlas, cp);
|
||||
},
|
||||
|
||||
.cursor => cursor: {
|
||||
// Cursors should be drawn with the original cell height if
|
||||
// it has been adjusted larger, so they don't get stretched.
|
||||
const height, const dy = adjust: {
|
||||
const h = metrics.cell_height;
|
||||
if (metrics.original_cell_height) |original| {
|
||||
if (h > original) {
|
||||
break :adjust .{ original, (h - original) / 2 };
|
||||
}
|
||||
}
|
||||
break :adjust .{ h, 0 };
|
||||
};
|
||||
|
||||
var g = try cursor.renderGlyph(
|
||||
alloc,
|
||||
atlas,
|
||||
@enumFromInt(cp),
|
||||
width,
|
||||
height,
|
||||
metrics.cursor_thickness,
|
||||
);
|
||||
|
||||
// Keep the cursor centered in the cell if it's shorter.
|
||||
g.offset_y += @intCast(dy);
|
||||
|
||||
break :cursor g;
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@ -179,6 +163,7 @@ const Kind = enum {
|
||||
overline,
|
||||
strikethrough,
|
||||
powerline,
|
||||
cursor,
|
||||
|
||||
pub fn init(cp: u32) ?Kind {
|
||||
return switch (cp) {
|
||||
@ -199,7 +184,7 @@ const Kind = enum {
|
||||
.cursor_rect,
|
||||
.cursor_hollow_rect,
|
||||
.cursor_bar,
|
||||
=> .box,
|
||||
=> .cursor,
|
||||
},
|
||||
|
||||
// == Box fonts ==
|
||||
|
61
src/font/sprite/cursor.zig
Normal file
61
src/font/sprite/cursor.zig
Normal file
@ -0,0 +1,61 @@
|
||||
//! 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(alloc);
|
||||
|
||||
// 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{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.offset_x = 0,
|
||||
.offset_y = @intCast(height),
|
||||
.atlas_x = region.x,
|
||||
.atlas_y = region.y,
|
||||
.advance_x = @floatFromInt(width),
|
||||
};
|
||||
}
|
@ -3,6 +3,7 @@ const builtin = @import("builtin");
|
||||
|
||||
const mouse = @import("input/mouse.zig");
|
||||
const key = @import("input/key.zig");
|
||||
const keyboard = @import("input/keyboard.zig");
|
||||
|
||||
pub const function_keys = @import("input/function_keys.zig");
|
||||
pub const keycodes = @import("input/keycodes.zig");
|
||||
@ -13,6 +14,7 @@ pub const Action = key.Action;
|
||||
pub const Binding = @import("input/Binding.zig");
|
||||
pub const Link = @import("input/Link.zig");
|
||||
pub const Key = key.Key;
|
||||
pub const KeyboardLayout = keyboard.Layout;
|
||||
pub const KeyEncoder = @import("input/KeyEncoder.zig");
|
||||
pub const KeyEvent = key.KeyEvent;
|
||||
pub const InspectorMode = Binding.Action.InspectorMode;
|
||||
|
@ -1149,6 +1149,41 @@ pub const Set = struct {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the configuration entries for the binding
|
||||
/// that this value is part of.
|
||||
///
|
||||
/// The value may be part of multiple configuration entries
|
||||
/// if they're all part of the same prefix sequence (e.g. 'a>b', 'a>c').
|
||||
/// These will result in multiple separate entries in the configuration.
|
||||
///
|
||||
/// `buffer_stream` is a FixedBufferStream used for temporary storage
|
||||
/// that is shared between calls to nested levels of the set.
|
||||
/// For example, 'a>b>c=x' and 'a>b>d=y' will re-use the 'a>b' written
|
||||
/// to the buffer before flushing it to the formatter with 'c=x' and 'd=y'.
|
||||
pub fn formatEntries(self: Value, buffer_stream: anytype, formatter: anytype) !void {
|
||||
switch (self) {
|
||||
.leader => |set| {
|
||||
// We'll rewind to this position after each sub-entry,
|
||||
// sharing the prefix between siblings.
|
||||
const pos = try buffer_stream.getPos();
|
||||
|
||||
var iter = set.bindings.iterator();
|
||||
while (iter.next()) |binding| {
|
||||
buffer_stream.seekTo(pos) catch unreachable; // can't fail
|
||||
std.fmt.format(buffer_stream.writer(), ">{s}", .{binding.key_ptr.*}) catch return error.OutOfMemory;
|
||||
try binding.value_ptr.*.formatEntries(buffer_stream, formatter);
|
||||
}
|
||||
},
|
||||
|
||||
.leaf => |leaf| {
|
||||
// When we get to the leaf, the buffer_stream contains
|
||||
// the full sequence of keys needed to reach this action.
|
||||
std.fmt.format(buffer_stream.writer(), "={s}", .{leaf.action}) catch return error.OutOfMemory;
|
||||
try formatter.formatEntry([]const u8, buffer_stream.getWritten());
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Leaf node of a set is an action to trigger. This is a "leaf" compared
|
||||
|
@ -208,7 +208,7 @@ fn kitty(
|
||||
// Determine if the Alt modifier should be treated as an actual
|
||||
// modifier (in which case it prevents associated text) or as
|
||||
// the macOS Option key, which does not prevent associated text.
|
||||
const alt_prevents_text = if (comptime builtin.target.isDarwin())
|
||||
const alt_prevents_text = if (comptime builtin.os.tag == .macos)
|
||||
switch (self.macos_option_as_alt) {
|
||||
.left => all_mods.sides.alt == .left,
|
||||
.right => all_mods.sides.alt == .right,
|
||||
@ -422,7 +422,7 @@ fn legacyAltPrefix(
|
||||
// On macOS, we only handle option like alt in certain
|
||||
// circumstances. Otherwise, macOS does a unicode translation
|
||||
// and we allow that to happen.
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
if (comptime builtin.os.tag == .macos) {
|
||||
switch (self.macos_option_as_alt) {
|
||||
.false => return null,
|
||||
.left => if (mods.sides.alt == .right) return null,
|
||||
|
@ -14,6 +14,7 @@ const Keymap = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const macos = @import("macos");
|
||||
const codes = @import("keycodes.zig").entries;
|
||||
const Key = @import("key.zig").Key;
|
||||
@ -72,6 +73,24 @@ pub fn reload(self: *Keymap) !void {
|
||||
try self.reinit();
|
||||
}
|
||||
|
||||
/// Get the input source ID for the current keyboard layout. The input
|
||||
/// source ID is a unique identifier for the keyboard layout which is uniquely
|
||||
/// defined by Apple.
|
||||
///
|
||||
/// This is macOS-only. Other platforms don't have an equivalent of this
|
||||
/// so this isn't expected to be generally implemented.
|
||||
pub fn sourceId(self: *const Keymap, buf: []u8) Allocator.Error![]const u8 {
|
||||
// Get the raw CFStringRef
|
||||
const id_raw = TISGetInputSourceProperty(
|
||||
self.source,
|
||||
kTISPropertyInputSourceID,
|
||||
) orelse return error.OutOfMemory;
|
||||
|
||||
// Convert the CFStringRef to a C string into our buffer.
|
||||
const id: *CFString = @ptrCast(id_raw);
|
||||
return id.cstring(buf, .utf8) orelse error.OutOfMemory;
|
||||
}
|
||||
|
||||
/// Reinit reinitializes the keymap. It assumes that all the memory associated
|
||||
/// with the keymap is already freed.
|
||||
fn reinit(self: *Keymap) !void {
|
||||
@ -89,6 +108,12 @@ fn reinit(self: *Keymap) !void {
|
||||
// The CFDataRef contains a UCKeyboardLayout pointer
|
||||
break :layout @ptrCast(data.getPointer());
|
||||
};
|
||||
|
||||
if (comptime builtin.mode == .Debug) id: {
|
||||
var buf: [256]u8 = undefined;
|
||||
const id = self.sourceId(&buf) catch break :id;
|
||||
std.log.debug("keyboard layout={s}", .{id});
|
||||
}
|
||||
}
|
||||
|
||||
/// Translate a single key input into a utf8 sequence.
|
||||
@ -200,6 +225,7 @@ extern "c" fn LMGetKbdType() u8;
|
||||
extern "c" fn UCKeyTranslate(*const UCKeyboardLayout, u16, u16, u32, u32, u32, *u32, c_ulong, *c_ulong, [*]u16) i32;
|
||||
extern const kTISPropertyLocalizedName: *CFString;
|
||||
extern const kTISPropertyUnicodeKeyLayoutData: *CFString;
|
||||
extern const kTISPropertyInputSourceID: *CFString;
|
||||
const TISInputSource = opaque {};
|
||||
const UCKeyboardLayout = opaque {};
|
||||
const kUCKeyActionDown: u16 = 0;
|
||||
|
58
src/input/keyboard.zig
Normal file
58
src/input/keyboard.zig
Normal file
@ -0,0 +1,58 @@
|
||||
const std = @import("std");
|
||||
const OptionAsAlt = @import("../config.zig").OptionAsAlt;
|
||||
|
||||
/// Keyboard layouts.
|
||||
///
|
||||
/// These aren't heavily used in Ghostty and having a fully comprehensive
|
||||
/// list is not important. We only need to distinguish between a few
|
||||
/// different layouts for some nice-to-have features, such as setting a default
|
||||
/// value for "macos-option-as-alt".
|
||||
pub const Layout = enum {
|
||||
// Unknown, unmapped layout. Ghostty should not make any assumptions
|
||||
// about the layout of the keyboard.
|
||||
unknown,
|
||||
|
||||
// The remaining should be fairly self-explanatory:
|
||||
us_standard,
|
||||
us_international,
|
||||
|
||||
/// Map an Apple keyboard layout ID to a value in this enum. The layout
|
||||
/// ID can be retrieved using Carbon's TIKeyboardLayoutGetInputSourceProperty
|
||||
/// function.
|
||||
///
|
||||
/// Even though our layout supports "unknown", we return null if we don't
|
||||
/// recognize the layout ID so callers can detect this scenario.
|
||||
pub fn mapAppleId(id: []const u8) ?Layout {
|
||||
if (std.mem.eql(u8, id, "com.apple.keylayout.US")) {
|
||||
return .us_standard;
|
||||
} else if (std.mem.eql(u8, id, "com.apple.keylayout.USInternational")) {
|
||||
return .us_international;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns the default macos-option-as-alt value for this layout.
|
||||
///
|
||||
/// We apply some heuristics to change the default based on the keyboard
|
||||
/// layout if "macos-option-as-alt" is unset. We do this because on some
|
||||
/// keyboard layouts such as US standard layouts, users generally expect
|
||||
/// an input such as option-b to map to alt-b but macOS by default will
|
||||
/// convert it to the codepoint "∫".
|
||||
///
|
||||
/// This behavior however is desired on international layout where the
|
||||
/// option key is used for important, regularly used inputs.
|
||||
pub fn detectOptionAsAlt(self: Layout) OptionAsAlt {
|
||||
return switch (self) {
|
||||
// On US standard, the option key is typically used as alt
|
||||
// and not as a modifier for other codepoints. For example,
|
||||
// option-B = ∫ but usually the user wants alt-B.
|
||||
.us_standard,
|
||||
.us_international,
|
||||
=> .true,
|
||||
|
||||
.unknown,
|
||||
=> .false,
|
||||
};
|
||||
}
|
||||
};
|
@ -24,6 +24,13 @@ must be explicitly enabled (`shell-integration = bash`).
|
||||
Bash shell integration can also be sourced manually from `bash/ghostty.bash`.
|
||||
This also works for older versions of Bash.
|
||||
|
||||
```bash
|
||||
# Ghostty shell integration for Bash. This must be at the top of your bashrc!
|
||||
if [ -n "${GHOSTTY_RESOURCES_DIR}" ]; then
|
||||
builtin source "${GHOSTTY_RESOURCES_DIR}/shell-integration/bash/ghostty.bash"
|
||||
fi
|
||||
```
|
||||
|
||||
### Elvish
|
||||
|
||||
For [Elvish](https://elv.sh), `$GHOSTTY_RESOURCES_DIR/src/shell-integration`
|
||||
@ -59,3 +66,9 @@ For `zsh`, Ghostty sets `ZDOTDIR` so that it loads our configuration
|
||||
from the `zsh` directory. The existing `ZDOTDIR` is retained so that
|
||||
after loading the Ghostty shell integration the normal Zsh loading
|
||||
sequence occurs.
|
||||
|
||||
```bash
|
||||
if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then
|
||||
"$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration
|
||||
fi
|
||||
```
|
||||
|
@ -25,9 +25,7 @@
|
||||
# Ghostty in all shells should add the following lines to their .zshrc:
|
||||
#
|
||||
# if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then
|
||||
# autoload -Uz -- "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration
|
||||
# ghostty-integration
|
||||
# unfunction ghostty-integration
|
||||
# "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration
|
||||
# fi
|
||||
#
|
||||
# Implementation note: We can assume that alias expansion is disabled in this
|
||||
@ -35,49 +33,53 @@
|
||||
# builtins with `builtin` to avoid accidentally invoking user-defined functions.
|
||||
# We avoid `function` reserved word as an additional defensive measure.
|
||||
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
# Note that updating options with `builtin emulate -L zsh` affects the global options
|
||||
# if it's called outside of a function. So nearly all code has to be in functions.
|
||||
_entrypoint() {
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
|
||||
[[ -o interactive ]] || builtin return 0 # non-interactive shell
|
||||
(( ! $+_ghostty_state )) || builtin return 0 # already initialized
|
||||
[[ -o interactive ]] || builtin return 0 # non-interactive shell
|
||||
(( ! $+_ghostty_state )) || builtin return 0 # already initialized
|
||||
|
||||
# 0: no OSC 133 [AC] marks have been written yet.
|
||||
# 1: the last written OSC 133 C has not been closed with D yet.
|
||||
# 2: none of the above.
|
||||
builtin typeset -gi _ghostty_state
|
||||
# 0: no OSC 133 [AC] marks have been written yet.
|
||||
# 1: the last written OSC 133 C has not been closed with D yet.
|
||||
# 2: none of the above.
|
||||
builtin typeset -gi _ghostty_state
|
||||
|
||||
# Attempt to create a writable file descriptor to the TTY so that we can print
|
||||
# to the TTY later even when STDOUT is redirected. This code is fairly subtle.
|
||||
#
|
||||
# - It's tempting to do `[[ -t 1 ]] && exec {_ghostty_state}>&1` but we cannot do this
|
||||
# because it'll create a file descriptor >= 10 without O_CLOEXEC. This file
|
||||
# descriptor will leak to child processes.
|
||||
# - If we do `exec {3}>&1`, the file descriptor won't leak to the child processes
|
||||
# but it'll still leak if the current process is replaced with another. In
|
||||
# addition, it'll break user code that relies on fd 3 being available.
|
||||
# - Zsh doesn't expose dup3, which would have allowed us to copy STDOUT with
|
||||
# O_CLOEXEC. The only way to create a file descriptor with O_CLOEXEC is via
|
||||
# sysopen.
|
||||
# - `zmodload zsh/system` and `sysopen -o cloexec -wu _ghostty_fd -- /dev/tty` can
|
||||
# fail with an error message to STDERR (the latter can happen even if /dev/tty
|
||||
# is writable), hence the redirection of STDERR. We do it for the whole block
|
||||
# for performance reasons (redirections are slow).
|
||||
# - We must open the file descriptor right here rather than in _ghostty_deferred_init
|
||||
# because there are broken zsh plugins out there that run `exec {fd}< <(cmd)`
|
||||
# and then close the file descriptor more than once while suppressing errors.
|
||||
# This could end up closing our file descriptor if we opened it in
|
||||
# _ghostty_deferred_init.
|
||||
typeset -gi _ghostty_fd
|
||||
{
|
||||
builtin zmodload zsh/system && (( $+builtins[sysopen] )) && {
|
||||
{ [[ -w $TTY ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- $TTY } ||
|
||||
{ [[ -w /dev/tty ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- /dev/tty }
|
||||
}
|
||||
} 2>/dev/null || (( _ghostty_fd = 1 ))
|
||||
# Attempt to create a writable file descriptor to the TTY so that we can print
|
||||
# to the TTY later even when STDOUT is redirected. This code is fairly subtle.
|
||||
#
|
||||
# - It's tempting to do `[[ -t 1 ]] && exec {_ghostty_state}>&1` but we cannot do this
|
||||
# because it'll create a file descriptor >= 10 without O_CLOEXEC. This file
|
||||
# descriptor will leak to child processes.
|
||||
# - If we do `exec {3}>&1`, the file descriptor won't leak to the child processes
|
||||
# but it'll still leak if the current process is replaced with another. In
|
||||
# addition, it'll break user code that relies on fd 3 being available.
|
||||
# - Zsh doesn't expose dup3, which would have allowed us to copy STDOUT with
|
||||
# O_CLOEXEC. The only way to create a file descriptor with O_CLOEXEC is via
|
||||
# sysopen.
|
||||
# - `zmodload zsh/system` and `sysopen -o cloexec -wu _ghostty_fd -- /dev/tty` can
|
||||
# fail with an error message to STDERR (the latter can happen even if /dev/tty
|
||||
# is writable), hence the redirection of STDERR. We do it for the whole block
|
||||
# for performance reasons (redirections are slow).
|
||||
# - We must open the file descriptor right here rather than in _ghostty_deferred_init
|
||||
# because there are broken zsh plugins out there that run `exec {fd}< <(cmd)`
|
||||
# and then close the file descriptor more than once while suppressing errors.
|
||||
# This could end up closing our file descriptor if we opened it in
|
||||
# _ghostty_deferred_init.
|
||||
typeset -gi _ghostty_fd
|
||||
{
|
||||
builtin zmodload zsh/system && (( $+builtins[sysopen] )) && {
|
||||
{ [[ -w $TTY ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- $TTY } ||
|
||||
{ [[ -w /dev/tty ]] && builtin sysopen -o cloexec -wu _ghostty_fd -- /dev/tty }
|
||||
}
|
||||
} 2>/dev/null || (( _ghostty_fd = 1 ))
|
||||
|
||||
# Defer initialization so that other zsh init files can be configure
|
||||
# the integration.
|
||||
builtin typeset -ag precmd_functions
|
||||
precmd_functions+=(_ghostty_deferred_init)
|
||||
# Defer initialization so that other zsh init files can be configure
|
||||
# the integration.
|
||||
builtin typeset -ag precmd_functions
|
||||
precmd_functions+=(_ghostty_deferred_init)
|
||||
}
|
||||
|
||||
_ghostty_deferred_init() {
|
||||
builtin emulate -L zsh -o no_warn_create_global -o no_aliases
|
||||
@ -310,3 +312,5 @@ _ghostty_deferred_init() {
|
||||
# to unfunction themselves when invoked. Unfunctioning is done by calling code.
|
||||
builtin unfunction _ghostty_deferred_init
|
||||
}
|
||||
|
||||
_entrypoint
|
||||
|
@ -3803,10 +3803,18 @@ test "PageList pointFromPin active from prior page" {
|
||||
|
||||
var s = try init(alloc, 80, 24, null);
|
||||
defer s.deinit();
|
||||
// Grow so we take up at least 5 pages.
|
||||
const page = &s.pages.last.?.data;
|
||||
var cur_page = s.pages.last.?;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
for (0..page.capacity.rows * 5) |_| {
|
||||
_ = try s.grow();
|
||||
if (try s.grow()) |new_page| {
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
cur_page = new_page;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
}
|
||||
}
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
|
||||
{
|
||||
try testing.expectEqual(point.Point{
|
||||
@ -3837,10 +3845,19 @@ test "PageList pointFromPin traverse pages" {
|
||||
|
||||
var s = try init(alloc, 80, 24, null);
|
||||
defer s.deinit();
|
||||
|
||||
// Grow so we take up at least 2 pages.
|
||||
const page = &s.pages.last.?.data;
|
||||
var cur_page = s.pages.last.?;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
for (0..page.capacity.rows * 2) |_| {
|
||||
_ = try s.grow();
|
||||
if (try s.grow()) |new_page| {
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
cur_page = new_page;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
}
|
||||
}
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
|
||||
{
|
||||
const pages = s.totalPages();
|
||||
@ -4530,9 +4547,11 @@ test "PageList pageIterator two pages" {
|
||||
// Grow to capacity
|
||||
const page1_node = s.pages.last.?;
|
||||
const page1 = page1_node.data;
|
||||
page1_node.data.pauseIntegrityChecks(true);
|
||||
for (0..page1.capacity.rows - page1.size.rows) |_| {
|
||||
try testing.expect(try s.grow() == null);
|
||||
}
|
||||
page1_node.data.pauseIntegrityChecks(false);
|
||||
try testing.expect(try s.grow() != null);
|
||||
|
||||
// Iterate the active area
|
||||
@ -4564,9 +4583,11 @@ test "PageList pageIterator history two pages" {
|
||||
// Grow to capacity
|
||||
const page1_node = s.pages.last.?;
|
||||
const page1 = page1_node.data;
|
||||
page1_node.data.pauseIntegrityChecks(true);
|
||||
for (0..page1.capacity.rows - page1.size.rows) |_| {
|
||||
try testing.expect(try s.grow() == null);
|
||||
}
|
||||
page1_node.data.pauseIntegrityChecks(false);
|
||||
try testing.expect(try s.grow() != null);
|
||||
|
||||
// Iterate the active area
|
||||
@ -4615,9 +4636,11 @@ test "PageList pageIterator reverse two pages" {
|
||||
// Grow to capacity
|
||||
const page1_node = s.pages.last.?;
|
||||
const page1 = page1_node.data;
|
||||
page1_node.data.pauseIntegrityChecks(true);
|
||||
for (0..page1.capacity.rows - page1.size.rows) |_| {
|
||||
try testing.expect(try s.grow() == null);
|
||||
}
|
||||
page1_node.data.pauseIntegrityChecks(false);
|
||||
try testing.expect(try s.grow() != null);
|
||||
|
||||
// Iterate the active area
|
||||
@ -4653,9 +4676,11 @@ test "PageList pageIterator reverse history two pages" {
|
||||
// Grow to capacity
|
||||
const page1_node = s.pages.last.?;
|
||||
const page1 = page1_node.data;
|
||||
page1_node.data.pauseIntegrityChecks(true);
|
||||
for (0..page1.capacity.rows - page1.size.rows) |_| {
|
||||
try testing.expect(try s.grow() == null);
|
||||
}
|
||||
page1_node.data.pauseIntegrityChecks(false);
|
||||
try testing.expect(try s.grow() != null);
|
||||
|
||||
// Iterate the active area
|
||||
@ -4781,9 +4806,16 @@ test "PageList erase" {
|
||||
|
||||
// Grow so we take up at least 5 pages.
|
||||
const page = &s.pages.last.?.data;
|
||||
var cur_page = s.pages.last.?;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
for (0..page.capacity.rows * 5) |_| {
|
||||
_ = try s.grow();
|
||||
if (try s.grow()) |new_page| {
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
cur_page = new_page;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
}
|
||||
}
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
try testing.expectEqual(@as(usize, 6), s.totalPages());
|
||||
|
||||
// Our total rows should be large
|
||||
@ -4808,9 +4840,16 @@ test "PageList erase reaccounts page size" {
|
||||
|
||||
// Grow so we take up at least 5 pages.
|
||||
const page = &s.pages.last.?.data;
|
||||
var cur_page = s.pages.last.?;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
for (0..page.capacity.rows * 5) |_| {
|
||||
_ = try s.grow();
|
||||
if (try s.grow()) |new_page| {
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
cur_page = new_page;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
}
|
||||
}
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
try testing.expect(s.page_size > start_size);
|
||||
|
||||
// Erase the entire history, we should be back to just our active set.
|
||||
@ -4827,9 +4866,16 @@ test "PageList erase row with tracked pin resets to top-left" {
|
||||
|
||||
// Grow so we take up at least 5 pages.
|
||||
const page = &s.pages.last.?.data;
|
||||
var cur_page = s.pages.last.?;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
for (0..page.capacity.rows * 5) |_| {
|
||||
_ = try s.grow();
|
||||
if (try s.grow()) |new_page| {
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
cur_page = new_page;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
}
|
||||
}
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
|
||||
// Our total rows should be large
|
||||
try testing.expect(s.totalRows() > s.rows);
|
||||
@ -4899,9 +4945,16 @@ test "PageList erase resets viewport to active if moves within active" {
|
||||
|
||||
// Grow so we take up at least 5 pages.
|
||||
const page = &s.pages.last.?.data;
|
||||
var cur_page = s.pages.last.?;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
for (0..page.capacity.rows * 5) |_| {
|
||||
_ = try s.grow();
|
||||
if (try s.grow()) |new_page| {
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
cur_page = new_page;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
}
|
||||
}
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
|
||||
// Move our viewport to the top
|
||||
s.scroll(.{ .delta_row = -@as(isize, @intCast(s.totalRows())) });
|
||||
@ -4922,9 +4975,16 @@ test "PageList erase resets viewport if inside erased page but not active" {
|
||||
|
||||
// Grow so we take up at least 5 pages.
|
||||
const page = &s.pages.last.?.data;
|
||||
var cur_page = s.pages.last.?;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
for (0..page.capacity.rows * 5) |_| {
|
||||
_ = try s.grow();
|
||||
if (try s.grow()) |new_page| {
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
cur_page = new_page;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
}
|
||||
}
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
|
||||
// Move our viewport to the top
|
||||
s.scroll(.{ .delta_row = -@as(isize, @intCast(s.totalRows())) });
|
||||
@ -4946,9 +5006,16 @@ test "PageList erase resets viewport to active if top is inside active" {
|
||||
|
||||
// Grow so we take up at least 5 pages.
|
||||
const page = &s.pages.last.?.data;
|
||||
var cur_page = s.pages.last.?;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
for (0..page.capacity.rows * 5) |_| {
|
||||
_ = try s.grow();
|
||||
if (try s.grow()) |new_page| {
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
cur_page = new_page;
|
||||
cur_page.data.pauseIntegrityChecks(true);
|
||||
}
|
||||
}
|
||||
cur_page.data.pauseIntegrityChecks(false);
|
||||
|
||||
// Move our viewport to the top
|
||||
s.scroll(.{ .top = {} });
|
||||
@ -5106,7 +5173,9 @@ test "PageList eraseRowBounded full rows two pages" {
|
||||
// Grow to two pages so our active area straddles
|
||||
{
|
||||
const page = &s.pages.last.?.data;
|
||||
page.pauseIntegrityChecks(true);
|
||||
for (0..page.capacity.rows - page.size.rows) |_| _ = try s.grow();
|
||||
page.pauseIntegrityChecks(false);
|
||||
try s.growRows(5);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalPages());
|
||||
try testing.expectEqual(@as(usize, 5), s.pages.last.?.data.size.rows);
|
||||
@ -6435,9 +6504,11 @@ test "PageList resize reflow more cols wrap across page boundary" {
|
||||
// Grow to the capacity of the first page.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
page.pauseIntegrityChecks(true);
|
||||
for (page.size.rows..page.capacity.rows) |_| {
|
||||
_ = try s.grow();
|
||||
}
|
||||
page.pauseIntegrityChecks(false);
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
try s.growRows(1);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalPages());
|
||||
@ -6564,9 +6635,11 @@ test "PageList resize reflow more cols wrap across page boundary cursor in secon
|
||||
// Grow to the capacity of the first page.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
page.pauseIntegrityChecks(true);
|
||||
for (page.size.rows..page.capacity.rows) |_| {
|
||||
_ = try s.grow();
|
||||
}
|
||||
page.pauseIntegrityChecks(false);
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
try s.growRows(1);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalPages());
|
||||
@ -6648,9 +6721,11 @@ test "PageList resize reflow less cols wrap across page boundary cursor in secon
|
||||
// Grow to the capacity of the first page.
|
||||
{
|
||||
const page = &s.pages.first.?.data;
|
||||
page.pauseIntegrityChecks(true);
|
||||
for (page.size.rows..page.capacity.rows) |_| {
|
||||
_ = try s.grow();
|
||||
}
|
||||
page.pauseIntegrityChecks(false);
|
||||
try testing.expectEqual(@as(usize, 1), s.totalPages());
|
||||
try s.growRows(5);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalPages());
|
||||
|
@ -2428,6 +2428,7 @@ pub fn selectWord(self: *Screen, pin: Pin) ?Selection {
|
||||
'}',
|
||||
'<',
|
||||
'>',
|
||||
'$',
|
||||
};
|
||||
|
||||
// If our cell is empty we can't select a word, because we can't select
|
||||
@ -3048,9 +3049,11 @@ test "Screen cursorCopy style deref new page" {
|
||||
|
||||
// Fill the scrollback with blank lines until
|
||||
// there are only 5 rows left on the first page.
|
||||
s2.pages.pages.first.?.data.pauseIntegrityChecks(true);
|
||||
for (0..first_page_size - 5) |_| {
|
||||
try s2.testWriteString("\n");
|
||||
}
|
||||
s2.pages.pages.first.?.data.pauseIntegrityChecks(false);
|
||||
|
||||
try s2.testWriteString("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
|
||||
|
||||
@ -3157,9 +3160,11 @@ test "Screen cursorCopy hyperlink deref new page" {
|
||||
|
||||
// Fill the scrollback with blank lines until
|
||||
// there are only 5 rows left on the first page.
|
||||
s2.pages.pages.first.?.data.pauseIntegrityChecks(true);
|
||||
for (0..first_page_size - 5) |_| {
|
||||
try s2.testWriteString("\n");
|
||||
}
|
||||
s2.pages.pages.first.?.data.pauseIntegrityChecks(false);
|
||||
|
||||
try s2.testWriteString("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
|
||||
|
||||
@ -3588,7 +3593,9 @@ test "Screen: cursorDown across pages preserves style" {
|
||||
// Scroll down enough to go to another page
|
||||
const start_page = &s.pages.pages.last.?.data;
|
||||
const rem = start_page.capacity.rows;
|
||||
start_page.pauseIntegrityChecks(true);
|
||||
for (0..rem) |_| try s.cursorDownOrScroll();
|
||||
start_page.pauseIntegrityChecks(false);
|
||||
|
||||
// We need our page to change for this test o make sense. If this
|
||||
// assertion fails then the bug is in the test: we should be scrolling
|
||||
@ -3638,7 +3645,9 @@ test "Screen: cursorUp across pages preserves style" {
|
||||
// Scroll down enough to go to another page
|
||||
const start_page = &s.pages.pages.last.?.data;
|
||||
const rem = start_page.capacity.rows;
|
||||
start_page.pauseIntegrityChecks(true);
|
||||
for (0..rem) |_| try s.cursorDownOrScroll();
|
||||
start_page.pauseIntegrityChecks(false);
|
||||
|
||||
// We need our page to change for this test o make sense. If this
|
||||
// assertion fails then the bug is in the test: we should be scrolling
|
||||
@ -3683,7 +3692,9 @@ test "Screen: cursorAbsolute across pages preserves style" {
|
||||
// Scroll down enough to go to another page
|
||||
const start_page = &s.pages.pages.last.?.data;
|
||||
const rem = start_page.capacity.rows;
|
||||
start_page.pauseIntegrityChecks(true);
|
||||
for (0..rem) |_| try s.cursorDownOrScroll();
|
||||
start_page.pauseIntegrityChecks(false);
|
||||
|
||||
// We need our page to change for this test o make sense. If this
|
||||
// assertion fails then the bug is in the test: we should be scrolling
|
||||
@ -3822,7 +3833,9 @@ test "Screen: scrolling across pages preserves style" {
|
||||
|
||||
// Scroll down enough to go to another page
|
||||
const rem = start_page.capacity.rows - start_page.size.rows + 1;
|
||||
for (0..rem) |_| try s.cursorDownScroll();
|
||||
start_page.pauseIntegrityChecks(true);
|
||||
for (0..rem) |_| try s.cursorDownOrScroll();
|
||||
start_page.pauseIntegrityChecks(false);
|
||||
|
||||
// We need our page to change for this test o make sense. If this
|
||||
// assertion fails then the bug is in the test: we should be scrolling
|
||||
@ -4303,7 +4316,9 @@ test "Screen: scroll above same page but cursor on previous page" {
|
||||
|
||||
// We need to get the cursor to a new page
|
||||
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||
s.pages.pages.first.?.data.pauseIntegrityChecks(true);
|
||||
for (0..first_page_size - 3) |_| try s.testWriteString("\n");
|
||||
s.pages.pages.first.?.data.pauseIntegrityChecks(false);
|
||||
|
||||
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||
try s.testWriteString("1A\n2B\n3C\n4D\n5E");
|
||||
@ -4361,7 +4376,9 @@ test "Screen: scroll above same page but cursor on previous page last row" {
|
||||
|
||||
// We need to get the cursor to a new page
|
||||
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||
s.pages.pages.first.?.data.pauseIntegrityChecks(true);
|
||||
for (0..first_page_size - 2) |_| try s.testWriteString("\n");
|
||||
s.pages.pages.first.?.data.pauseIntegrityChecks(false);
|
||||
|
||||
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||
try s.testWriteString("1A\n2B\n3C\n4D\n5E");
|
||||
@ -4436,7 +4453,9 @@ test "Screen: scroll above creates new page" {
|
||||
|
||||
// We need to get the cursor to a new page
|
||||
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||
s.pages.pages.first.?.data.pauseIntegrityChecks(true);
|
||||
for (0..first_page_size - 3) |_| try s.testWriteString("\n");
|
||||
s.pages.pages.first.?.data.pauseIntegrityChecks(false);
|
||||
|
||||
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||
@ -4477,7 +4496,9 @@ test "Screen: scroll above no scrollback bottom of page" {
|
||||
defer s.deinit();
|
||||
|
||||
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||
s.pages.pages.first.?.data.pauseIntegrityChecks(true);
|
||||
for (0..first_page_size - 3) |_| try s.testWriteString("\n");
|
||||
s.pages.pages.first.?.data.pauseIntegrityChecks(false);
|
||||
|
||||
try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } });
|
||||
try s.testWriteString("1ABCD\n2EFGH\n3IJKL");
|
||||
@ -7298,6 +7319,7 @@ test "Screen: selectWord with character boundary" {
|
||||
" }abc} \n123",
|
||||
" <abc< \n123",
|
||||
" >abc> \n123",
|
||||
" $abc$ \n123",
|
||||
};
|
||||
|
||||
for (cases) |case| {
|
||||
@ -8254,9 +8276,11 @@ test "Screen: selectionString multi-page" {
|
||||
const first_page_size = s.pages.pages.first.?.data.capacity.rows;
|
||||
|
||||
// Lazy way to seek to the first page boundary.
|
||||
s.pages.pages.first.?.data.pauseIntegrityChecks(true);
|
||||
for (0..first_page_size - 1) |_| {
|
||||
try s.testWriteString("\n");
|
||||
}
|
||||
s.pages.pages.first.?.data.pauseIntegrityChecks(false);
|
||||
|
||||
try s.testWriteString("y\ny\ny");
|
||||
|
||||
|
@ -342,7 +342,7 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
if (c == 0xFE0F or c == 0xFE0E) {
|
||||
// This only applies to emoji
|
||||
const prev_props = unicode.getProperties(prev.cell.content.codepoint);
|
||||
const emoji = prev_props.grapheme_boundary_class == .extended_pictographic;
|
||||
const emoji = prev_props.grapheme_boundary_class.isExtendedPictographic();
|
||||
if (!emoji) return;
|
||||
|
||||
switch (c) {
|
||||
@ -2763,9 +2763,15 @@ test "Terminal: input glitch text" {
|
||||
var t = try init(alloc, .{ .cols = 30, .rows = 30 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
for (0..100) |_| {
|
||||
const page = t.screen.pages.pages.first.?;
|
||||
const grapheme_cap = page.data.capacity.grapheme_bytes;
|
||||
|
||||
while (page.data.capacity.grapheme_bytes == grapheme_cap) {
|
||||
try t.printString(glitch);
|
||||
}
|
||||
|
||||
// We're testing to make sure that grapheme capacity gets increased.
|
||||
try testing.expect(page.data.capacity.grapheme_bytes > grapheme_cap);
|
||||
}
|
||||
|
||||
test "Terminal: zero-width character at start" {
|
||||
@ -3193,6 +3199,51 @@ test "Terminal: print multicodepoint grapheme, mode 2027" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: Fitzpatrick skin tone next to non-base" {
|
||||
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
// Enable grapheme clustering
|
||||
t.modes.set(.grapheme_cluster, true);
|
||||
|
||||
// This is: "🏿" (which may not render correctly in your editor!)
|
||||
try t.print(0x22); // "
|
||||
try t.print(0x1F3FF); // Dark skin tone
|
||||
try t.print(0x22); // "
|
||||
|
||||
// We should have 4 cells taken up. Importantly, the skin tone
|
||||
// should not join with the quotes.
|
||||
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 4), t.screen.cursor.x);
|
||||
|
||||
// Row should be dirty
|
||||
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
|
||||
|
||||
// Assert various properties about our screen to verify
|
||||
// we have all expected cells.
|
||||
{
|
||||
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0x22), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
|
||||
}
|
||||
{
|
||||
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 1, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0x1F3FF), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.wide, cell.wide);
|
||||
}
|
||||
{
|
||||
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 3, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, 0x22), cell.content.codepoint);
|
||||
try testing.expect(!cell.hasGrapheme());
|
||||
try testing.expectEqual(Cell.Wide.narrow, cell.wide);
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: multicodepoint grapheme marks dirty on every codepoint" {
|
||||
var t = try init(testing.allocator, .{ .cols = 80, .rows = 80 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
@ -155,7 +155,7 @@ fn transmit(
|
||||
assert(!load.more);
|
||||
var d_copy = d;
|
||||
d_copy.image_id = load.image.id;
|
||||
return display(alloc, terminal, &.{
|
||||
result = display(alloc, terminal, &.{
|
||||
.control = .{ .display = d_copy },
|
||||
.quiet = cmd.quiet,
|
||||
});
|
||||
@ -164,9 +164,9 @@ fn transmit(
|
||||
// If there are more chunks expected we do not respond.
|
||||
if (load.more) return .{};
|
||||
|
||||
// If our image has no ID or number, we don't respond at all. Conversely,
|
||||
// if we have either an ID or number, we always respond.
|
||||
if (load.image.id == 0 and load.image.number == 0) return .{};
|
||||
// If the loaded image was assigned its ID automatically, not based
|
||||
// on a number or explicitly specified ID, then we don't respond.
|
||||
if (load.image.implicit_id) return .{};
|
||||
|
||||
// After the image is added, set the ID in case it changed.
|
||||
// The resulting image number and placement ID never change.
|
||||
@ -335,6 +335,10 @@ fn loadAndAddImage(
|
||||
if (loading.image.id == 0) {
|
||||
loading.image.id = storage.next_image_id;
|
||||
storage.next_image_id +%= 1;
|
||||
|
||||
// If the image also has no number then its auto-ID is "implicit".
|
||||
// See the doc comment on the Image.implicit_id field for more detail.
|
||||
if (loading.image.number == 0) loading.image.implicit_id = true;
|
||||
}
|
||||
|
||||
// If this is chunked, this is the beginning of a new chunked transmission.
|
||||
@ -529,3 +533,39 @@ test "kittygfx test valid i32 (expect invalid image ID)" {
|
||||
try testing.expect(!resp.ok());
|
||||
try testing.expectEqual(resp.message, "ENOENT: image not found");
|
||||
}
|
||||
|
||||
test "kittygfx no response with no image ID or number" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
{
|
||||
const cmd = try command.Parser.parseString(
|
||||
alloc,
|
||||
"a=t,f=24,t=d,s=1,v=2,c=10,r=1,i=0,I=0;////////",
|
||||
);
|
||||
defer cmd.deinit(alloc);
|
||||
const resp = execute(alloc, &t, &cmd);
|
||||
try testing.expect(resp == null);
|
||||
}
|
||||
}
|
||||
|
||||
test "kittygfx no response with no image ID or number load and display" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var t = try Terminal.init(alloc, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
{
|
||||
const cmd = try command.Parser.parseString(
|
||||
alloc,
|
||||
"a=T,f=24,t=d,s=1,v=2,c=10,r=1,i=0,I=0;////////",
|
||||
);
|
||||
defer cmd.deinit(alloc);
|
||||
const resp = execute(alloc, &t, &cmd);
|
||||
try testing.expect(resp == null);
|
||||
}
|
||||
}
|
||||
|
@ -455,6 +455,12 @@ pub const Image = struct {
|
||||
data: []const u8 = "",
|
||||
transmit_time: std.time.Instant = undefined,
|
||||
|
||||
/// Set this to true if this image was loaded by a command that
|
||||
/// doesn't specify an ID or number, since such commands should
|
||||
/// not be responded to, even though we do currently give them
|
||||
/// IDs in the public range (which is bad!).
|
||||
implicit_id: bool = false,
|
||||
|
||||
pub const Error = error{
|
||||
InternalError,
|
||||
InvalidData,
|
||||
|
@ -31,6 +31,9 @@ pub const ImageStorage = struct {
|
||||
|
||||
/// This is the next automatically assigned image ID. We start mid-way
|
||||
/// through the u32 range to avoid collisions with buggy programs.
|
||||
/// TODO: This isn't good enough, it's perfectly legal for programs
|
||||
/// to use IDs in the latter half of the range and collisions
|
||||
/// are not gracefully handled.
|
||||
next_image_id: u32 = 2147483647,
|
||||
|
||||
/// This is the next automatically assigned placement ID. This is never
|
||||
|
@ -1795,7 +1795,7 @@ pub const std_capacity: Capacity = .{
|
||||
.cols = 215,
|
||||
.rows = 215,
|
||||
.styles = 128,
|
||||
.grapheme_bytes = 8192,
|
||||
.grapheme_bytes = if (builtin.is_test) 512 else 8192,
|
||||
};
|
||||
|
||||
/// The size of this page.
|
||||
|
@ -51,7 +51,7 @@ const Precompute = struct {
|
||||
const data = precompute: {
|
||||
var result: [std.math.maxInt(u10)]Value = undefined;
|
||||
|
||||
@setEvalBranchQuota(2_000);
|
||||
@setEvalBranchQuota(3_000);
|
||||
const info = @typeInfo(GraphemeBoundaryClass).Enum;
|
||||
for (0..std.math.maxInt(u2) + 1) |state_init| {
|
||||
for (info.fields) |field1| {
|
||||
@ -80,7 +80,7 @@ fn graphemeBreakClass(
|
||||
state: *BreakState,
|
||||
) bool {
|
||||
// GB11: Emoji Extend* ZWJ x Emoji
|
||||
if (!state.extended_pictographic and gbc1 == .extended_pictographic) {
|
||||
if (!state.extended_pictographic and gbc1.isExtendedPictographic()) {
|
||||
state.extended_pictographic = true;
|
||||
}
|
||||
|
||||
@ -131,12 +131,21 @@ fn graphemeBreakClass(
|
||||
// GB11: Emoji Extend* ZWJ x Emoji
|
||||
if (state.extended_pictographic and
|
||||
gbc1 == .zwj and
|
||||
gbc2 == .extended_pictographic)
|
||||
gbc2.isExtendedPictographic())
|
||||
{
|
||||
state.extended_pictographic = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// UTS #51. This isn't covered by UAX #29 as far as I can tell (but
|
||||
// I'm probably wrong). This is a special case for emoji modifiers
|
||||
// which only do not break if they're next to a base.
|
||||
//
|
||||
// emoji_modifier_sequence := emoji_modifier_base emoji_modifier
|
||||
if (gbc2 == .emoji_modifier and gbc1 == .extended_pictographic_base) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -181,3 +190,19 @@ pub fn main() !void {
|
||||
pub const std_options = struct {
|
||||
pub const log_level: std.log.Level = .info;
|
||||
};
|
||||
|
||||
test "grapheme break: emoji modifier" {
|
||||
const testing = std.testing;
|
||||
|
||||
// Emoji and modifier
|
||||
{
|
||||
var state: BreakState = .{};
|
||||
try testing.expect(!graphemeBreak(0x261D, 0x1F3FF, &state));
|
||||
}
|
||||
|
||||
// Non-emoji and emoji modifier
|
||||
{
|
||||
var state: BreakState = .{};
|
||||
try testing.expect(graphemeBreak(0x22, 0x1F3FF, &state));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
const props = @This();
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const ziglyph = @import("ziglyph");
|
||||
const lut = @import("lut.zig");
|
||||
|
||||
@ -73,12 +74,21 @@ pub const GraphemeBoundaryClass = enum(u4) {
|
||||
spacing_mark,
|
||||
regional_indicator,
|
||||
extended_pictographic,
|
||||
extended_pictographic_base, // \p{Extended_Pictographic} & \p{Emoji_Modifier_Base}
|
||||
emoji_modifier, // \p{Emoji_Modifier}
|
||||
|
||||
/// Gets the grapheme boundary class for a codepoint. This is VERY
|
||||
/// SLOW. The use case for this is only in generating lookup tables.
|
||||
pub fn init(cp: u21) GraphemeBoundaryClass {
|
||||
// We special-case modifier bases because we should not break
|
||||
// if a modifier isn't next to a base.
|
||||
if (ziglyph.emoji.isEmojiModifierBase(cp)) {
|
||||
assert(ziglyph.emoji.isExtendedPictographic(cp));
|
||||
return .extended_pictographic_base;
|
||||
}
|
||||
|
||||
if (ziglyph.emoji.isEmojiModifier(cp)) return .emoji_modifier;
|
||||
if (ziglyph.emoji.isExtendedPictographic(cp)) return .extended_pictographic;
|
||||
if (ziglyph.emoji.isEmojiModifier(cp)) return .extend;
|
||||
if (ziglyph.grapheme_break.isL(cp)) return .L;
|
||||
if (ziglyph.grapheme_break.isV(cp)) return .V;
|
||||
if (ziglyph.grapheme_break.isT(cp)) return .T;
|
||||
@ -95,6 +105,19 @@ pub const GraphemeBoundaryClass = enum(u4) {
|
||||
// anything that doesn't fit into the above categories.
|
||||
return .invalid;
|
||||
}
|
||||
|
||||
/// Returns true if this is an extended pictographic type. This
|
||||
/// should be used instead of comparing the enum value directly
|
||||
/// because we classify multiple.
|
||||
pub fn isExtendedPictographic(self: GraphemeBoundaryClass) bool {
|
||||
return switch (self) {
|
||||
.extended_pictographic,
|
||||
.extended_pictographic_base,
|
||||
=> true,
|
||||
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn get(cp: u21) Properties {
|
||||
|
Reference in New Issue
Block a user