WIP: keyboard and controller support
This commit is contained in:
parent
8bba628d98
commit
e5a4dbdce7
5 changed files with 453 additions and 4 deletions
174
Cargo.lock
generated
174
Cargo.lock
generated
|
|
@ -315,6 +315,12 @@ version = "0.1.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
|
|
@ -459,6 +465,40 @@ dependencies = [
|
|||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gilrs"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fa85c2e35dc565c90511917897ea4eae16b77f2773d5223536f7b602536d462"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"gilrs-core",
|
||||
"log",
|
||||
"uuid",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gilrs-core"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d23f2cc5144060a7f8d9e02d3fce5d06705376568256a509cdbc3c24d47e4f04"
|
||||
dependencies = [
|
||||
"inotify",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
"log",
|
||||
"nix 0.30.1",
|
||||
"objc2-core-foundation",
|
||||
"objc2-io-kit",
|
||||
"uuid",
|
||||
"vec_map",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.20.12"
|
||||
|
|
@ -762,6 +802,26 @@ dependencies = [
|
|||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
|
|
@ -823,6 +883,16 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
|
|
@ -892,6 +962,18 @@ dependencies = [
|
|||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
|
|
@ -910,6 +992,26 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-io-kit"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
|
|
@ -1256,7 +1358,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"async-trait",
|
||||
"dirs",
|
||||
"nix",
|
||||
"nix 0.29.0",
|
||||
"serde",
|
||||
"shell-escape",
|
||||
"shepherd-api",
|
||||
|
|
@ -1291,7 +1393,7 @@ dependencies = [
|
|||
name = "shepherd-ipc"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"nix 0.29.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shepherd-api",
|
||||
|
|
@ -1310,6 +1412,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"clap",
|
||||
"dirs",
|
||||
"gilrs",
|
||||
"gtk4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -1720,6 +1823,12 @@ version = "0.2.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.1"
|
||||
|
|
@ -1792,6 +1901,37 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core",
|
||||
"windows-future",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
|
|
@ -1805,6 +1945,17 @@ dependencies = [
|
|||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
|
|
@ -1833,6 +1984,16 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
|
|
@ -1935,6 +2096,15 @@ dependencies = [
|
|||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ tracing-subscriber = { workspace = true }
|
|||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
dirs = "5.0"
|
||||
gilrs = "0.11"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ use gtk4::glib;
|
|||
use gtk4::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, error, info};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::client::{CommandClient, ServiceClient};
|
||||
use crate::grid::LauncherGrid;
|
||||
|
|
@ -41,6 +42,13 @@ window {
|
|||
border-color: #4a90d9;
|
||||
}
|
||||
|
||||
.launcher-tile:focus,
|
||||
.launcher-tile:focus-visible {
|
||||
background: #1f3460;
|
||||
background-color: #1f3460;
|
||||
border-color: #ffd166;
|
||||
}
|
||||
|
||||
.launcher-tile:active {
|
||||
background: #0f3460;
|
||||
background-color: #0f3460;
|
||||
|
|
@ -155,6 +163,8 @@ impl LauncherApp {
|
|||
stack.add_named(&disconnected_view.0, Some("disconnected"));
|
||||
|
||||
window.set_child(Some(&stack));
|
||||
Self::setup_keyboard_input(&window, &grid);
|
||||
Self::setup_gamepad_input(&window, &grid);
|
||||
|
||||
// Create shared state
|
||||
let state = SharedState::new();
|
||||
|
|
@ -342,6 +352,7 @@ impl LauncherApp {
|
|||
if let Some(grid) = grid {
|
||||
grid.set_entries(entries);
|
||||
grid.set_tiles_sensitive(true);
|
||||
grid.grab_focus();
|
||||
}
|
||||
if let Some(ref win) = window {
|
||||
win.set_visible(true);
|
||||
|
|
@ -381,6 +392,164 @@ impl LauncherApp {
|
|||
window.present();
|
||||
}
|
||||
|
||||
fn setup_keyboard_input(window: >k4::ApplicationWindow, grid: &LauncherGrid) {
|
||||
let key_controller = gtk4::EventControllerKey::new();
|
||||
key_controller.set_propagation_phase(gtk4::PropagationPhase::Capture);
|
||||
let grid_weak = grid.downgrade();
|
||||
key_controller.connect_key_pressed(move |_, key, _, _| {
|
||||
let Some(grid) = grid_weak.upgrade() else {
|
||||
return glib::Propagation::Proceed;
|
||||
};
|
||||
|
||||
let handled = match key {
|
||||
gtk4::gdk::Key::Up | gtk4::gdk::Key::w | gtk4::gdk::Key::W => {
|
||||
grid.move_selection(0, -1);
|
||||
true
|
||||
}
|
||||
gtk4::gdk::Key::Down | gtk4::gdk::Key::s | gtk4::gdk::Key::S => {
|
||||
grid.move_selection(0, 1);
|
||||
true
|
||||
}
|
||||
gtk4::gdk::Key::Left | gtk4::gdk::Key::a | gtk4::gdk::Key::A => {
|
||||
grid.move_selection(-1, 0);
|
||||
true
|
||||
}
|
||||
gtk4::gdk::Key::Right | gtk4::gdk::Key::d | gtk4::gdk::Key::D => {
|
||||
grid.move_selection(1, 0);
|
||||
true
|
||||
}
|
||||
gtk4::gdk::Key::Return | gtk4::gdk::Key::KP_Enter | gtk4::gdk::Key::space => {
|
||||
grid.launch_selected();
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if handled {
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
window.add_controller(key_controller);
|
||||
|
||||
let exit_controller = gtk4::EventControllerKey::new();
|
||||
let window_weak = window.downgrade();
|
||||
exit_controller.connect_key_pressed(move |_, key, _, modifiers| {
|
||||
let alt_f4 = key == gtk4::gdk::Key::F4
|
||||
&& modifiers.intersects(gtk4::gdk::ModifierType::ALT_MASK);
|
||||
let ctrl_w = (key == gtk4::gdk::Key::w || key == gtk4::gdk::Key::W)
|
||||
&& modifiers.intersects(gtk4::gdk::ModifierType::CONTROL_MASK);
|
||||
let home = key == gtk4::gdk::Key::Home || key == gtk4::gdk::Key::HomePage;
|
||||
|
||||
if alt_f4 || ctrl_w || home {
|
||||
if let Some(window) = window_weak.upgrade() {
|
||||
window.close();
|
||||
}
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
});
|
||||
window.add_controller(exit_controller);
|
||||
}
|
||||
|
||||
fn setup_gamepad_input(window: >k4::ApplicationWindow, grid: &LauncherGrid) {
|
||||
let mut gilrs = match gilrs::Gilrs::new() {
|
||||
Ok(gilrs) => gilrs,
|
||||
Err(e) => {
|
||||
warn!(error = %e, "Gamepad input unavailable");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let grid_weak = grid.downgrade();
|
||||
let window_weak = window.downgrade();
|
||||
let mut axis_state = GamepadAxisState::default();
|
||||
|
||||
glib::timeout_add_local(Duration::from_millis(16), move || {
|
||||
while let Some(event) = gilrs.next_event() {
|
||||
let Some(grid) = grid_weak.upgrade() else {
|
||||
return glib::ControlFlow::Break;
|
||||
};
|
||||
|
||||
match event.event {
|
||||
gilrs::EventType::ButtonPressed(button, _) => match button {
|
||||
gilrs::Button::DPadUp => grid.move_selection(0, -1),
|
||||
gilrs::Button::DPadDown => grid.move_selection(0, 1),
|
||||
gilrs::Button::DPadLeft => grid.move_selection(-1, 0),
|
||||
gilrs::Button::DPadRight => grid.move_selection(1, 0),
|
||||
gilrs::Button::South | gilrs::Button::East | gilrs::Button::Start => {
|
||||
grid.launch_selected();
|
||||
}
|
||||
gilrs::Button::Mode => {
|
||||
if let Some(window) = window_weak.upgrade() {
|
||||
window.close();
|
||||
return glib::ControlFlow::Break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
gilrs::EventType::AxisChanged(axis, value, _) => {
|
||||
Self::handle_gamepad_axis(&grid, axis, value, &mut axis_state);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
glib::ControlFlow::Continue
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_gamepad_axis(
|
||||
grid: &LauncherGrid,
|
||||
axis: gilrs::Axis,
|
||||
value: f32,
|
||||
axis_state: &mut GamepadAxisState,
|
||||
) {
|
||||
const THRESHOLD: f32 = 0.65;
|
||||
|
||||
match axis {
|
||||
gilrs::Axis::LeftStickX | gilrs::Axis::DPadX => {
|
||||
if value <= -THRESHOLD {
|
||||
if !axis_state.left {
|
||||
grid.move_selection(-1, 0);
|
||||
}
|
||||
axis_state.left = true;
|
||||
axis_state.right = false;
|
||||
} else if value >= THRESHOLD {
|
||||
if !axis_state.right {
|
||||
grid.move_selection(1, 0);
|
||||
}
|
||||
axis_state.right = true;
|
||||
axis_state.left = false;
|
||||
} else {
|
||||
axis_state.left = false;
|
||||
axis_state.right = false;
|
||||
}
|
||||
}
|
||||
gilrs::Axis::LeftStickY | gilrs::Axis::DPadY => {
|
||||
if value <= -THRESHOLD {
|
||||
if !axis_state.up {
|
||||
grid.move_selection(0, -1);
|
||||
}
|
||||
axis_state.up = true;
|
||||
axis_state.down = false;
|
||||
} else if value >= THRESHOLD {
|
||||
if !axis_state.down {
|
||||
grid.move_selection(0, 1);
|
||||
}
|
||||
axis_state.down = true;
|
||||
axis_state.up = false;
|
||||
} else {
|
||||
axis_state.up = false;
|
||||
axis_state.down = false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_loading_view() -> gtk4::Box {
|
||||
let container = gtk4::Box::new(gtk4::Orientation::Vertical, 16);
|
||||
container.set_halign(gtk4::Align::Center);
|
||||
|
|
@ -458,3 +627,11 @@ impl LauncherApp {
|
|||
(container, retry_button)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GamepadAxisState {
|
||||
left: bool,
|
||||
right: bool,
|
||||
up: bool,
|
||||
down: bool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ mod imp {
|
|||
|
||||
// Configure flow box
|
||||
self.flow_box.set_homogeneous(true);
|
||||
self.flow_box.set_selection_mode(gtk4::SelectionMode::None);
|
||||
self.flow_box.set_selection_mode(gtk4::SelectionMode::Single);
|
||||
self.flow_box.set_max_children_per_line(6);
|
||||
self.flow_box.set_min_children_per_line(2);
|
||||
self.flow_box.set_row_spacing(24);
|
||||
|
|
@ -60,6 +60,7 @@ mod imp {
|
|||
self.flow_box.set_valign(gtk4::Align::Center);
|
||||
self.flow_box.set_hexpand(true);
|
||||
self.flow_box.set_vexpand(true);
|
||||
self.flow_box.set_focusable(true);
|
||||
self.flow_box.add_css_class("launcher-grid");
|
||||
|
||||
// Wrap in a scrolled window
|
||||
|
|
@ -125,6 +126,8 @@ impl LauncherGrid {
|
|||
imp.flow_box.insert(&tile, -1);
|
||||
imp.tiles.borrow_mut().push(tile);
|
||||
}
|
||||
|
||||
self.select_first();
|
||||
}
|
||||
|
||||
/// Enable or disable all tiles
|
||||
|
|
@ -133,6 +136,82 @@ impl LauncherGrid {
|
|||
tile.set_sensitive(sensitive);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_first(&self) {
|
||||
let imp = self.imp();
|
||||
if let Some(child) = imp.flow_box.child_at_index(0) {
|
||||
imp.flow_box.select_child(&child);
|
||||
child.grab_focus();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_selection(&self, dx: i32, dy: i32) {
|
||||
let imp = self.imp();
|
||||
let tile_count = imp.tiles.borrow().len() as i32;
|
||||
if tile_count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let current_index = imp
|
||||
.flow_box
|
||||
.selected_children()
|
||||
.first()
|
||||
.map(|child| child.index())
|
||||
.unwrap_or(0);
|
||||
|
||||
let columns = self.estimated_columns(tile_count);
|
||||
let mut new_index = current_index + dx + (dy * columns);
|
||||
new_index = new_index.clamp(0, tile_count - 1);
|
||||
|
||||
if let Some(child) = imp.flow_box.child_at_index(new_index) {
|
||||
imp.flow_box.select_child(&child);
|
||||
child.grab_focus();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch_selected(&self) {
|
||||
let imp = self.imp();
|
||||
let maybe_child = imp.flow_box.selected_children().first().cloned();
|
||||
let Some(child) = maybe_child else {
|
||||
return;
|
||||
};
|
||||
|
||||
let index = child.index();
|
||||
if index < 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let tile = imp.tiles.borrow().get(index as usize).cloned();
|
||||
if let Some(tile) = tile {
|
||||
if !tile.is_sensitive() {
|
||||
return;
|
||||
}
|
||||
if let Some(entry_id) = tile.entry_id()
|
||||
&& let Some(callback) = imp.on_launch.borrow().as_ref()
|
||||
{
|
||||
callback(entry_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn estimated_columns(&self, tile_count: i32) -> i32 {
|
||||
let imp = self.imp();
|
||||
let width = imp.flow_box.allocation().width();
|
||||
|
||||
let max_cols = imp.flow_box.max_children_per_line() as i32;
|
||||
let min_cols = imp.flow_box.min_children_per_line() as i32;
|
||||
let fallback = max_cols.clamp(min_cols, tile_count.max(1));
|
||||
|
||||
if width <= 0 {
|
||||
return fallback.max(1);
|
||||
}
|
||||
|
||||
// Tile width is 160 and column spacing is 24.
|
||||
let estimated = width / (160 + 24);
|
||||
estimated
|
||||
.clamp(min_cols.max(1), max_cols.max(1))
|
||||
.clamp(1, tile_count.max(1))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LauncherGrid {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
# Controller And Keyboard Launching
|
||||
|
||||
Issue: <https://github.com/aarmea/shepherd-launcher/issues/20>
|
||||
|
||||
Prompt summary:
|
||||
- Launching activities required pointer input.
|
||||
- Requested non-pointer controls:
|
||||
- Selection via arrow keys, WASD, D-pad, or analog stick
|
||||
- Launch via Enter, Space, controller A/B/Start
|
||||
- Exit via Alt+F4, Ctrl+W, controller home
|
||||
- Goal was better accessibility and support for pointer-less handheld systems.
|
||||
|
||||
Implemented summary:
|
||||
- Added keyboard navigation and activation support in launcher UI grid.
|
||||
- Added explicit keyboard exit shortcuts at the window level.
|
||||
- Added gamepad input handling via `gilrs` for D-pad, analog stick, A/B/Start launch, and home exit.
|
||||
- Added focused tile styling so non-pointer selection is visible.
|
||||
|
||||
Key files:
|
||||
- crates/shepherd-launcher-ui/src/app.rs
|
||||
- crates/shepherd-launcher-ui/src/grid.rs
|
||||
- crates/shepherd-launcher-ui/Cargo.toml
|
||||
Loading…
Reference in a new issue