Compare commits

...

1 commit

Author SHA1 Message Date
Albert Armea
4dc5842949 Add keyboard navigation for launcher grid 2026-02-07 18:16:00 -05:00
4 changed files with 106 additions and 0 deletions

View file

@ -1,6 +1,7 @@
//! Main GTK4 application for the launcher //! Main GTK4 application for the launcher
use gtk4::glib; use gtk4::glib;
use gtk4::gdk;
use gtk4::prelude::*; use gtk4::prelude::*;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
@ -41,6 +42,14 @@ window {
border-color: #4a90d9; border-color: #4a90d9;
} }
.launcher-tile:focus,
.launcher-tile:focus-visible {
background: #1f3460;
background-color: #1f3460;
border-color: #f8c24e;
outline: none;
}
.launcher-tile:active { .launcher-tile:active {
background: #0f3460; background: #0f3460;
background-color: #0f3460; background-color: #0f3460;
@ -156,6 +165,61 @@ impl LauncherApp {
window.set_child(Some(&stack)); window.set_child(Some(&stack));
let grid_for_keys = grid.downgrade();
let window_for_keys = window.downgrade();
let key_controller = gtk4::EventControllerKey::new();
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
let Some(window) = window_for_keys.upgrade() else {
return glib::Propagation::Proceed;
};
let Some(grid) = grid_for_keys.upgrade() else {
return glib::Propagation::Proceed;
};
let is_alt = modifiers.contains(gdk::ModifierType::ALT_MASK);
let is_ctrl = modifiers.contains(gdk::ModifierType::CONTROL_MASK);
if (key == gdk::Key::w || key == gdk::Key::W) && is_ctrl {
window.close();
return glib::Propagation::Stop;
}
let handled = match key {
gdk::Key::Up | gdk::Key::KP_Up | gdk::Key::w | gdk::Key::W => {
grid.move_focus(gtk4::DirectionType::Up);
true
}
gdk::Key::Down | gdk::Key::KP_Down | gdk::Key::s | gdk::Key::S => {
grid.move_focus(gtk4::DirectionType::Down);
true
}
gdk::Key::Left | gdk::Key::KP_Left | gdk::Key::a | gdk::Key::A => {
grid.move_focus(gtk4::DirectionType::Left);
true
}
gdk::Key::Right | gdk::Key::KP_Right | gdk::Key::d | gdk::Key::D => {
grid.move_focus(gtk4::DirectionType::Right);
true
}
gdk::Key::Home | gdk::Key::KP_Home => {
window.close();
true
}
gdk::Key::F4 if is_alt => {
window.close();
true
}
_ => false,
};
if handled {
glib::Propagation::Stop
} else {
glib::Propagation::Proceed
}
});
window.add_controller(key_controller);
// Create shared state // Create shared state
let state = SharedState::new(); let state = SharedState::new();
let state_receiver = state.subscribe(); let state_receiver = state.subscribe();

View file

@ -60,6 +60,7 @@ mod imp {
self.flow_box.set_valign(gtk4::Align::Center); self.flow_box.set_valign(gtk4::Align::Center);
self.flow_box.set_hexpand(true); self.flow_box.set_hexpand(true);
self.flow_box.set_vexpand(true); self.flow_box.set_vexpand(true);
self.flow_box.set_can_focus(true);
self.flow_box.add_css_class("launcher-grid"); self.flow_box.add_css_class("launcher-grid");
// Wrap in a scrolled window // Wrap in a scrolled window
@ -125,6 +126,8 @@ impl LauncherGrid {
imp.flow_box.insert(&tile, -1); imp.flow_box.insert(&tile, -1);
imp.tiles.borrow_mut().push(tile); imp.tiles.borrow_mut().push(tile);
} }
self.focus_first_tile();
} }
/// Enable or disable all tiles /// Enable or disable all tiles
@ -133,6 +136,31 @@ impl LauncherGrid {
tile.set_sensitive(sensitive); tile.set_sensitive(sensitive);
} }
} }
pub fn move_focus(&self, direction: gtk4::DirectionType) {
self.ensure_focus();
self.imp().flow_box.child_focus(direction);
}
fn ensure_focus(&self) {
if self
.imp()
.tiles
.borrow()
.iter()
.any(|tile| tile.has_focus())
{
return;
}
self.focus_first_tile();
}
fn focus_first_tile(&self) {
if let Some(tile) = self.imp().tiles.borrow().first() {
tile.grab_focus();
}
}
} }
impl Default for LauncherGrid { impl Default for LauncherGrid {

View file

@ -51,6 +51,7 @@ mod imp {
obj.add_css_class("launcher-tile"); obj.add_css_class("launcher-tile");
obj.add_css_class("flat"); obj.add_css_class("flat");
obj.set_size_request(160, 160); obj.set_size_request(160, 160);
obj.set_can_focus(true);
} }
} }

View file

@ -0,0 +1,13 @@
# Launcher Keyboard & Controller Navigation
Issue: <https://github.com/aarmea/shepherd-launcher/issues/20>
Summary:
- Added keyboard/controller navigation for the launcher grid (arrow keys, WASD, D-pad) with focus movement between tiles.
- Enabled focus styling on tiles and ensured the grid focuses the first tile on state updates.
- Added keyboard shortcuts to close the launcher (Alt+F4, Ctrl+W, Home).
Key files:
- crates/shepherd-launcher-ui/src/app.rs
- crates/shepherd-launcher-ui/src/grid.rs
- crates/shepherd-launcher-ui/src/tile.rs