Add native shell launcher with themed search and browsing
This commit is contained in:
1 parent
2a400b26d0
commit
d8bdb68b2a
43 files changed
+3351
-115
No files matched your search
@@ -184,6 +184,7 @@ impl DirectBackendState {
|
||||
window: &smithay::desktop::Window,
|
||||
output: &Output,
|
||||
cursor: Option<smithay::utils::Rectangle<i32, smithay::utils::Logical>>,
|
||||
scale: f64,
|
||||
) -> Result<crate::handlers::screenshot::CaptureBuffer, String> {
|
||||
let device = self
|
||||
.devices
|
||||
@@ -194,7 +195,7 @@ impl DirectBackendState {
|
||||
&mut device.renderer,
|
||||
window,
|
||||
window.geometry(),
|
||||
output.current_scale().fractional_scale(),
|
||||
scale,
|
||||
cursor.map(|rect| (state, rect)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1214,7 +1214,7 @@ mod tests {
|
||||
let input = config.input_settings().unwrap();
|
||||
let bindings = config.bindings(&input).unwrap();
|
||||
|
||||
assert_eq!(bindings.len(), 62);
|
||||
assert_eq!(bindings.len(), 63);
|
||||
for (shift, action) in [
|
||||
(false, BindingAction::ToggleMaximized),
|
||||
(true, BindingAction::ToggleFullscreen),
|
||||
@@ -1225,6 +1225,10 @@ mod tests {
|
||||
&& binding.action == action));
|
||||
}
|
||||
assert!(bindings.iter().any(|binding| binding.action == BindingAction::Exit));
|
||||
assert!(bindings.iter().any(|binding| binding.modifiers.logo
|
||||
&& !binding.modifiers.shift
|
||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_space)
|
||||
&& binding.action == BindingAction::ToggleLauncher));
|
||||
assert!(bindings.iter().any(|binding| binding.modifiers.logo
|
||||
&& binding.modifiers.shift
|
||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_t)
|
||||
@@ -1262,7 +1266,7 @@ mod tests {
|
||||
|
||||
let input = replaced.input_settings().unwrap();
|
||||
let bindings = replaced.bindings(&input).unwrap();
|
||||
assert_eq!(bindings.len(), 62);
|
||||
assert_eq!(bindings.len(), 63);
|
||||
assert!(bindings.iter().any(|binding| {
|
||||
binding.action
|
||||
== BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()].into())
|
||||
@@ -1270,7 +1274,7 @@ mod tests {
|
||||
|
||||
let input = unbound.input_settings().unwrap();
|
||||
let bindings = unbound.bindings(&input).unwrap();
|
||||
assert_eq!(bindings.len(), 61);
|
||||
assert_eq!(bindings.len(), 62);
|
||||
assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close));
|
||||
}
|
||||
|
||||
|
||||
@@ -378,6 +378,10 @@ pub(super) fn parse_action(
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleDisplayMode)
|
||||
}
|
||||
"toggle-launcher" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleLauncher)
|
||||
}
|
||||
"toggle-keybinding-guide" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleKeybindingGuide)
|
||||
@@ -405,6 +409,7 @@ pub(super) fn parse_workspace(argument: &str) -> Result<u8, ConfigError> {
|
||||
|
||||
pub(super) fn default_bindings() -> Vec<BindingConfig> {
|
||||
let mut bindings = vec![
|
||||
binding("Super+Space", "toggle-launcher", None),
|
||||
binding("Super+Enter", "spawn", Some("terminal")),
|
||||
binding("Super+Shift+S", "spawn", Some("screenshot")),
|
||||
binding("Print", "spawn", Some("screenshot-full")),
|
||||
@@ -524,6 +529,7 @@ pub enum BindingAction {
|
||||
ToggleFloating,
|
||||
ToggleOverview,
|
||||
ToggleKeybindingGuide,
|
||||
ToggleLauncher,
|
||||
ToggleDisplayMode,
|
||||
Media(&'static str),
|
||||
}
|
||||
@@ -648,6 +654,7 @@ impl Binding {
|
||||
.into(),
|
||||
BindingAction::ToggleOverview => "Open or close overview".into(),
|
||||
BindingAction::ToggleDisplayMode => "Open display mode chooser".into(),
|
||||
BindingAction::ToggleLauncher => "Open or close launcher".into(),
|
||||
BindingAction::ToggleKeybindingGuide => "Open or close shortcut hint".into(),
|
||||
};
|
||||
let mut parts = Vec::new();
|
||||
|
||||
@@ -330,7 +330,7 @@ impl Ferese {
|
||||
)
|
||||
} else {
|
||||
let mut backend = self.direct_backend.take()?;
|
||||
let result = backend.capture_window_frame(self, &window, &output, cursor);
|
||||
let result = backend.capture_window_frame(self, &window, &output, cursor, scale);
|
||||
self.direct_backend = Some(backend);
|
||||
result
|
||||
};
|
||||
|
||||
@@ -1267,6 +1267,9 @@ impl Ferese {
|
||||
BindingAction::ToggleDisplayMode => {
|
||||
self.toggle_display_mode();
|
||||
}
|
||||
BindingAction::ToggleLauncher => {
|
||||
self.toggle_launcher();
|
||||
}
|
||||
BindingAction::ToggleKeybindingGuide => {
|
||||
self.toggle_keybinding_guide();
|
||||
}
|
||||
|
||||
@@ -1083,6 +1083,14 @@ impl Ferese {
|
||||
"toggle-display-mode" => {
|
||||
self.toggle_display_mode();
|
||||
}
|
||||
"toggle-launcher" => {
|
||||
if !self.toggle_launcher() {
|
||||
return Err(CommandError::new(
|
||||
"unavailable",
|
||||
"Launcher requires an unlocked session and an updated Ferese shell",
|
||||
));
|
||||
}
|
||||
}
|
||||
"toggle-keybinding-guide" => {
|
||||
if !self.toggle_keybinding_guide() {
|
||||
return Err(CommandError::new(
|
||||
|
||||
@@ -427,6 +427,7 @@ impl SessionLockHandler for Ferese {
|
||||
}
|
||||
|
||||
fn lock(&mut self, confirmation: SessionLocker) {
|
||||
self.dismiss_launcher();
|
||||
if !self.session_lock.lifecycle.can_acquire() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::private_client::ClientCapabilities;
|
||||
use crate::state::ClientState;
|
||||
|
||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||
display.create_global::<Ferese, FereseShellManagerV1, _>(9, ());
|
||||
display.create_global::<Ferese, FereseShellManagerV1, _>(10, ());
|
||||
}
|
||||
|
||||
pub(crate) fn config_chunks(source: &str) -> Vec<&str> {
|
||||
@@ -98,6 +98,18 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
return;
|
||||
}
|
||||
match request {
|
||||
ferese_shell_v1::Request::Thumbnail {
|
||||
token,
|
||||
window_hi,
|
||||
window_lo,
|
||||
} => {
|
||||
if let Some((width, height, file)) = state.shell_thumbnail(join_id(window_hi, window_lo)) {
|
||||
use std::os::fd::AsFd;
|
||||
shell.thumbnail(token, window_hi, window_lo, width, height, file.as_fd());
|
||||
} else {
|
||||
shell.thumbnail_unavailable(token, window_hi, window_lo);
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::ActivateWindow { window_hi, window_lo } => state.handle_shell_window_request(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::ActivateWindow,
|
||||
@@ -246,6 +258,22 @@ impl Ferese {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_launcher(&mut self) -> bool {
|
||||
if self.session_lock.active() {
|
||||
return false;
|
||||
}
|
||||
let Some(shell) = self
|
||||
.shell_resources
|
||||
.iter()
|
||||
.filter_map(|shell| shell.upgrade().ok())
|
||||
.find(|shell| shell.version() >= 10)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
shell.toggle_launcher(self.focused_output().map_or_else(String::new, |output| output.name()));
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_keybinding_guide(&mut self) -> bool {
|
||||
if self.session_lock.active() {
|
||||
return false;
|
||||
@@ -888,3 +916,76 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
pub(crate) fn dismiss_launcher(&self) {
|
||||
for shell in self
|
||||
.shell_resources
|
||||
.iter()
|
||||
.filter_map(|s| s.upgrade().ok())
|
||||
.filter(|s| s.version() >= 10)
|
||||
{
|
||||
shell.dismiss_launcher();
|
||||
}
|
||||
}
|
||||
|
||||
fn shell_thumbnail(&mut self, id: WindowId) -> Option<(u32, u32, std::fs::File)> {
|
||||
use std::io::{Seek, Write};
|
||||
if self.session_lock.active() {
|
||||
return None;
|
||||
}
|
||||
let window = self
|
||||
.windows
|
||||
.ids()
|
||||
.iter()
|
||||
.find(|(_, candidate)| **candidate == id)?
|
||||
.0
|
||||
.clone();
|
||||
if self.capture_protected(&window) {
|
||||
return None;
|
||||
}
|
||||
let workspace = self.workspaces.workspace_for_window(id)?;
|
||||
let owner = self.output_workspaces.output_for_workspace(workspace)?;
|
||||
let output = self
|
||||
.output_ids
|
||||
.iter()
|
||||
.find(|(_, candidate)| **candidate == owner)?
|
||||
.0
|
||||
.clone();
|
||||
let geometry = window.geometry();
|
||||
if geometry.size.w <= 0 || geometry.size.h <= 0 {
|
||||
return None;
|
||||
}
|
||||
let scale = (320. / f64::from(geometry.size.w))
|
||||
.min(168. / f64::from(geometry.size.h))
|
||||
.min(1.);
|
||||
let result = if let Some(backend) = self.nested_backend.clone() {
|
||||
let mut backend = backend.try_borrow_mut().ok()?;
|
||||
crate::render::capture_window_frame(backend.renderer(), &window, geometry, scale, None)
|
||||
} else {
|
||||
let mut backend = self.direct_backend.take()?;
|
||||
let result = backend.capture_window_frame(self, &window, &output, None, scale);
|
||||
self.direct_backend = Some(backend);
|
||||
result
|
||||
};
|
||||
let mut buffer = result.ok()?;
|
||||
if buffer.width <= 0 || buffer.width > 320 || buffer.height <= 0 || buffer.height > 168 {
|
||||
return None;
|
||||
}
|
||||
// GL ABGR readback is RGBA bytes on the supported little-endian targets.
|
||||
// Iced expects straight rather than premultiplied channels.
|
||||
for pixel in buffer.pixels.chunks_exact_mut(4) {
|
||||
let alpha = u16::from(pixel[3]);
|
||||
for channel in &mut pixel[..3] {
|
||||
*channel = (u16::from(*channel) * 255 + alpha / 2)
|
||||
.checked_div(alpha)
|
||||
.unwrap_or(0)
|
||||
.min(255) as u8;
|
||||
}
|
||||
}
|
||||
let mut file = tempfile::tempfile().ok()?;
|
||||
file.write_all(&buffer.pixels).ok()?;
|
||||
file.rewind().ok()?;
|
||||
Some((buffer.width as u32, buffer.height as u32, file))
|
||||
}
|
||||
}
|
||||
@@ -647,9 +647,6 @@ impl Ferese {
|
||||
let Some(output_id) = self.output_workspaces.output_for_workspace(destination) else {
|
||||
return false;
|
||||
};
|
||||
if self.output_workspaces.output_for_workspace(source) != Some(output_id) {
|
||||
return false;
|
||||
}
|
||||
if source == destination {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1178,3 +1178,42 @@ fn tab_indicator_yields_to_a_floating_window_and_disappears_with_one_tab() {
|
||||
assert!(state.scrolling_tab_bar(a).is_none());
|
||||
assert!(state.tab_indicator.hover.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_can_move_a_window_to_the_current_workspace_on_another_output() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"state::window_lifecycle_tests::shell_can_move_a_window_to_the_current_workspace_on_another_output",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut events, mut state, _) = fixture();
|
||||
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
|
||||
let id = state.windows.ids()[&window];
|
||||
let source = state.workspaces.workspace_for_window(id).unwrap();
|
||||
let second = Output::new(
|
||||
"second".into(),
|
||||
smithay::output::PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: smithay::output::Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
second.change_current_state(
|
||||
Some(smithay::output::Mode {
|
||||
size: (800, 600).into(),
|
||||
refresh: 60_000,
|
||||
}),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
state.space.map_output(&second, (800, 0));
|
||||
state.register_output(&second, "second".into());
|
||||
let output = state.output_ids[&second];
|
||||
let destination = state.output_workspaces.active_workspace(output).unwrap();
|
||||
assert_ne!(source, destination);
|
||||
assert!(state.move_managed_window_to_workspace(id, destination));
|
||||
assert_eq!(state.workspaces.workspace_for_window(id), Some(destination));
|
||||
assert!(state.move_managed_window_to_workspace(id, destination));
|
||||
}
|
||||
Reference in new issue
Block a user