Skip to content
This repository has been archived by the owner on Jul 15, 2024. It is now read-only.

Support composition on Wayland (and x11) #119

Merged
merged 35 commits into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
23f51d3
Begin to wire up compose events
DJMcNab Jul 21, 2023
2e4d028
Get closer to composing
DJMcNab Jul 21, 2023
54d034f
Start to describe some composition behaviour
DJMcNab Jul 23, 2023
e5ebc7b
Start implementing the new compose behaviour
DJMcNab Jul 23, 2023
8e33fa1
Implement the compose string, but not wired up
DJMcNab Jul 24, 2023
6cc90a8
Commit some more progress
DJMcNab Jul 24, 2023
e29c9b5
Finish composition except for interaction with IME
DJMcNab Jul 24, 2023
358eb5a
Actually use the result as the result
DJMcNab Jul 24, 2023
4853f51
Fix incorrect IME things
DJMcNab Jul 24, 2023
7664445
Fix some lingering bugs
DJMcNab Jul 24, 2023
45c45c2
Fix some remaining ComposeFeedSyms
DJMcNab Jul 24, 2023
c5254fc
Finish? compose handling
DJMcNab Jul 25, 2023
621e486
Re-inline variable
DJMcNab Jul 25, 2023
e8037db
Start to sketch out some improvements
DJMcNab Jul 27, 2023
78cca49
Fix the backspace behaviour
DJMcNab Jul 27, 2023
89788d2
Sketch out the required API for compose with IME
DJMcNab Jul 28, 2023
f7e8b65
Mostly finish handling of compose for x11
DJMcNab Jul 29, 2023
dd8427b
Fix behaviour on window focus loss
DJMcNab Jul 29, 2023
d2d0e2e
Handle missing drops
DJMcNab Jul 29, 2023
718fc89
Start to sketch out more principled text input ownership
DJMcNab Jul 29, 2023
9a0c83e
Start more careful infrastructure for preedit
DJMcNab Jul 31, 2023
7fc6d5f
Update bindgen
DJMcNab Aug 2, 2023
657643b
Start to think through required UX
DJMcNab Aug 2, 2023
c044a24
Some more progress
DJMcNab Aug 3, 2023
f02118d
Finish keyboard handling's interactaction with preedit
DJMcNab Aug 4, 2023
3f64439
Finish before testing IME/Keyboard interaction
DJMcNab Aug 4, 2023
0a36b82
Fix some lingering bugs
DJMcNab Aug 4, 2023
df1a8c6
Add a debug print of the final text, to work around text rendering is…
DJMcNab Aug 5, 2023
15c93d0
Fix boundaries in the edit_text example
DJMcNab Aug 5, 2023
2d4a59e
Solve clippy things
DJMcNab Aug 5, 2023
a351b43
Implement repeat keys
DJMcNab Aug 6, 2023
d738d87
Fix repeat behaviour, and avoid segfault on panic
DJMcNab Aug 6, 2023
d51e210
Give `edit_text` a window title
DJMcNab Aug 6, 2023
7deec45
Undo the x11rb changes
DJMcNab Aug 12, 2023
76c3536
Add some more comments
DJMcNab Aug 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ license = "Apache-2.0"
repository = "https://github.com/linebender/glazier"
description = "Cross-platform native API abstraction for building GUI applications."
keywords = ["gui", "native", "window", "menu", "winit"]
categories = ["gui", "os", "os::windows-apis", "os::macos-apis", "os::linux-apis"]
categories = [
"gui",
"os",
"os::windows-apis",
"os::macos-apis",
"os::linux-apis",
]
exclude = ["/.github/"]
publish = false # Until it's ready

Expand All @@ -18,7 +24,7 @@ default-target = "x86_64-pc-windows-msvc"
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]

[features]
default = ["x11"]
default = ["x11", "wayland"]
x11 = ["ashpd", "bindgen", "futures", "nix", "pkg-config", "x11rb"]
wayland = [
# Required for XKBCommon
Expand Down Expand Up @@ -116,8 +122,11 @@ x11rb = { version = "0.12", features = [
rand = { version = "0.8.0", optional = true }
log = { version = "0.4.14", optional = true }

smithay-client-toolkit = { version = "0.17.0", optional = true, default-features = false, features = [
# Don't use the built-in xkb handling
"calloop",
] }
# Wayland dependencies
smithay-client-toolkit = { version = "0.17.0", optional = true }
# Needed for supporting RawWindowHandle
wayland-backend = { version = "0.1.0", default_features = false, features = [
"client_system",
Expand Down Expand Up @@ -151,7 +160,7 @@ pollster = "0.3.0"
wgpu = "0.17.0"

[target.'cfg(any(target_os = "freebsd", target_os="linux", target_os="openbsd"))'.build-dependencies]
bindgen = { version = "0.60.1", optional = true }
bindgen = { version = "0.66", optional = true }
pkg-config = { version = "0.3.25", optional = true }

[[example]]
Expand Down
4 changes: 4 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ fn main() {
// we use FILE from libc
.blocklist_type("FILE")
.blocklist_type("va_list")
.default_enum_style(bindgen::EnumVariation::NewType {
is_bitfield: true,
is_global: false,
})
.generate()
.expect("Unable to generate bindings");

Expand Down
26 changes: 23 additions & 3 deletions examples/edit_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ fn main() {
let window = glazier::WindowBuilder::new(app.clone())
.resizable(true)
.size((WIDTH as f64 / 2., HEIGHT as f64 / 2.).into())
.title("Edit Text - Glazier Example")
.handler(Box::new(WindowState::new()))
.build()
.unwrap();
Expand Down Expand Up @@ -93,6 +94,22 @@ struct DocumentState {
font_context: FontContext,
}

impl DocumentState {
fn update_selection(&mut self) {
let next_char_boundary = |mut idx| {
if idx > self.text.len() {
panic!("Tried to set selection outside of the string")
}
while !self.text.is_char_boundary(idx) {
idx += 1;
}
idx
};
self.selection.active = next_char_boundary(self.selection.active);
self.selection.anchor = next_char_boundary(self.selection.anchor);
}
}

impl Default for DocumentState {
fn default() -> Self {
let mut this = Self {
Expand Down Expand Up @@ -247,11 +264,11 @@ impl WindowState {
let rect = Rect::from_points(
Point::new(
composition_start.min(composition_end - 1.0),
TEXT_Y + FONT_SIZE as f64 + 2.0,
TEXT_Y + FONT_SIZE as f64 + 7.0,
),
Point::new(
composition_start.max(composition_end + 1.0),
TEXT_Y + FONT_SIZE as f64,
TEXT_Y + FONT_SIZE as f64 + 5.0,
),
);
sb.fill(
Expand Down Expand Up @@ -382,7 +399,9 @@ impl InputHandler for AppInputHandler {
self.state.borrow().composition.clone()
}
fn set_selection(&mut self, range: Selection) {
self.state.borrow_mut().selection = range;
let mut state = self.state.borrow_mut();
state.selection = range;
state.update_selection();
self.window_handle.request_anim_frame();
}
fn set_composition_range(&mut self, range: Option<Range<usize>>) {
Expand All @@ -403,6 +422,7 @@ impl InputHandler for AppInputHandler {
doc.selection.anchor = range.start + text.len();
doc.selection.active = range.start + text.len();
}
doc.update_selection();
doc.refresh_layout();
doc.composition = None;
self.window_handle.request_anim_frame();
Expand Down
19 changes: 11 additions & 8 deletions src/backend/shared/linux/env.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
pub fn locale() -> String {
let mut locale = iso_locale();
// This is done because the locale parsing library we use (TODO - do we?) expects an unicode locale, but these vars have an ISO locale
if let Some(idx) = locale.chars().position(|c| c == '.' || c == '@') {
locale.truncate(idx);
}
locale
}

pub fn iso_locale() -> String {
fn locale_env_var(var: &str) -> Option<String> {
match std::env::var(var) {
Ok(s) if s.is_empty() => {
Expand All @@ -22,18 +31,12 @@ pub fn locale() -> String {

// from gettext manual
// https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html#Locale-Environment-Variables
let mut locale = locale_env_var("LANGUAGE")
locale_env_var("LANGUAGE")
// the LANGUAGE value is priority list separated by :
// See: https://www.gnu.org/software/gettext/manual/html_node/The-LANGUAGE-variable.html#The-LANGUAGE-variable
.and_then(|locale| locale.split(':').next().map(String::from))
.or_else(|| locale_env_var("LC_ALL"))
.or_else(|| locale_env_var("LC_MESSAGES"))
.or_else(|| locale_env_var("LANG"))
.unwrap_or_else(|| "en-US".to_string());

// This is done because the locale parsing library we use expects an unicode locale, but these vars have an ISO locale
if let Some(idx) = locale.chars().position(|c| c == '.' || c == '@') {
locale.truncate(idx);
}
locale
.unwrap_or_else(|| "en-US".to_string())
}
71 changes: 71 additions & 0 deletions src/backend/shared/xkb.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
mod xkb_api;
pub use xkb_api::*;

use crate::{
text::{simulate_compose, InputHandler},
KeyEvent,
};

mod keycodes;
mod xkbcommon_sys;

pub enum KeyboardHandled {
UpdatedReleasingCompose,
UpdatedClaimingCompose,
UpdatedRetainingCompose,
UpdatedNoCompose,
NoUpdate,
}

/// Handle the results of a single keypress event
///
/// `handler` must be a mutable lock, and must not be *owned*
/// by any other input methods. In particular, the composition region
/// must:
/// - Be `None` for the first call to this function
/// - Be `None` if [`KeyEventsState::cancel_composing`]
/// was called since the previous call to this function, as would occur
/// if another IME claimed the input field, or the focused field was changed
/// - Be the range previously set by the previous call to this function in all other cases
///
/// If a different input method exists on the backend, it *must*
/// be removed from the input handler before calling this method
///
/// Note that this does assume that if IME is in some sense *active*,
/// it consumes all keypresses. This is a correct assumption on Wayland[^consumes],
/// and we don't currently intend to implement X11 input methods ourselves.
///
/// [^consumes]: The text input spec doesn't actually make this guarantee, but
/// it also provides no mechanism to mark a keypress as "pre-handled", so
/// in practice all implementations (probably) have to do so
pub fn xkb_simulate_input(
xkb_state: &mut KeyEventsState,
keysym: KeySym,
event: &KeyEvent,
// To handle composition, we have chosen to require being inside a text field
// This does mean that we don't get composition outside of a text field
// but that's expected, as there is no suitable `handler` method for that
// case. We get the same behaviour on macOS (?)

// TODO: There are a few cases where this input lock doesn't need to be mutable (or exist at all)
// e.g. primarily for e.g. pressing control and other modifier keys
// It would require a significant rearchitecture to make it possible to not acquire the lock in
// that case, and this is only a minor inefficiency, but it's better to be sure
handler: &mut dyn InputHandler,
) -> KeyboardHandled {
let compose_result = xkb_state.compose_key_down(event, keysym);
let result_if_update_occurs = match compose_result {
crate::text::CompositionResult::NoComposition => KeyboardHandled::UpdatedNoCompose,
crate::text::CompositionResult::Cancelled(_)
| crate::text::CompositionResult::Finished(_) => KeyboardHandled::UpdatedReleasingCompose,
crate::text::CompositionResult::Updated { just_started, .. } if just_started => {
KeyboardHandled::UpdatedClaimingCompose
}
crate::text::CompositionResult::Updated { .. } => KeyboardHandled::UpdatedRetainingCompose,
};
if simulate_compose(handler, event, compose_result) {
result_if_update_occurs
} else {
KeyboardHandled::NoUpdate
}
}
48 changes: 47 additions & 1 deletion src/backend/shared/xkb/keycodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use keyboard_types::Key;

use super::xkbcommon_sys::*;
use super::{xkbcommon_sys::*, ComposeFeedSym};

/// Map from an xkb_common key code to a key, if possible.
pub fn map_key(keysym: u32) -> Key {
Expand Down Expand Up @@ -193,3 +193,49 @@ pub fn map_key(keysym: u32) -> Key {
_ => Unidentified,
}
}

pub(super) fn map_for_compose(keysym: u32) -> Option<ComposeFeedSym> {
use ComposeFeedSym::*;
let sym = match keysym {
XKB_KEY_dead_grave => DeadGrave,
XKB_KEY_dead_acute => DeadAcute,
XKB_KEY_dead_circumflex => DeadCircumflex,
XKB_KEY_dead_tilde => DeadTilde,
XKB_KEY_dead_macron => DeadMacron,
XKB_KEY_dead_breve => DeadBreve,
XKB_KEY_dead_abovedot => DeadAbovedot,
XKB_KEY_dead_diaeresis => DeadDiaeresis,
XKB_KEY_dead_abovering => DeadAbovering,
XKB_KEY_dead_doubleacute => DeadDoubleacute,
XKB_KEY_dead_caron => DeadCaron,
XKB_KEY_dead_cedilla => DeadCedilla,
XKB_KEY_dead_ogonek => DeadOgonek,
XKB_KEY_dead_iota => DeadIota,
XKB_KEY_dead_voiced_sound => DeadVoicedSound,
XKB_KEY_dead_semivoiced_sound => DeadSemivoicedSound,
XKB_KEY_dead_belowdot => DeadBelowdot,
XKB_KEY_dead_hook => DeadHook,
XKB_KEY_dead_horn => DeadHorn,
XKB_KEY_dead_stroke => DeadStroke,
XKB_KEY_dead_abovecomma => DeadAbovecomma,
XKB_KEY_dead_abovereversedcomma => DeadAbovereversedcomma,
XKB_KEY_dead_doublegrave => DeadDoublegrave,
XKB_KEY_dead_belowring => DeadBelowring,
XKB_KEY_dead_belowmacron => DeadBelowmacron,
XKB_KEY_dead_belowcircumflex => DeadBelowcircumflex,
XKB_KEY_dead_belowtilde => DeadBelowtilde,
XKB_KEY_dead_belowbreve => DeadBelowbreve,
XKB_KEY_dead_belowdiaeresis => DeadBelowdiaeresis,
XKB_KEY_dead_invertedbreve => DeadInvertedbreve,
XKB_KEY_dead_belowcomma => DeadBelowcomma,
XKB_KEY_dead_currency => DeadCurrency,
XKB_KEY_dead_greek => DeadGreek,
XKB_KEY_Multi_key => Compose,
_ => return None,
};
Some(sym)
}

pub fn is_backspace(keysym: u32) -> bool {
keysym == XKB_KEY_BackSpace
}
Loading