forked from NotAShelf/beer
wayland: render IME preedit and commit via text-input-v3
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I84a3735ca2e75e63d098fb17836ffd786a6a6964
This commit is contained in:
parent
155954a491
commit
53924d381a
2 changed files with 168 additions and 0 deletions
|
|
@ -250,6 +250,46 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw the IME preedit string inline, starting at grid cell `start_col` of
|
||||||
|
/// row `row`, over whatever was there. The preedit sits on the selection
|
||||||
|
/// background and is underlined so it reads as uncommitted, in-flight text.
|
||||||
|
pub fn render_preedit(
|
||||||
|
&mut self,
|
||||||
|
pixels: &mut [u8],
|
||||||
|
dims: (usize, usize),
|
||||||
|
theme: &Theme,
|
||||||
|
row: usize,
|
||||||
|
start_col: usize,
|
||||||
|
text: &str,
|
||||||
|
) {
|
||||||
|
let (width, height) = dims;
|
||||||
|
let mut canvas = Canvas {
|
||||||
|
pixels,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
let m = self.fonts.metrics();
|
||||||
|
let (pad_x, pad_y) = self.pad;
|
||||||
|
let row_top = pad_y + row as i32 * m.height as i32;
|
||||||
|
let style = Style {
|
||||||
|
bold: false,
|
||||||
|
italic: false,
|
||||||
|
};
|
||||||
|
let mut x = pad_x + start_col as i32 * m.width as i32;
|
||||||
|
for c in text.chars() {
|
||||||
|
if x < 0 || x as usize + m.width as usize > width {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
canvas.fill_rect(x, row_top, m.width, m.height, theme.selection_bg);
|
||||||
|
if c != ' ' {
|
||||||
|
self.draw_glyph(&mut canvas, c, style, x, row_top, theme.fg);
|
||||||
|
}
|
||||||
|
// Underline the run a row above the cell bottom.
|
||||||
|
canvas.hline(x, row_top + m.height as i32 - 2, m.width, theme.fg);
|
||||||
|
x += m.width as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw the cursor: a solid block/underline/beam when focused, a hollow
|
/// Draw the cursor: a solid block/underline/beam when focused, a hollow
|
||||||
/// outline when not. A blinking cursor shape is only drawn while `blink_on`.
|
/// outline when not. A blinking cursor shape is only drawn while `blink_on`.
|
||||||
fn draw_cursor(
|
fn draw_cursor(
|
||||||
|
|
|
||||||
128
src/wayland.rs
128
src/wayland.rs
|
|
@ -82,6 +82,10 @@ use wayland_protocols::wp::fractional_scale::v1::client::{
|
||||||
wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1,
|
wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1,
|
||||||
wp_fractional_scale_v1::{self, WpFractionalScaleV1},
|
wp_fractional_scale_v1::{self, WpFractionalScaleV1},
|
||||||
};
|
};
|
||||||
|
use wayland_protocols::wp::text_input::zv3::client::{
|
||||||
|
zwp_text_input_manager_v3::ZwpTextInputManagerV3,
|
||||||
|
zwp_text_input_v3::{self, ContentHint, ContentPurpose, ZwpTextInputV3},
|
||||||
|
};
|
||||||
use wayland_protocols::wp::viewporter::client::{
|
use wayland_protocols::wp::viewporter::client::{
|
||||||
wp_viewport::WpViewport, wp_viewporter::WpViewporter,
|
wp_viewport::WpViewport, wp_viewporter::WpViewporter,
|
||||||
};
|
};
|
||||||
|
|
@ -137,6 +141,8 @@ struct RowSnap {
|
||||||
search: Vec<(usize, usize, bool)>,
|
search: Vec<(usize, usize, bool)>,
|
||||||
/// Search-prompt text drawn over this row (only the bottom row, when active).
|
/// Search-prompt text drawn over this row (only the bottom row, when active).
|
||||||
overlay: Option<String>,
|
overlay: Option<String>,
|
||||||
|
/// IME preedit `(start_col, text)` drawn inline over this row (cursor row).
|
||||||
|
preedit: Option<(usize, String)>,
|
||||||
/// Blink phase, but only varied when the row actually has blinking ink, so
|
/// Blink phase, but only varied when the row actually has blinking ink, so
|
||||||
/// non-blinking rows stay equal across phase toggles.
|
/// non-blinking rows stay equal across phase toggles.
|
||||||
blink: bool,
|
blink: bool,
|
||||||
|
|
@ -168,6 +174,7 @@ fn row_snap(grid: &Grid, y: usize, focused: bool, blink_on: bool) -> RowSnap {
|
||||||
sel: grid.selection_span_on(abs),
|
sel: grid.selection_span_on(abs),
|
||||||
search: grid.search_spans_on(abs),
|
search: grid.search_spans_on(abs),
|
||||||
overlay: None,
|
overlay: None,
|
||||||
|
preedit: None,
|
||||||
blink: if has_blink { blink_on } else { true },
|
blink: if has_blink { blink_on } else { true },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -215,6 +222,7 @@ pub fn run(config: Config, config_path: Option<std::path::PathBuf>) -> anyhow::R
|
||||||
bind_global::<WpFractionalScaleManagerV1>(&globals, &qh)
|
bind_global::<WpFractionalScaleManagerV1>(&globals, &qh)
|
||||||
.map(|mgr| mgr.get_fractional_scale(window.wl_surface(), &qh, ()))
|
.map(|mgr| mgr.get_fractional_scale(window.wl_surface(), &qh, ()))
|
||||||
});
|
});
|
||||||
|
let text_input_manager = bind_global::<ZwpTextInputManagerV3>(&globals, &qh);
|
||||||
|
|
||||||
// First commit with no buffer kicks off the initial configure.
|
// First commit with no buffer kicks off the initial configure.
|
||||||
window.commit();
|
window.commit();
|
||||||
|
|
@ -251,6 +259,10 @@ pub fn run(config: Config, config_path: Option<std::path::PathBuf>) -> anyhow::R
|
||||||
data_device_manager,
|
data_device_manager,
|
||||||
primary_manager,
|
primary_manager,
|
||||||
cursor_shape_manager,
|
cursor_shape_manager,
|
||||||
|
text_input_manager,
|
||||||
|
preedit: String::new(),
|
||||||
|
ime_preedit_pending: String::new(),
|
||||||
|
ime_commit_pending: String::new(),
|
||||||
viewport,
|
viewport,
|
||||||
fractional_scale,
|
fractional_scale,
|
||||||
scale120: 120,
|
scale120: 120,
|
||||||
|
|
@ -393,6 +405,8 @@ struct SeatData {
|
||||||
cursor_shape_device: Option<WpCursorShapeDeviceV1>,
|
cursor_shape_device: Option<WpCursorShapeDeviceV1>,
|
||||||
data_device: Option<DataDevice>,
|
data_device: Option<DataDevice>,
|
||||||
primary_device: Option<PrimarySelectionDevice>,
|
primary_device: Option<PrimarySelectionDevice>,
|
||||||
|
/// text-input-v3 handle for IME preedit/commit, if the compositor offers it.
|
||||||
|
text_input: Option<ZwpTextInputV3>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Window + Wayland client state shared across all protocol handlers.
|
/// Window + Wayland client state shared across all protocol handlers.
|
||||||
|
|
@ -411,6 +425,13 @@ struct App {
|
||||||
primary_manager: Option<PrimarySelectionManagerState>,
|
primary_manager: Option<PrimarySelectionManagerState>,
|
||||||
/// Sets the pointer to an I-beam over the window (cursor-shape-v1).
|
/// Sets the pointer to an I-beam over the window (cursor-shape-v1).
|
||||||
cursor_shape_manager: Option<CursorShapeManager>,
|
cursor_shape_manager: Option<CursorShapeManager>,
|
||||||
|
/// IME manager (text-input-v3); per-seat handles live in `seats`.
|
||||||
|
text_input_manager: Option<ZwpTextInputManagerV3>,
|
||||||
|
/// Committed IME preedit string shown inline at the cursor while composing.
|
||||||
|
preedit: String,
|
||||||
|
/// Preedit/commit accumulated since the last text-input `done`.
|
||||||
|
ime_preedit_pending: String,
|
||||||
|
ime_commit_pending: String,
|
||||||
/// Presents a scaled buffer at the logical surface size (viewporter).
|
/// Presents a scaled buffer at the logical surface size (viewporter).
|
||||||
viewport: Option<WpViewport>,
|
viewport: Option<WpViewport>,
|
||||||
/// Per-surface fractional-scale object; kept alive to receive scale events.
|
/// Per-surface fractional-scale object; kept alive to receive scale events.
|
||||||
|
|
@ -837,6 +858,7 @@ impl App {
|
||||||
cursor_shape_device: None,
|
cursor_shape_device: None,
|
||||||
data_device: None,
|
data_device: None,
|
||||||
primary_device: None,
|
primary_device: None,
|
||||||
|
text_input: None,
|
||||||
});
|
});
|
||||||
self.seats.len() - 1
|
self.seats.len() - 1
|
||||||
}
|
}
|
||||||
|
|
@ -1125,6 +1147,39 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Point the IME's candidate popup at the terminal cursor, in logical
|
||||||
|
/// surface coordinates (the renderer works in physical pixels, so divide
|
||||||
|
/// the physical cell rectangle back down by the scale).
|
||||||
|
fn ime_set_cursor_rect(&self, ti: &ZwpTextInputV3) {
|
||||||
|
let Some(session) = self.session.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (cx, cy) = session.term.grid().cursor();
|
||||||
|
let m = self.renderer.metrics();
|
||||||
|
let s = f64::from(self.scale120) / 120.0;
|
||||||
|
let pad_x = f64::from(self.to_phys(self.config.main.pad_x));
|
||||||
|
let pad_y = f64::from(self.to_phys(self.config.main.pad_y));
|
||||||
|
let x = ((pad_x + cx as f64 * f64::from(m.width)) / s) as i32;
|
||||||
|
let y = ((pad_y + cy as f64 * f64::from(m.height)) / s) as i32;
|
||||||
|
let w = (f64::from(m.width) / s) as i32;
|
||||||
|
let h = (f64::from(m.height) / s) as i32;
|
||||||
|
ti.set_cursor_rectangle(x, y, w.max(1), h.max(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply one IME transaction: commit any committed text to the shell, adopt
|
||||||
|
/// the new preedit, then re-commit our state (cursor rectangle) to the IME.
|
||||||
|
fn ime_done(&mut self, ti: &ZwpTextInputV3) {
|
||||||
|
let commit = std::mem::take(&mut self.ime_commit_pending);
|
||||||
|
// Preedit is replaced wholesale each cycle; an absent preedit clears it.
|
||||||
|
self.preedit = std::mem::take(&mut self.ime_preedit_pending);
|
||||||
|
if !commit.is_empty() {
|
||||||
|
self.send_to_shell(commit.as_bytes());
|
||||||
|
}
|
||||||
|
self.ime_set_cursor_rect(ti);
|
||||||
|
ti.commit();
|
||||||
|
self.needs_draw = true;
|
||||||
|
}
|
||||||
|
|
||||||
/// Act on the OSC 52 clipboard requests an application made: take ownership
|
/// Act on the OSC 52 clipboard requests an application made: take ownership
|
||||||
/// of the selection it set, or answer a query with what we currently hold.
|
/// of the selection it set, or answer a query with what we currently hold.
|
||||||
fn handle_clipboard_ops(&mut self, ops: Vec<crate::vt::ClipboardOp>) {
|
fn handle_clipboard_ops(&mut self, ops: Vec<crate::vt::ClipboardOp>) {
|
||||||
|
|
@ -1414,6 +1469,14 @@ impl App {
|
||||||
cur[rows - 1].overlay = Some(text.clone());
|
cur[rows - 1].overlay = Some(text.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The IME preedit is drawn inline at the cursor while composing.
|
||||||
|
if !self.preedit.is_empty() && grid.view_at_bottom() {
|
||||||
|
let (cx, cy) = grid.cursor();
|
||||||
|
if cy < rows {
|
||||||
|
cur[cy].preedit = Some((cx, self.preedit.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reuse a buffer the compositor has released, else grow the ring.
|
// Reuse a buffer the compositor has released, else grow the ring.
|
||||||
let stride = w as i32 * 4;
|
let stride = w as i32 * 4;
|
||||||
let mut idx = None;
|
let mut idx = None;
|
||||||
|
|
@ -1486,6 +1549,13 @@ impl App {
|
||||||
self.renderer
|
self.renderer
|
||||||
.render_search_bar(canvas, dims, theme, rows - 1, text);
|
.render_search_bar(canvas, dims, theme, rows - 1, text);
|
||||||
}
|
}
|
||||||
|
// Draw the IME preedit inline over its (repainted) cursor row.
|
||||||
|
for &y in &dirty {
|
||||||
|
if let Some((col, text)) = &cur[y].preedit {
|
||||||
|
self.renderer
|
||||||
|
.render_preedit(canvas, dims, theme, y, *col, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
self.frames[idx].rows = cur;
|
self.frames[idx].rows = cur;
|
||||||
|
|
||||||
let surface = self.window.wl_surface();
|
let surface = self.window.wl_surface();
|
||||||
|
|
@ -1640,6 +1710,11 @@ impl SeatHandler for App {
|
||||||
Ok(keyboard) => self.seats[i].keyboard = Some(keyboard),
|
Ok(keyboard) => self.seats[i].keyboard = Some(keyboard),
|
||||||
Err(err) => tracing::warn!("get keyboard: {err}"),
|
Err(err) => tracing::warn!("get keyboard: {err}"),
|
||||||
}
|
}
|
||||||
|
if self.seats[i].text_input.is_none()
|
||||||
|
&& let Some(mgr) = self.text_input_manager.as_ref()
|
||||||
|
{
|
||||||
|
self.seats[i].text_input = Some(mgr.get_text_input(&seat, qh, ()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if capability == Capability::Pointer && self.seats[i].pointer.is_none() {
|
if capability == Capability::Pointer && self.seats[i].pointer.is_none() {
|
||||||
match self.seat_state.get_pointer(qh, &seat) {
|
match self.seat_state.get_pointer(qh, &seat) {
|
||||||
|
|
@ -2104,6 +2179,59 @@ impl Dispatch<WpViewport, ()> for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Dispatch<ZwpTextInputManagerV3, ()> for App {
|
||||||
|
fn event(
|
||||||
|
_: &mut Self,
|
||||||
|
_: &ZwpTextInputManagerV3,
|
||||||
|
_: <ZwpTextInputManagerV3 as Proxy>::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// text-input-v3 batches preedit/commit between `enter` and `done`; we apply the
|
||||||
|
// accumulated transaction on `done` and re-enable on focus enter.
|
||||||
|
impl Dispatch<ZwpTextInputV3, ()> for App {
|
||||||
|
fn event(
|
||||||
|
state: &mut Self,
|
||||||
|
ti: &ZwpTextInputV3,
|
||||||
|
event: zwp_text_input_v3::Event,
|
||||||
|
_: &(),
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
) {
|
||||||
|
use zwp_text_input_v3::Event;
|
||||||
|
match event {
|
||||||
|
Event::Enter { .. } => {
|
||||||
|
ti.enable();
|
||||||
|
ti.set_content_type(ContentHint::None, ContentPurpose::Terminal);
|
||||||
|
state.ime_set_cursor_rect(ti);
|
||||||
|
ti.commit();
|
||||||
|
}
|
||||||
|
Event::Leave { .. } => {
|
||||||
|
ti.disable();
|
||||||
|
ti.commit();
|
||||||
|
state.preedit.clear();
|
||||||
|
state.ime_preedit_pending.clear();
|
||||||
|
state.ime_commit_pending.clear();
|
||||||
|
state.needs_draw = true;
|
||||||
|
}
|
||||||
|
Event::PreeditString { text, .. } => {
|
||||||
|
state.ime_preedit_pending = text.unwrap_or_default();
|
||||||
|
}
|
||||||
|
Event::CommitString { text } => {
|
||||||
|
state.ime_commit_pending.push_str(&text.unwrap_or_default());
|
||||||
|
}
|
||||||
|
Event::Done { .. } => state.ime_done(ti),
|
||||||
|
// We do not expose surrounding text, so nothing to delete.
|
||||||
|
Event::DeleteSurroundingText { .. } => {}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate_compositor!(App);
|
delegate_compositor!(App);
|
||||||
delegate_output!(App);
|
delegate_output!(App);
|
||||||
delegate_shm!(App);
|
delegate_shm!(App);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue