1
0
Fork 0
forked from wry/wry

control-center: add input pane

This commit is contained in:
Julian Orth 2026-03-07 14:49:13 +01:00
parent db06d719dd
commit edbdcdca32
11 changed files with 783 additions and 63 deletions

View file

@ -0,0 +1,679 @@
use {
crate::{
backend::{InputDeviceCapability, InputDeviceId},
control_center::{
ControlCenterInner, GridExt, PaneState, bool, bool_ui, combo_box, combo_box_ui,
drag_value, drag_value_ui, grid, grid_label, grid_label_ui, label, text_edit, tip,
},
egui_adapter::egui_platform::icons::ICON_PENDING,
ifs::{
wl_output::WlOutputGlobal,
wl_seat::{SeatId, WlSeatGlobal},
},
kbvm::KbvmMap,
state::{DeviceHandlerData, State},
utils::{errorfmt::ErrorFmt, static_text::StaticText},
},
ahash::AHashMap,
egui::{
CollapsingHeader, ComboBox, DragValue, Event, Grid, Id, TextBuffer, TextFormat, Ui,
UiBuilder, ViewportCommand, Widget, emath::Numeric, text::LayoutJob,
},
isnt::std_1::string::IsntStringExt,
jay_config::keyboard::syms::KeySym,
kbvm::Keysym,
linearize::LinearizeExt,
rand::random,
std::{mem, rc::Rc},
};
pub struct InputPane {
state: Rc<State>,
paste_requested: Option<Id>,
keymaps: AHashMap<Key, KeymapState>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
enum Key {
Seat(SeatId),
Dev(InputDeviceId),
}
struct KeymapState {
seed: u64,
rules_default: bool,
rules: String,
model_default: bool,
model: String,
layouts: String,
variants: String,
options: String,
backup: Option<Rc<KbvmMap>>,
pointer_revert_key: Keysym,
pointer_revert_key_str: Option<String>,
unknown_pointer_revert_key: bool,
}
impl Default for KeymapState {
fn default() -> Self {
Self {
seed: random(),
rules_default: true,
rules: Default::default(),
model_default: true,
model: Default::default(),
layouts: Default::default(),
variants: Default::default(),
options: Default::default(),
backup: Default::default(),
pointer_revert_key: Default::default(),
pointer_revert_key_str: None,
unknown_pointer_revert_key: false,
}
}
}
impl ControlCenterInner {
pub fn create_input_pane(self: &Rc<Self>) -> InputPane {
InputPane {
state: self.state.clone(),
paste_requested: Default::default(),
keymaps: Default::default(),
}
}
}
impl InputPane {
pub fn title(&self, res: &mut String) {
res.push_str("Input");
}
pub fn show(&mut self, ps: &mut PaneState, ui: &mut Ui) {
let state = self.state.clone();
let seats = state.globals.seats.lock();
let mut seats: Vec<_> = seats.values().collect();
seats.sort_by_key(|d| d.seat_name());
for seat in &seats {
self.show_seat(ps, ui, seat);
}
let outputs = state.globals.outputs.lock();
let mut outputs: Vec<_> = outputs.values().collect();
outputs.sort_by_key(|o| &o.connector.name);
let dev = &*state.input_device_handlers.borrow();
let mut dev: Vec<_> = dev.values().collect();
dev.sort_by_key(|d| d.data.device.name());
for dev in dev {
self.show_device(ps, ui, &dev.data, &seats, &outputs);
}
}
fn show_seat(&mut self, ps: &mut PaneState, ui: &mut Ui, seat: &Rc<WlSeatGlobal>) {
let mut layout_job = LayoutJob::default();
layout_job.append(
"Seat",
0.0,
TextFormat {
color: ui.style().visuals.widgets.inactive.text_color(),
..Default::default()
},
);
layout_job.append(
seat.seat_name(),
10.0,
TextFormat {
color: ui.style().visuals.widgets.active.text_color(),
..Default::default()
},
);
let ks = self.keymaps.entry(Key::Seat(seat.id())).or_default();
CollapsingHeader::new(layout_job)
.id_salt(("seat", seat.id()))
.show(ui, |ui| {
grid(ui, ("seat", seat.id()), |ui| {
let mut dv = |name: &str, get: &dyn Fn(&mut (i32, i32)) -> &mut i32| {
let ui = &mut *ui.row();
grid_label(ui, name);
let mut v = seat.get_rate();
let old = v;
ui.horizontal(|ui| {
let v = get(&mut v);
DragValue::new(v).range(0..=i32::MAX).ui(ui);
if ui.button("-20").clicked() {
*v = v.saturating_sub(20).max(0)
}
if ui.button("+20").clicked() {
*v = v.saturating_add(20);
}
});
if v != old {
seat.set_rate(v.0, v.1);
}
};
dv("Repeat Rate", &|v| &mut v.0);
dv("Repeat Delay", &|v| &mut v.1);
drag_value(
ui,
"Cursor Size",
seat.cursor_group().cursor_size(),
0..=u32::MAX,
1.0,
|v| seat.cursor_group().set_cursor_size(v),
);
bool_ui(
ui,
"Simple IM",
|ui| {
tip(ui, |ui| {
ui.label("A simple input method based on Xcompose files.");
ui.label(concat!(
"If you're not using another input method, you should ",
"leave this enabled as it will work for sandboxed ",
"applications, which regular Xcompose will not.",
));
ui.label(concat!(
"The `enable-unicode-input` action can be used to input ",
"characters by their unicode value.",
));
});
},
seat.simple_im_enabled(),
|b| seat.set_simple_im_enabled(b),
);
bool_ui(
ui,
"Hardware Cursor",
|ui| {
tip(ui, |ui| {
ui.label(
"Allow this seat to use the hardware cursor, if available.",
);
ui.label("Only one seat can use the hardware cursor at a time.");
});
},
seat.cursor_group().hardware_cursor(),
|b| seat.cursor_group().set_hardware_cursor(b),
);
{
let ui = &mut *ui.row();
let v = seat.pointer_revert_key();
let v = Keysym(v.0);
if mem::replace(&mut ks.pointer_revert_key, v) != v {
ks.pointer_revert_key_str = None;
}
let name = ks
.pointer_revert_key_str
.get_or_insert_with(|| v.name().unwrap_or_default().to_string());
grid_label_ui(ui, |ui| {
ui.label("Pointer Revert Key");
tip(ui, |ui| {
ui.label(concat!(
"Pressing this key reverts the pointer to the default state, ",
"breaking grabs, drags, etc.",
));
ui.label(
"Setting this to `NoSymbol` effectively disables this feature.",
);
});
});
ui.horizontal(|ui| {
if ui.text_edit_singleline(name).changed() {
let v = Keysym::from_str(name);
ks.unknown_pointer_revert_key = v.is_none();
if let Some(v) = v {
ks.pointer_revert_key = v;
seat.set_pointer_revert_key(KeySym(v.0));
}
}
if ks.unknown_pointer_revert_key {
ui.label("Error: Unknown key");
}
});
}
bool(ui, "Focus Follows Mouse", seat.focus_follows_mouse(), |v| {
seat.set_focus_follows_mouse(v);
});
combo_box_ui(
ui,
"Fallback Output Mode",
|ui| {
tip(ui, |ui| {
ui.label(concat!(
"This determines the output to use in operations where no ",
"output is explicitly specified.",
));
ui.label(concat!(
"For example, when a new window is opened, this determines ",
"where the window will be opened.",
));
ui.label("`Cursor` refers to the output that contains the cursor.");
ui.label(
"`Focus` refers to the output that has the keyboard focus.",
);
});
},
seat.fallback_output_mode(),
|v| seat.set_fallback_output_mode(v),
);
});
ui.label("Focus History");
ui.indent("focus-history", |ui| {
let mut v = seat.focus_history_visible();
if ui.checkbox(&mut v, "Only Visible").changed() {
seat.focus_history_set_visible(v);
}
let mut v = seat.focus_history_same_workspace();
if ui.checkbox(&mut v, "Same Workspace").changed() {
seat.focus_history_set_same_workspace(v);
}
});
if ui.button("Reload Simple IM").clicked() {
seat.reload_simple_im();
}
show_keymap(
&self.state,
ps,
&mut self.paste_requested,
ks,
ui,
Some(&seat.keymap()),
|m| seat.set_seat_keymap(m),
);
});
}
fn show_device(
&mut self,
ps: &mut PaneState,
ui: &mut Ui,
dev: &Rc<DeviceHandlerData>,
seats: &[&Rc<WlSeatGlobal>],
outputs: &[&Rc<WlOutputGlobal>],
) {
let mut layout_job = LayoutJob::default();
layout_job.append(
"Device",
0.0,
TextFormat {
color: ui.style().visuals.widgets.inactive.text_color(),
..Default::default()
},
);
layout_job.append(
&dev.device.name(),
10.0,
TextFormat {
color: ui.style().visuals.widgets.active.text_color(),
..Default::default()
},
);
let dev_id = dev.device.id();
CollapsingHeader::new(layout_job)
.id_salt(("device", dev_id))
.show(ui, |ui| {
grid(ui, ("device", dev_id), |ui| {
{
let old = dev.seat.get();
let ui = &mut *ui.row();
grid_label(ui, "Seat");
let mut seat = old.as_ref();
ui.horizontal(|ui| {
let mut cb = ComboBox::from_id_salt("seat");
if let Some(seat) = seat {
cb = cb.selected_text(seat.seat_name());
}
cb.show_ui(ui, |ui| {
for s in seats {
ui.selectable_value(&mut seat, Some(s), s.seat_name());
}
});
if ui.button("Detach").clicked() {
seat = None;
}
});
if seat != old.as_ref() {
dev.set_seat(&self.state, seat.cloned());
}
}
macro_rules! string {
($field:ident, $name:expr) => {
if let Some(v) = &dev.$field {
label(ui, $name, v);
}
};
}
string!(syspath, "Syspath");
string!(devnode, "Devnode");
{
let ui = &mut *ui.row();
grid_label(ui, "Capabilities");
let mut s = String::new();
for cap in InputDeviceCapability::variants() {
if dev.device.has_capability(cap) {
if s.is_not_empty() {
s.push_str(" | ");
}
s.push_str(cap.text());
}
}
ui.label(s);
}
if let Some(old) = dev.device.natural_scrolling_enabled() {
bool(ui, "Natural Scrolling", old, |v| {
dev.set_natural_scrolling_enabled(&self.state, v)
});
}
if dev.device.has_capability(InputDeviceCapability::Pointer) {
drag_value_ui(
ui,
"Scroll Distance (px)",
|ui| {
tip(ui, |ui| {
ui.label(concat!(
"This only applies to applications that use the ",
"legacy px scrolling events.",
));
});
},
dev.px_per_scroll_wheel.get(),
-f64::INFINITY..=f64::INFINITY,
0.1,
|v| dev.set_px_per_scroll_wheel(&self.state, v),
);
}
if let Some(old) = dev.device.accel_profile() {
combo_box(ui, "Accel Profile", old, |v| {
dev.set_accel_profile(&self.state, v)
});
}
if let Some(old) = dev.device.accel_speed() {
drag_value(ui, "Accel Speed", old, 0.0..=1.0, 0.01, |v| {
dev.set_accel_speed(&self.state, v)
});
}
if let Some(old) = dev.device.click_method() {
combo_box(ui, "Click Method", old, |v| {
dev.set_click_method(&self.state, v)
});
}
if let Some(old) = dev.device.tap_enabled() {
bool(ui, "Tap Enabled", old, |v| {
dev.set_tap_enabled(&self.state, v)
});
}
if let Some(old) = dev.device.drag_enabled() {
bool(ui, "Tap Drag Enabled", old, |v| {
dev.set_drag_enabled(&self.state, v)
});
}
if let Some(old) = dev.device.drag_lock_enabled() {
bool(ui, "Tap Drag Lock Enabled", old, |v| {
dev.set_drag_lock_enabled(&self.state, v)
});
}
if let Some(old) = dev.device.left_handed() {
bool(ui, "Left Handed", old, |v| {
dev.set_left_handed(&self.state, v)
});
}
if let Some(old) = dev.device.middle_button_emulation_enabled() {
bool(ui, "Middle Button Emulation", old, |v| {
dev.set_middle_button_emulation_enabled(&self.state, v)
});
}
{
let ui = &mut *ui.row();
grid_label_ui(ui, |ui| {
ui.label("Output");
tip(ui, |ui| {
ui.label("This applies to touch and tablet input.");
});
});
ui.horizontal(|ui| {
let old = dev.output.get().and_then(|v| v.global.get());
let old = old.as_ref();
let mut v = old;
let mut cb = ComboBox::from_id_salt("output");
if let Some(v) = v {
cb = cb.selected_text(&*v.connector.name);
}
cb.show_ui(ui, |ui| {
for &output in outputs {
ui.selectable_value(
&mut v,
Some(output),
&*output.connector.name,
);
}
});
if v != old {
dev.set_output(&self.state, v.map(|v| &**v));
}
if ui.button("Detach").clicked() {
dev.set_output(&self.state, None);
}
});
}
matrix_ui(
ui,
"Transform Matrix",
|ui| {
tip(ui, |ui| {
ui.label("This matrix is applied to relative pointer movements.");
});
},
dev.device
.has_capability(InputDeviceCapability::Pointer)
.then(|| {
dev.device
.transform_matrix()
.unwrap_or([[1.0, 0.0], [0.0, 1.0]])
}),
|v| dev.set_transform_matrix(&self.state, v),
);
matrix(
ui,
"Calibration Matrix",
dev.device.calibration_matrix(),
|v| dev.set_calibration_matrix(&self.state, v),
);
});
if dev.device.has_capability(InputDeviceCapability::Keyboard) {
ui.collapsing("Device Keymap", |ui| {
let ks = self.keymaps.entry(Key::Dev(dev_id)).or_default();
let map = dev.keymap.get();
ui.add_enabled_ui(map.is_some(), |ui| {
if ui.button("Use Seat Keymap").clicked() {
ks.backup(map.as_ref());
dev.set_keymap(&self.state, None);
}
});
show_keymap(
&self.state,
ps,
&mut self.paste_requested,
ks,
ui,
map.as_ref(),
|m| {
dev.set_keymap(&self.state, Some(m.clone()));
},
);
});
}
});
}
}
impl KeymapState {
fn backup(&mut self, map: Option<&Rc<KbvmMap>>) {
if self.backup.is_none()
&& let Some(map) = map
{
self.backup = Some(map.clone());
}
}
}
fn show_keymap(
state: &State,
ps: &mut PaneState,
paste_requested: &mut Option<Id>,
ks: &mut KeymapState,
ui: &mut Ui,
map: Option<&Rc<KbvmMap>>,
set_map: impl Fn(&Rc<KbvmMap>),
) {
ui.scope_builder(UiBuilder::new().id(("keymap-settings", ks.seed)), |ui| {
ui.add_enabled_ui(map.is_some(), |ui| {
if ui.button("Copy Keymap").clicked()
&& let Some(map) = map
{
ui.ctx().copy_text(map.map_text.clone());
}
});
let backup = |ks: &mut KeymapState| {
ks.backup(map);
};
if ui.button("Load Default Keymap").clicked() {
backup(ks);
set_map(&state.default_keymap);
}
ui.horizontal(|ui| {
ui.add_enabled_ui(map.is_some(), |ui| {
if ui.button("Backup Keymap").clicked() {
ks.backup = None;
backup(ks);
}
});
if let Some(backup) = &ks.backup
&& ui.button("Restore Keymap").clicked()
{
set_map(backup);
ks.backup = None;
}
});
let mut label = "Load Keymap from Clipboard".to_string();
if *paste_requested == Some(ui.id()) {
label.push_str(" ");
label.push_str(ICON_PENDING);
}
let button = ui.button(label);
if button.clicked() {
*paste_requested = Some(ui.id());
button.request_focus();
ui.ctx().send_viewport_cmd(ViewportCommand::RequestPaste);
} else if *paste_requested == Some(ui.id()) && button.has_focus() {
ui.input(|e| {
let map = e
.events
.iter()
.filter_map(|e| match e {
Event::Paste(s) => Some(s),
_ => None,
})
.next();
let Some(map) = map else {
return;
};
*paste_requested = None;
let map = match state.kb_ctx.parse_keymap(map.as_bytes()) {
Ok(m) => m,
Err(e) => {
let error = format!("Could not parse keymap: {}", ErrorFmt(e));
ps.errors.push(error);
return;
}
};
backup(ks);
set_map(&map);
});
} else if *paste_requested == Some(ui.id()) {
*paste_requested = None;
}
ui.collapsing("Create Keymap from Names", |ui| {
grid(ui, ("keymap-from-names", ui.id()), |ui| {
let defaulted =
|ui: &mut Ui, name: &str, default: &mut bool, text: &mut dyn TextBuffer| {
let ui = &mut *ui.row();
grid_label(ui, name);
ui.add_enabled_ui(!*default, |ui| {
text_edit(ui, text);
});
ui.checkbox(default, "Default");
};
let required = |ui: &mut Ui, name, text| {
let ui = &mut *ui.row();
grid_label(ui, name);
text_edit(ui, text);
};
defaulted(ui, "Rules", &mut ks.rules_default, &mut ks.rules);
defaulted(ui, "Model", &mut ks.model_default, &mut ks.model);
required(ui, "Layouts", &mut ks.layouts);
required(ui, "Variants", &mut ks.variants);
required(ui, "Options", &mut ks.options);
});
if ui.button("Load").clicked() {
'set_map: {
let map = state.kb_ctx.keymap_from_rmlvo(
(!ks.rules_default).then_some(&ks.rules),
(!ks.model_default).then_some(&ks.model),
Some(&ks.layouts),
Some(&ks.variants),
Some(&ks.options),
);
let map = match map {
Ok(map) => map,
Err(e) => {
let error = format!("Could not parse keymap: {}", ErrorFmt(e));
ps.errors.push(error);
break 'set_map;
}
};
backup(ks);
set_map(&map);
}
}
});
});
}
fn matrix<T, const W: usize>(
ui: &mut Ui,
name: &str,
old: Option<[[T; W]; 2]>,
set: impl FnOnce([[T; W]; 2]),
) where
T: Numeric,
{
matrix_ui(ui, name, |_| (), old, set);
}
fn matrix_ui<R, T, const W: usize>(
ui: &mut Ui,
name: &str,
label: impl FnOnce(&mut Ui) -> R,
old: Option<[[T; W]; 2]>,
set: impl FnOnce([[T; W]; 2]),
) where
T: Numeric,
{
if let Some(mut m) = old {
let old = m;
let ui = &mut *ui.row();
grid_label_ui(ui, |ui| {
ui.label(name);
label(ui);
});
Grid::new(name).show(ui, |ui| {
for row in &mut m {
for cell in row {
DragValue::new(cell).speed(0.01).ui(ui);
}
ui.end_row();
}
});
if old != m {
set(m);
}
}
}