Merge 749a13888a into 4df593bd24
commit
0c19044329
|
|
@ -1126,7 +1126,7 @@ GHOSTTY_API bool ghostty_surface_key_is_binding(ghostty_surface_t,
|
|||
ghostty_input_key_s,
|
||||
ghostty_binding_flags_e*);
|
||||
GHOSTTY_API void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
|
||||
GHOSTTY_API void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t);
|
||||
GHOSTTY_API void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t, int32_t);
|
||||
GHOSTTY_API bool ghostty_surface_mouse_captured(ghostty_surface_t);
|
||||
GHOSTTY_API bool ghostty_surface_mouse_button(ghostty_surface_t,
|
||||
ghostty_input_mouse_state_e,
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ extension Ghostty {
|
|||
var notificationIdentifiers: Set<String> = []
|
||||
|
||||
private var markedText: NSMutableAttributedString
|
||||
private var markedTextSelectedLocation: Int = 0
|
||||
private(set) var focused: Bool = true
|
||||
private var prevPressureStage: Int = 0
|
||||
private var appearanceObserver: NSKeyValueObservation?
|
||||
|
|
@ -1883,9 +1884,11 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
|||
switch string {
|
||||
case let v as NSAttributedString:
|
||||
self.markedText = NSMutableAttributedString(attributedString: v)
|
||||
self.markedTextSelectedLocation = selectedRange.location
|
||||
|
||||
case let v as String:
|
||||
self.markedText = NSMutableAttributedString(string: v)
|
||||
self.markedTextSelectedLocation = selectedRange.location
|
||||
|
||||
default:
|
||||
print("unknown marked text: \(string)")
|
||||
|
|
@ -2060,13 +2063,18 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
|||
if len > 0 {
|
||||
markedText.string.withCString { ptr in
|
||||
// Subtract 1 for the null terminator
|
||||
ghostty_surface_preedit(surface, ptr, UInt(len - 1))
|
||||
ghostty_surface_preedit(
|
||||
surface,
|
||||
ptr,
|
||||
UInt(len - 1),
|
||||
Int32(markedTextSelectedLocation)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if clearIfNeeded {
|
||||
// If we had marked text before but don't now, we're no longer
|
||||
// in a preedit state so we can clear it.
|
||||
ghostty_surface_preedit(surface, nil, 0)
|
||||
ghostty_surface_preedit(surface, nil, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2496,7 +2496,7 @@ fn balancePaddingIfNeeded(self: *Surface) void {
|
|||
/// the preedit state correctly.
|
||||
///
|
||||
/// The preedit input must be UTF-8 encoded.
|
||||
pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
|
||||
pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8, cursor_pos: i32) !void {
|
||||
// log.debug("text preeditCallback value={any}", .{preedit_});
|
||||
|
||||
// Crash metadata in case we crash in here
|
||||
|
|
@ -2563,6 +2563,7 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
|
|||
|
||||
self.renderer_state.preedit = .{
|
||||
.codepoints = try codepoints.toOwnedSlice(self.alloc),
|
||||
.cursor_pos = cursor_pos
|
||||
};
|
||||
try self.queueRender();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -891,8 +891,8 @@ pub const Surface = struct {
|
|||
};
|
||||
}
|
||||
|
||||
pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) void {
|
||||
_ = self.core_surface.preeditCallback(preedit_) catch |err| {
|
||||
pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8, cursor_pos: i32) void {
|
||||
_ = self.core_surface.preeditCallback(preedit_, cursor_pos) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
|
@ -1829,8 +1829,9 @@ pub const CAPI = struct {
|
|||
surface: *Surface,
|
||||
ptr: [*]const u8,
|
||||
len: usize,
|
||||
cursor_pos: i32
|
||||
) void {
|
||||
surface.preeditCallback(if (len == 0) null else ptr[0..len]);
|
||||
surface.preeditCallback(if (len == 0) null else ptr[0..len], cursor_pos);
|
||||
}
|
||||
|
||||
/// Returns true if the surface currently has mouse capturing
|
||||
|
|
|
|||
|
|
@ -659,6 +659,7 @@ pub const Surface = extern struct {
|
|||
im_composing: bool = false,
|
||||
im_buf: [128]u8 = undefined,
|
||||
im_len: u7 = 0,
|
||||
im_show_cursor: bool = false,
|
||||
|
||||
/// True when we have a precision scroll in progress
|
||||
precision_scroll: bool = false,
|
||||
|
|
@ -1448,7 +1449,7 @@ pub const Surface = extern struct {
|
|||
// such as quotation mark ordering for Chinese input.
|
||||
if (priv.im_composing) {
|
||||
priv.im_context.as(gtk.IMContext).reset();
|
||||
surface.preeditCallback(null) catch {};
|
||||
surface.preeditCallback(null, 0) catch {};
|
||||
}
|
||||
|
||||
// Bell stops ringing when any key is pressed that is used by
|
||||
|
|
@ -3084,6 +3085,7 @@ pub const Surface = extern struct {
|
|||
const priv = self.private();
|
||||
priv.im_composing = true;
|
||||
priv.im_len = 0;
|
||||
priv.im_show_cursor = false;
|
||||
}
|
||||
|
||||
fn imPreeditChanged(
|
||||
|
|
@ -3107,17 +3109,28 @@ pub const Surface = extern struct {
|
|||
|
||||
// Get our pre-edit string that we'll use to show the user.
|
||||
var buf: [*:0]u8 = undefined;
|
||||
var cursor_pos: i32 = 0;
|
||||
ctx.as(gtk.IMContext).getPreeditString(
|
||||
&buf,
|
||||
null,
|
||||
null,
|
||||
&cursor_pos,
|
||||
);
|
||||
defer glib.free(buf);
|
||||
const str = std.mem.sliceTo(buf, 0);
|
||||
|
||||
// some IME may hard code the cursor as a bar,
|
||||
// in which case, cursor_pos would always be 0
|
||||
// if cursor_pos has never been non-zero,
|
||||
// set it to -1 to hide the cursor to avoid conflict
|
||||
if (cursor_pos > 0) {
|
||||
priv.im_show_cursor = true;
|
||||
} else if (!priv.im_show_cursor) {
|
||||
cursor_pos = -1;
|
||||
}
|
||||
|
||||
// Update our preedit state in Ghostty core
|
||||
// log.warn("GTKIM: preedit change str={s}", .{str});
|
||||
surface.preeditCallback(str) catch |err| {
|
||||
surface.preeditCallback(str, cursor_pos) catch |err| {
|
||||
log.warn(
|
||||
"error in preedit callback err={}",
|
||||
.{err},
|
||||
|
|
@ -3137,7 +3150,7 @@ pub const Surface = extern struct {
|
|||
|
||||
// End our preedit state in Ghostty core
|
||||
const surface = priv.core_surface orelse return;
|
||||
surface.preeditCallback(null) catch |err| {
|
||||
surface.preeditCallback(null, 0) catch |err| {
|
||||
log.warn("error in preedit callback err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
|
@ -3206,7 +3219,7 @@ pub const Surface = extern struct {
|
|||
if (priv.core_surface) |surface| {
|
||||
// End our preedit state. Well-behaved input methods do this for us
|
||||
// by triggering a preedit-end event but some do not (ibus 1.5.29).
|
||||
surface.preeditCallback(null) catch |err| {
|
||||
surface.preeditCallback(null, 0) catch |err| {
|
||||
log.warn("error in preedit callback err={}", .{err});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -915,6 +915,23 @@ palette: Palette = .{},
|
|||
/// behavior around edge cases is possible.
|
||||
@"cursor-click-to-move": bool = true,
|
||||
|
||||
/// The style of the cursor when editing the preedit text using
|
||||
/// an IME (Input Method Editor).
|
||||
///
|
||||
/// All other cursor configs are applicable to IME cursor as well,
|
||||
/// with the exception of `cursor-click-to-move` and `cursor-style-blink`
|
||||
///
|
||||
/// Note: Some IME hardcodes the cursor as a bar as a part of the preedit text.
|
||||
/// To avoid showing two cursors, the native Ghostty cursor would be hidden.
|
||||
///
|
||||
/// Valid values are:
|
||||
///
|
||||
/// * `block`
|
||||
/// * `bar`
|
||||
/// * `underline`
|
||||
/// * `block_hollow`
|
||||
@"ime-cursor-style": terminal.CursorStyle = .bar,
|
||||
|
||||
/// Hide the mouse immediately when typing. The mouse becomes visible again
|
||||
/// when the mouse is used (button, movement, etc.). Platform-specific behavior
|
||||
/// may dictate other scenarios where the mouse is shown. For example on macOS,
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ pub const Mouse = struct {
|
|||
pub const Preedit = struct {
|
||||
/// The codepoints to render as preedit text.
|
||||
codepoints: []const Codepoint = &.{},
|
||||
cursor_pos: i32,
|
||||
|
||||
/// A single codepoint to render as preedit text.
|
||||
pub const Codepoint = struct {
|
||||
|
|
@ -62,6 +63,7 @@ pub const Preedit = struct {
|
|||
pub fn clone(self: *const Preedit, alloc: Allocator) !Preedit {
|
||||
return .{
|
||||
.codepoints = try alloc.dupe(Codepoint, self.codepoints),
|
||||
.cursor_pos = self.cursor_pos
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -544,6 +544,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
cursor_color: ?configpkg.Config.TerminalColor,
|
||||
cursor_opacity: f64,
|
||||
cursor_text: ?configpkg.Config.TerminalColor,
|
||||
ime_cursor_style: terminal.CursorStyle,
|
||||
background: terminal.color.RGB,
|
||||
background_opacity: f64,
|
||||
background_opacity_cells: bool,
|
||||
|
|
@ -616,6 +617,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
.cursor_color = config.@"cursor-color",
|
||||
.cursor_text = config.@"cursor-text",
|
||||
.cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")),
|
||||
.ime_cursor_style = config.@"ime-cursor-style",
|
||||
|
||||
.background = config.background.toTerminalRGB(),
|
||||
.foreground = config.foreground.toTerminalRGB(),
|
||||
|
|
@ -2444,6 +2446,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
|
||||
// Setup our cursor rendering information.
|
||||
cursor: {
|
||||
// Preedit cursor has custom logic
|
||||
if (preedit != null) break :cursor;
|
||||
|
||||
// Clear our cursor by default.
|
||||
self.cells.setCursor(null, null);
|
||||
self.uniforms.cursor_pos = .{
|
||||
|
|
@ -2451,136 +2456,62 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
std.math.maxInt(u16),
|
||||
};
|
||||
|
||||
// If the cursor isn't visible on the viewport, don't show
|
||||
// a cursor. Otherwise, get our cursor cell, because we may
|
||||
// need it for styling.
|
||||
// If the cursor isn't visible on the viewport, don't show a cursor.
|
||||
const cursor_vp = state.cursor.viewport orelse break :cursor;
|
||||
const cursor_style: terminal.Style = cursor_style: {
|
||||
const cells = state.row_data.items(.cells);
|
||||
const cell = cells[cursor_vp.y].get(cursor_vp.x);
|
||||
break :cursor_style if (cell.raw.hasStyling())
|
||||
cell.style
|
||||
else
|
||||
.{};
|
||||
};
|
||||
|
||||
// If we have preedit text, we don't setup a cursor
|
||||
if (preedit != null) break :cursor;
|
||||
|
||||
// If there isn't a cursor visual style requested then
|
||||
// we don't render a cursor.
|
||||
const style = cursor_style_ orelse break :cursor;
|
||||
|
||||
// Determine the cursor color.
|
||||
const cursor_color = cursor_color: {
|
||||
// If an explicit cursor color was set by OSC 12, use that.
|
||||
if (state.colors.cursor) |v| break :cursor_color v;
|
||||
const cell_style: terminal.Style = cell_style: {
|
||||
const cells = state.row_data.items(.cells);
|
||||
const cell = cells[cursor_vp.y].get(cursor_vp.x);
|
||||
break :cell_style if (cell.raw.hasStyling())
|
||||
cell.style
|
||||
else
|
||||
.{};
|
||||
};
|
||||
const cell_fg = cell_style.fg(.{
|
||||
.default = state.colors.foreground,
|
||||
.palette = &state.colors.palette,
|
||||
.bold = self.config.bold_color,
|
||||
});
|
||||
const cell_bg = cell_style.bg(
|
||||
&state.cursor.cell,
|
||||
&state.colors.palette,
|
||||
) orelse state.colors.background;
|
||||
|
||||
// Use our configured color if specified
|
||||
if (self.config.cursor_color) |v| switch (v) {
|
||||
.color => |color| break :cursor_color color.toTerminalRGB(),
|
||||
|
||||
inline .@"cell-foreground",
|
||||
.@"cell-background",
|
||||
=> |_, tag| {
|
||||
const fg_style = cursor_style.fg(.{
|
||||
.default = state.colors.foreground,
|
||||
.palette = &state.colors.palette,
|
||||
.bold = self.config.bold_color,
|
||||
});
|
||||
const bg_style = cursor_style.bg(
|
||||
&state.cursor.cell,
|
||||
&state.colors.palette,
|
||||
) orelse state.colors.background;
|
||||
|
||||
break :cursor_color switch (tag) {
|
||||
.color => unreachable,
|
||||
.@"cell-foreground" => if (cursor_style.flags.inverse)
|
||||
bg_style
|
||||
else
|
||||
fg_style,
|
||||
.@"cell-background" => if (cursor_style.flags.inverse)
|
||||
fg_style
|
||||
else
|
||||
bg_style,
|
||||
};
|
||||
},
|
||||
// Add the cursor. We render the cursor over the wide character if
|
||||
// we're on the wide character tail.
|
||||
const wide, const x = cell: {
|
||||
// The cursor goes over the screen cursor position.
|
||||
if (!cursor_vp.wide_tail) break :cell .{
|
||||
state.cursor.cell.wide == .wide,
|
||||
cursor_vp.x,
|
||||
};
|
||||
|
||||
break :cursor_color state.colors.foreground;
|
||||
// If we're part of a wide character, we move the cursor back
|
||||
// to the actual character.
|
||||
break :cell .{ true, cursor_vp.x - 1 };
|
||||
};
|
||||
|
||||
self.addCursor(
|
||||
&state.cursor,
|
||||
style,
|
||||
cursor_color,
|
||||
if (cell_style.flags.inverse) cell_bg else cell_fg,
|
||||
if (cell_style.flags.inverse) cell_fg else cell_bg,
|
||||
wide,
|
||||
x,
|
||||
cursor_vp.y,
|
||||
);
|
||||
|
||||
// If the cursor is visible then we set our uniforms.
|
||||
if (style == .block) {
|
||||
const wide = state.cursor.cell.wide;
|
||||
|
||||
self.uniforms.cursor_pos = .{
|
||||
// If we are a spacer tail of a wide cell, our cursor needs
|
||||
// to move back one cell. The saturate is to ensure we don't
|
||||
// overflow but this shouldn't happen with well-formed input.
|
||||
switch (wide) {
|
||||
.narrow, .spacer_head, .wide => cursor_vp.x,
|
||||
.spacer_tail => cursor_vp.x -| 1,
|
||||
},
|
||||
@intCast(cursor_vp.y),
|
||||
};
|
||||
|
||||
self.uniforms.bools.cursor_wide = switch (wide) {
|
||||
.narrow, .spacer_head => false,
|
||||
.wide, .spacer_tail => true,
|
||||
};
|
||||
|
||||
const uniform_color = if (self.config.cursor_text) |txt| blk: {
|
||||
// If cursor-text is set, then compute the correct color.
|
||||
// Otherwise, use the background color.
|
||||
if (txt == .color) {
|
||||
// Use the color set by cursor-text, if any.
|
||||
break :blk txt.color.toTerminalRGB();
|
||||
}
|
||||
|
||||
const fg_style = cursor_style.fg(.{
|
||||
.default = state.colors.foreground,
|
||||
.palette = &state.colors.palette,
|
||||
.bold = self.config.bold_color,
|
||||
});
|
||||
const bg_style = cursor_style.bg(
|
||||
&state.cursor.cell,
|
||||
&state.colors.palette,
|
||||
) orelse state.colors.background;
|
||||
|
||||
break :blk switch (txt) {
|
||||
// If the cell is reversed, use the opposite cell color instead.
|
||||
.@"cell-foreground" => if (cursor_style.flags.inverse)
|
||||
bg_style
|
||||
else
|
||||
fg_style,
|
||||
.@"cell-background" => if (cursor_style.flags.inverse)
|
||||
fg_style
|
||||
else
|
||||
bg_style,
|
||||
else => unreachable,
|
||||
};
|
||||
} else state.colors.background;
|
||||
|
||||
self.uniforms.cursor_color = .{
|
||||
uniform_color.r,
|
||||
uniform_color.g,
|
||||
uniform_color.b,
|
||||
255,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Setup our preedit text.
|
||||
if (preedit) |preedit_v| preedit: {
|
||||
const range = preedit_range orelse break :preedit;
|
||||
var x = range.x[0];
|
||||
var cp_count: i32 = 0;
|
||||
var cursor_x = x;
|
||||
var cursor_wide = false;
|
||||
for (preedit_v.codepoints[range.cp_offset..]) |cp| {
|
||||
self.addPreeditCell(
|
||||
cp,
|
||||
|
|
@ -2594,7 +2525,30 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
});
|
||||
};
|
||||
|
||||
if (cp_count == preedit_v.cursor_pos) {
|
||||
cursor_x = x;
|
||||
cursor_wide = cp.wide;
|
||||
}
|
||||
x += if (cp.wide) 2 else 1;
|
||||
cp_count += 1;
|
||||
}
|
||||
if (cp_count == preedit_v.cursor_pos) cursor_x = x;
|
||||
|
||||
// Clear our cursor by default.
|
||||
self.cells.setCursor(null, null);
|
||||
self.uniforms.cursor_pos = .{
|
||||
std.math.maxInt(u16),
|
||||
std.math.maxInt(u16),
|
||||
};
|
||||
if (preedit_v.cursor_pos >= 0) {
|
||||
self.addCursor(
|
||||
.fromTerminal(self.config.ime_cursor_style),
|
||||
state.colors.foreground,
|
||||
state.colors.background,
|
||||
cursor_wide,
|
||||
cursor_x,
|
||||
range.y
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3223,26 +3177,14 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
|
||||
fn addCursor(
|
||||
self: *Self,
|
||||
cursor_state: *const terminal.RenderState.Cursor,
|
||||
cursor_style: renderer.CursorStyle,
|
||||
cursor_color: terminal.color.RGB,
|
||||
cell_fg: terminal.color.RGB,
|
||||
cell_bg: terminal.color.RGB,
|
||||
wide: bool,
|
||||
x: u16,
|
||||
y: u16,
|
||||
) void {
|
||||
const cursor_vp = cursor_state.viewport orelse return;
|
||||
|
||||
// Add the cursor. We render the cursor over the wide character if
|
||||
// we're on the wide character tail.
|
||||
const wide, const x = cell: {
|
||||
// The cursor goes over the screen cursor position.
|
||||
if (!cursor_vp.wide_tail) break :cell .{
|
||||
cursor_state.cell.wide == .wide,
|
||||
cursor_vp.x,
|
||||
};
|
||||
|
||||
// If we're part of a wide character, we move the cursor back
|
||||
// to the actual character.
|
||||
break :cell .{ true, cursor_vp.x - 1 };
|
||||
};
|
||||
|
||||
const state: *terminal.RenderState = &self.terminal_state;
|
||||
const alpha: u8 = if (!self.focused) 255 else alpha: {
|
||||
const alpha = 255 * self.config.cursor_opacity;
|
||||
break :alpha @intFromFloat(@ceil(alpha));
|
||||
|
|
@ -3296,10 +3238,32 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
},
|
||||
};
|
||||
|
||||
const cursor_color = cursor_color: {
|
||||
// If an explicit cursor color was set by OSC 12, use that.
|
||||
if (state.colors.cursor) |v| break :cursor_color v;
|
||||
|
||||
// Use our configured color if specified
|
||||
if (self.config.cursor_color) |v| switch (v) {
|
||||
.color => |color| break :cursor_color color.toTerminalRGB(),
|
||||
|
||||
inline .@"cell-foreground",
|
||||
.@"cell-background",
|
||||
=> |_, tag| {
|
||||
break :cursor_color switch (tag) {
|
||||
.color => unreachable,
|
||||
.@"cell-foreground" => cell_fg,
|
||||
.@"cell-background" => cell_bg,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
break :cursor_color state.colors.foreground;
|
||||
};
|
||||
|
||||
self.cells.setCursor(.{
|
||||
.atlas = .grayscale,
|
||||
.bools = .{ .is_cursor_glyph = true },
|
||||
.grid_pos = .{ x, cursor_vp.y },
|
||||
.grid_pos = .{ x, y },
|
||||
.color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha },
|
||||
.glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y },
|
||||
.glyph_size = .{ render.glyph.width, render.glyph.height },
|
||||
|
|
@ -3308,6 +3272,34 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
|
|||
@intCast(render.glyph.offset_y),
|
||||
},
|
||||
}, cursor_style);
|
||||
|
||||
if (cursor_style != .block) return;
|
||||
// set uniform for block cursor
|
||||
|
||||
const uniform_color = if (self.config.cursor_text) |txt| blk: {
|
||||
// If cursor-text is set, then compute the correct color.
|
||||
// Otherwise, use the background color.
|
||||
if (txt == .color) {
|
||||
// Use the color set by cursor-text, if any.
|
||||
break :blk txt.color.toTerminalRGB();
|
||||
}
|
||||
|
||||
break :blk switch (txt) {
|
||||
// If the cell is reversed, use the opposite cell color instead.
|
||||
.@"cell-foreground" => cell_fg,
|
||||
.@"cell-background" => cell_bg,
|
||||
else => unreachable,
|
||||
};
|
||||
} else state.colors.background;
|
||||
|
||||
self.uniforms.cursor_pos = .{ x, y, };
|
||||
self.uniforms.bools.cursor_wide = wide;
|
||||
self.uniforms.cursor_color = .{
|
||||
uniform_color.r,
|
||||
uniform_color.g,
|
||||
uniform_color.b,
|
||||
255,
|
||||
};
|
||||
}
|
||||
|
||||
fn addPreeditCell(
|
||||
|
|
|
|||
Loading…
Reference in New Issue