feat: enable scaling mouse-scroll-multiplier for both precision and discrete scrolling (#8927)
Resolves Issue: #8670 Now precision and discrete scrolling can be scaled independently. Supports following configuration, ```code # Apply everywhere mouse-scroll-multiplier = 3 # Apply separately mouse-scroll-multiplier = precision:0.1,discrete:3 (default) # Also it's order agnostic mouse-scroll-multiplier = discrete:3,precision:2 # Apply one, default other mouse-scroll-multiplier = precision:2 ``` The default precision value is set 0.1, as it felt natural to me at least on my track-pad. I've also set the min clamp value precision to 0.1 as 0.01 felt kind of useless to me but I'm unsure.pull/8955/head
commit
a2663692bb
|
|
@ -260,7 +260,7 @@ const DerivedConfig = struct {
|
|||
font: font.SharedGridSet.DerivedConfig,
|
||||
mouse_interval: u64,
|
||||
mouse_hide_while_typing: bool,
|
||||
mouse_scroll_multiplier: f64,
|
||||
mouse_scroll_multiplier: configpkg.MouseScrollMultiplier,
|
||||
mouse_shift_capture: configpkg.MouseShiftCapture,
|
||||
macos_non_native_fullscreen: configpkg.NonNativeFullscreen,
|
||||
macos_option_as_alt: ?configpkg.OptionAsAlt,
|
||||
|
|
@ -2829,7 +2829,7 @@ pub fn scrollCallback(
|
|||
// scroll events to pixels by multiplying the wheel tick value and the cell size. This means
|
||||
// that a wheel tick of 1 results in single scroll event.
|
||||
const yoff_adjusted: f64 = if (scroll_mods.precision)
|
||||
yoff
|
||||
yoff * self.config.mouse_scroll_multiplier.precision
|
||||
else yoff_adjusted: {
|
||||
// Round out the yoff to an absolute minimum of 1. macos tries to
|
||||
// simulate precision scrolling with non precision events by
|
||||
|
|
@ -2843,7 +2843,7 @@ pub fn scrollCallback(
|
|||
else
|
||||
@min(yoff, -1);
|
||||
|
||||
break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier;
|
||||
break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier.discrete;
|
||||
};
|
||||
|
||||
// Add our previously saved pending amount to the offset to get the
|
||||
|
|
|
|||
|
|
@ -507,13 +507,18 @@ pub fn parseTaggedUnion(comptime T: type, alloc: Allocator, v: []const u8) !T {
|
|||
|
||||
fn parseStruct(comptime T: type, alloc: Allocator, v: []const u8) !T {
|
||||
return switch (@typeInfo(T).@"struct".layout) {
|
||||
.auto => parseAutoStruct(T, alloc, v),
|
||||
.auto => parseAutoStruct(T, alloc, v, null),
|
||||
.@"packed" => parsePackedStruct(T, v),
|
||||
else => @compileError("unsupported struct layout"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T {
|
||||
pub fn parseAutoStruct(
|
||||
comptime T: type,
|
||||
alloc: Allocator,
|
||||
v: []const u8,
|
||||
default_: ?T,
|
||||
) !T {
|
||||
const info = @typeInfo(T).@"struct";
|
||||
comptime assert(info.layout == .auto);
|
||||
|
||||
|
|
@ -573,9 +578,18 @@ pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T {
|
|||
// Ensure all required fields are set
|
||||
inline for (info.fields, 0..) |field, i| {
|
||||
if (!fields_set.isSet(i)) {
|
||||
const default_ptr = field.default_value_ptr orelse return error.InvalidValue;
|
||||
const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr));
|
||||
@field(result, field.name) = typed_ptr.*;
|
||||
@field(result, field.name) = default: {
|
||||
// If we're given a default value then we inherit those.
|
||||
// Otherwise we use the default values as specified by the
|
||||
// struct.
|
||||
if (default_) |default| {
|
||||
break :default @field(default, field.name);
|
||||
} else {
|
||||
const default_ptr = field.default_value_ptr orelse return error.InvalidValue;
|
||||
const typed_ptr: *const field.type = @alignCast(@ptrCast(default_ptr));
|
||||
break :default typed_ptr.*;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1194,7 +1208,18 @@ test "parseIntoField: struct with basic fields" {
|
|||
try testing.expectEqual(84, data.value.b);
|
||||
try testing.expectEqual(24, data.value.c);
|
||||
|
||||
// Missing require dfield
|
||||
// Set with explicit default
|
||||
data.value = try parseAutoStruct(
|
||||
@TypeOf(data.value),
|
||||
alloc,
|
||||
"a:hello",
|
||||
.{ .a = "oh no", .b = 42 },
|
||||
);
|
||||
try testing.expectEqualStrings("hello", data.value.a);
|
||||
try testing.expectEqual(42, data.value.b);
|
||||
try testing.expectEqual(12, data.value.c);
|
||||
|
||||
// Missing required field
|
||||
try testing.expectError(
|
||||
error.InvalidValue,
|
||||
parseIntoField(@TypeOf(data), alloc, &data, "value", "a:hello"),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ pub const FontStyle = Config.FontStyle;
|
|||
pub const FreetypeLoadFlags = Config.FreetypeLoadFlags;
|
||||
pub const Keybinds = Config.Keybinds;
|
||||
pub const MouseShiftCapture = Config.MouseShiftCapture;
|
||||
pub const MouseScrollMultiplier = Config.MouseScrollMultiplier;
|
||||
pub const NonNativeFullscreen = Config.NonNativeFullscreen;
|
||||
pub const OptionAsAlt = Config.OptionAsAlt;
|
||||
pub const RepeatableCodepointMap = Config.RepeatableCodepointMap;
|
||||
|
|
|
|||
|
|
@ -833,14 +833,20 @@ palette: Palette = .{},
|
|||
/// * `never`
|
||||
@"mouse-shift-capture": MouseShiftCapture = .false,
|
||||
|
||||
/// Multiplier for scrolling distance with the mouse wheel. Any value less
|
||||
/// than 0.01 or greater than 10,000 will be clamped to the nearest valid
|
||||
/// value.
|
||||
/// Multiplier for scrolling distance with the mouse wheel.
|
||||
///
|
||||
/// A value of "3" (default) scrolls 3 lines per tick.
|
||||
/// A prefix of `precision:` or `discrete:` can be used to set the multiplier
|
||||
/// only for scrolling with the specific type of devices. These can be
|
||||
/// comma-separated to set both types of multipliers at the same time, e.g.
|
||||
/// `precision:0.1,discrete:3`. If no prefix is used, the multiplier applies
|
||||
/// to all scrolling devices. Specifying a prefix was introduced in Ghostty
|
||||
/// 1.2.1.
|
||||
///
|
||||
/// Available since: 1.2.0
|
||||
@"mouse-scroll-multiplier": f64 = 3.0,
|
||||
/// The value will be clamped to [0.01, 10,000]. Both of these are extreme
|
||||
/// and you're likely to have a bad experience if you set either extreme.
|
||||
///
|
||||
/// The default value is "3" for discrete devices and "1" for precision devices.
|
||||
@"mouse-scroll-multiplier": MouseScrollMultiplier = .default,
|
||||
|
||||
/// The opacity level (opposite of transparency) of the background. A value of
|
||||
/// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0
|
||||
|
|
@ -4077,7 +4083,8 @@ pub fn finalize(self: *Config) !void {
|
|||
}
|
||||
|
||||
// Clamp our mouse scroll multiplier
|
||||
self.@"mouse-scroll-multiplier" = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier"));
|
||||
self.@"mouse-scroll-multiplier".precision = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".precision));
|
||||
self.@"mouse-scroll-multiplier".discrete = @min(10_000.0, @max(0.01, self.@"mouse-scroll-multiplier".discrete));
|
||||
|
||||
// Clamp our split opacity
|
||||
self.@"unfocused-split-opacity" = @min(1.0, @max(0.15, self.@"unfocused-split-opacity"));
|
||||
|
|
@ -6508,7 +6515,7 @@ pub const RepeatableCodepointMap = struct {
|
|||
return .{ .map = try self.map.clone(alloc) };
|
||||
}
|
||||
|
||||
/// Compare if two of our value are requal. Required by Config.
|
||||
/// Compare if two of our value are equal. Required by Config.
|
||||
pub fn equal(self: Self, other: Self) bool {
|
||||
const itemsA = self.map.list.slice();
|
||||
const itemsB = other.map.list.slice();
|
||||
|
|
@ -7010,6 +7017,7 @@ pub const RepeatableCommand = struct {
|
|||
inputpkg.Command,
|
||||
alloc,
|
||||
input,
|
||||
null,
|
||||
);
|
||||
try self.value.append(alloc, cmd);
|
||||
}
|
||||
|
|
@ -7319,6 +7327,108 @@ pub const MouseShiftCapture = enum {
|
|||
never,
|
||||
};
|
||||
|
||||
/// See mouse-scroll-multiplier
|
||||
pub const MouseScrollMultiplier = struct {
|
||||
const Self = @This();
|
||||
|
||||
precision: f64 = 1,
|
||||
discrete: f64 = 3,
|
||||
|
||||
pub const default: MouseScrollMultiplier = .{};
|
||||
|
||||
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void {
|
||||
const input = input_ orelse return error.ValueRequired;
|
||||
self.* = cli.args.parseAutoStruct(
|
||||
MouseScrollMultiplier,
|
||||
alloc,
|
||||
input,
|
||||
self.*,
|
||||
) catch |err| switch (err) {
|
||||
error.InvalidValue => bare: {
|
||||
const v = std.fmt.parseFloat(
|
||||
f64,
|
||||
input,
|
||||
) catch return error.InvalidValue;
|
||||
break :bare .{
|
||||
.precision = v,
|
||||
.discrete = v,
|
||||
};
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self {
|
||||
_ = alloc;
|
||||
return self.*;
|
||||
}
|
||||
|
||||
/// Compare if two of our value are equal. Required by Config.
|
||||
pub fn equal(self: Self, other: Self) bool {
|
||||
return self.precision == other.precision and self.discrete == other.discrete;
|
||||
}
|
||||
|
||||
/// Used by Formatter
|
||||
pub fn formatEntry(self: Self, formatter: anytype) !void {
|
||||
var buf: [32]u8 = undefined;
|
||||
const formatted = std.fmt.bufPrint(
|
||||
&buf,
|
||||
"precision:{d},discrete:{d}",
|
||||
.{ self.precision, self.discrete },
|
||||
) catch return error.OutOfMemory;
|
||||
try formatter.formatEntry([]const u8, formatted);
|
||||
}
|
||||
|
||||
test "parse" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const epsilon = 0.00001;
|
||||
|
||||
var args: Self = .{ .precision = 0.1, .discrete = 3 };
|
||||
try args.parseCLI(alloc, "3");
|
||||
try testing.expectApproxEqAbs(3, args.precision, epsilon);
|
||||
try testing.expectApproxEqAbs(3, args.discrete, epsilon);
|
||||
|
||||
args = .{ .precision = 0.1, .discrete = 3 };
|
||||
try args.parseCLI(alloc, "precision:1");
|
||||
try testing.expectApproxEqAbs(1, args.precision, epsilon);
|
||||
try testing.expectApproxEqAbs(3, args.discrete, epsilon);
|
||||
|
||||
args = .{ .precision = 0.1, .discrete = 3 };
|
||||
try args.parseCLI(alloc, "discrete:5");
|
||||
try testing.expectApproxEqAbs(0.1, args.precision, epsilon);
|
||||
try testing.expectApproxEqAbs(5, args.discrete, epsilon);
|
||||
|
||||
args = .{ .precision = 0.1, .discrete = 3 };
|
||||
try args.parseCLI(alloc, "precision:3,discrete:7");
|
||||
try testing.expectApproxEqAbs(3, args.precision, epsilon);
|
||||
try testing.expectApproxEqAbs(7, args.discrete, epsilon);
|
||||
|
||||
args = .{ .precision = 0.1, .discrete = 3 };
|
||||
try args.parseCLI(alloc, "discrete:8,precision:6");
|
||||
try testing.expectApproxEqAbs(6, args.precision, epsilon);
|
||||
try testing.expectApproxEqAbs(8, args.discrete, epsilon);
|
||||
|
||||
args = .default;
|
||||
try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "foo:1"));
|
||||
try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:bar"));
|
||||
try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,discrete:3,foo:5"));
|
||||
try testing.expectError(error.InvalidValue, args.parseCLI(alloc, "precision:1,,discrete:3"));
|
||||
try testing.expectError(error.InvalidValue, args.parseCLI(alloc, ",precision:1,discrete:3"));
|
||||
}
|
||||
|
||||
test "format entry MouseScrollMultiplier" {
|
||||
const testing = std.testing;
|
||||
var buf = std.ArrayList(u8).init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
var args: Self = .{ .precision = 1.5, .discrete = 2.5 };
|
||||
try args.formatEntry(formatterpkg.entryFormatter("mouse-scroll-multiplier", buf.writer()));
|
||||
try testing.expectEqualSlices(u8, "mouse-scroll-multiplier = precision:1.5,discrete:2.5\n", buf.items);
|
||||
}
|
||||
};
|
||||
|
||||
/// How to treat requests to write to or read from the clipboard
|
||||
pub const ClipboardAccess = enum {
|
||||
allow,
|
||||
|
|
@ -7933,6 +8043,7 @@ pub const Theme = struct {
|
|||
Theme,
|
||||
alloc,
|
||||
input,
|
||||
null,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue