Skip to content

Commit befcd9f

Browse files
committed
feat(launcher): live previews
Hovering an icon will display a live window preview image. Resolves #157
1 parent b1776c9 commit befcd9f

File tree

20 files changed

+1199
-59
lines changed

20 files changed

+1199
-59
lines changed

Cargo.lock

Lines changed: 258 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,9 @@ schema = ["dep:schemars"]
119119

120120
[dependencies]
121121
# core
122-
gtk = { package = "gtk4", version = "0.10.0", features = ["v4_10"] }
122+
gtk = { package = "gtk4", version = "0.10.0", features = ["v4_14"] }
123123
gtk-layer-shell = { package = "gtk4-layer-shell", version = "0.6.3" }
124-
glib = "0.21.1"
124+
glib = "0.21.3"
125125
tokio = { version = "1.47.1", features = [
126126
"macros",
127127
"rt-multi-thread",
@@ -145,9 +145,17 @@ walkdir = "2.5.0"
145145
notify = { version = "8.2.0", default-features = false }
146146
wayland-client = "0.31.1"
147147
wayland-protocols-wlr = { version = "0.3.9", features = ["client"] }
148+
wayland-protocols-hyprland = { version = "1.1.0", features = ["client"] }
148149
smithay-client-toolkit = { version = "0.20.0", default-features = false, features = [
149150
"calloop",
150151
] }
152+
153+
# -- testing --
154+
gbm = "0.18.0"
155+
drm = "0.14.1"
156+
udev = "0.9.3"
157+
# -- end testing --
158+
151159
universal-config = { version = "0.5.1", default-features = false }
152160
ctrlc = "3.5.0"
153161
cfg-if = "1.0.3"
@@ -198,9 +206,10 @@ serde_json = { version = "1.0.145", optional = true } # ipc, niri
198206

199207
# schema
200208
schemars = { version = "1.0.4", optional = true, features = ["indexmap2"] }
209+
log = "0.4.27"
201210

202211
[build-dependencies]
203212
clap = { version = "4.5.48", features = ["derive"] }
204213
clap_complete = "4.5.58"
205214
serde = { version = "1.0.226", features = ["derive"] }
206-
serde_json = "1.0.145"
215+
serde_json = "1.0.145"

docs/modules/Launcher.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ Optionally displays a launchable set of favourites.
1919
| `show_names` | `boolean` | `false` | Whether to show app names on the button label. Names will still show on tooltips when set to false. |
2020
| `show_icons` | `boolean` | `true` | Whether to show app icons on the button. |
2121
| `icon_size` | `integer` | `32` | Size to render icon at (image icons only). |
22-
| `launch_command` | `string` | `gtk-launch {app_name}` | Command used to launch applications. |
22+
| `show_previews` | `boolean` | `true` | Whether to show window previews when hovering icons. |
23+
| `preview_size` | `integer` | `256` | Target width for a window preview. Aspect ratio is preserved. |
24+
| `preview_fps` | `integer` | `60` | Target framerate for window preview updates. 0-999. |
25+
| `launch_command` | `string` | `gtk-launch {app_name}` | Command used to launch applications. |
2326
| `reversed` | `boolean` | `false` | Whether to reverse the order of favorites/items |
2427
| `minimize_focused` | `boolean` | `true` | Whether to minimize a focused window when its icon is clicked. Only minimizes single windows. |
2528
| `truncate.mode` | `'start'` or `'middle'` or `'end'` or `off` | `end` | Location of the ellipses and where to truncate text from. Applies to application names when `show_names` is enabled. |
@@ -31,6 +34,7 @@ Optionally displays a launchable set of favourites.
3134
| `page_size` | `integer` | `1000` | Number of items to show on a page. When the number of items is reached, controls appear which can be used to move forward/back through the list of items. |
3235
| `icons.page_back` | `string` or [image](images) | `󰅁` | Icon to show for page back button. |
3336
| `icons.page_forward` | `string` or [image](images) | `󰅂` | Icon to show for page forward button. |
37+
3438
<details>
3539
<summary>JSON</summary>
3640

src/clients/wayland/dmabuf.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use std::collections::HashMap;
2+
use std::fs::File;
3+
use std::os::fd::{AsFd, BorrowedFd};
4+
use drm::Device;
5+
use drm::control::Device as ControlDevice;
6+
use smithay_client_toolkit::dmabuf::{DmabufFeedback, DmabufHandler, DmabufState};
7+
use smithay_client_toolkit::reexports::protocols::wp::linux_dmabuf::zv1::client::zwp_linux_buffer_params_v1::ZwpLinuxBufferParamsV1;
8+
use smithay_client_toolkit::reexports::protocols::wp::linux_dmabuf::zv1::client::zwp_linux_dmabuf_feedback_v1::ZwpLinuxDmabufFeedbackV1;
9+
use tracing::{error, trace};
10+
use udev::DeviceType;
11+
use wayland_client::{Connection, QueueHandle};
12+
use wayland_client::protocol::wl_buffer::WlBuffer;
13+
use crate::clients::wayland::{Environment};
14+
use color_eyre::eyre::OptionExt;
15+
use color_eyre::Result;
16+
17+
struct Card(File);
18+
19+
#[allow(non_camel_case_types)]
20+
type dev_t = u64;
21+
22+
impl Card {
23+
fn open(device: dev_t) -> Result<Self> {
24+
let device = udev::Device::from_devnum(DeviceType::Character, device)?;
25+
26+
let mut options = std::fs::OpenOptions::new();
27+
options.read(true);
28+
options.write(true);
29+
30+
let file = device
31+
.devnode()
32+
.map(|node| options.open(node))
33+
.ok_or_eyre("Device has no node")??;
34+
35+
Ok(Self(file))
36+
}
37+
}
38+
39+
impl AsFd for Card {
40+
fn as_fd(&self) -> BorrowedFd<'_> {
41+
self.0.as_fd()
42+
}
43+
}
44+
45+
impl Device for Card {}
46+
impl ControlDevice for Card {}
47+
48+
impl DmabufHandler for Environment {
49+
fn dmabuf_state(&mut self) -> &mut DmabufState {
50+
&mut self.dmabuf_state
51+
}
52+
53+
fn dmabuf_feedback(
54+
&mut self,
55+
_conn: &Connection,
56+
_qh: &QueueHandle<Self>,
57+
_proxy: &ZwpLinuxDmabufFeedbackV1,
58+
feedback: DmabufFeedback,
59+
) {
60+
trace!("Got feedback: {feedback:?}");
61+
62+
let format_table = feedback.format_table();
63+
let tranches = feedback.tranches();
64+
65+
let mut formats = HashMap::new();
66+
67+
for tranch in tranches {
68+
for index in &tranch.formats {
69+
let format = format_table[*index as usize];
70+
formats
71+
.entry(format.format)
72+
.or_insert_with(Vec::new)
73+
.push(format.modifier);
74+
}
75+
}
76+
77+
self.dmabuf_formats.extend(formats);
78+
79+
// FIXME: This will probably break on multi-gpu setups
80+
// may need to try each tranche in order
81+
let card = Card::open(feedback.main_device()).unwrap();
82+
83+
let gbm_dev = gbm::Device::new(card.0).unwrap();
84+
self.gbm_device = Some(gbm_dev);
85+
}
86+
87+
fn created(
88+
&mut self,
89+
_conn: &Connection,
90+
_qh: &QueueHandle<Self>,
91+
params: &ZwpLinuxBufferParamsV1,
92+
_buffer: WlBuffer,
93+
) {
94+
trace!("created (async)");
95+
params.destroy();
96+
}
97+
98+
fn failed(
99+
&mut self,
100+
_conn: &Connection,
101+
_qh: &QueueHandle<Self>,
102+
params: &ZwpLinuxBufferParamsV1,
103+
) {
104+
error!("Failed to create DMA-BUF buffer");
105+
params.destroy();
106+
}
107+
108+
fn released(&mut self, _conn: &Connection, _qh: &QueueHandle<Self>, _buffer: &WlBuffer) {
109+
trace!("DMA-BUF handle released");
110+
}
111+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
mod frame;
2+
mod manager;
3+
mod session;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use super::manager::ToplevelManagerState;
2+
use super::{Buffer, BufferRequest};
3+
use crate::clients::wayland::ToplevelHandle;
4+
use crate::lock;
5+
use color_eyre::Result;
6+
use std::sync::{Arc, Mutex};
7+
use tracing::{error, warn};
8+
use wayland_client::{Connection, Dispatch, QueueHandle, WEnum};
9+
use wayland_protocols_hyprland::toplevel_export::v1::client::hyprland_toplevel_export_frame_v1::{
10+
Event, Flags, HyprlandToplevelExportFrameV1,
11+
};
12+
13+
pub trait ToplevelFrameDataExt: Send + Sync {
14+
fn buffer_request(&self) -> BufferRequest;
15+
fn set_buffer_request(&self, buffer: BufferRequest);
16+
17+
fn handle(&self) -> Option<&ToplevelHandle>;
18+
19+
fn copied_first_frame(&self) -> bool;
20+
fn set_copied_first_frame(&self);
21+
}
22+
23+
impl ToplevelFrameDataExt for ToplevelFrameData {
24+
fn buffer_request(&self) -> BufferRequest {
25+
let inner = lock!(self.inner);
26+
inner.buffer_request
27+
}
28+
29+
fn set_buffer_request(&self, request: BufferRequest) {
30+
lock!(self.inner).buffer_request = request;
31+
}
32+
33+
fn handle(&self) -> Option<&ToplevelHandle> {
34+
self.handle.as_ref()
35+
}
36+
37+
fn copied_first_frame(&self) -> bool {
38+
lock!(self.inner).copied_first_frame
39+
}
40+
41+
fn set_copied_first_frame(&self) {
42+
lock!(self.inner).copied_first_frame = true;
43+
}
44+
}
45+
46+
#[derive(Default, Debug)]
47+
pub struct ToplevelFrameData {
48+
pub handle: Option<ToplevelHandle>,
49+
pub inner: Arc<Mutex<ToplevelFrameDataInner>>,
50+
}
51+
52+
impl ToplevelFrameData {}
53+
54+
#[derive(Debug, Default)]
55+
pub struct ToplevelFrameDataInner {
56+
buffer_request: BufferRequest,
57+
copied_first_frame: bool,
58+
}
59+
60+
pub trait ToplevelFrameHandler: Sized {
61+
/// Requests a new DMA-BUF is created for the provided parameters.
62+
fn dma_buffer(&mut self, request: BufferRequest, handle_id: usize) -> Result<Buffer>;
63+
64+
/// Provides the buffer once ready.
65+
/// This includes the copied contents.
66+
fn buffer_ready(&mut self, handle: &ToplevelHandle);
67+
}
68+
69+
impl<D, U> Dispatch<HyprlandToplevelExportFrameV1, U, D> for ToplevelManagerState
70+
where
71+
D: Dispatch<HyprlandToplevelExportFrameV1, U> + ToplevelFrameHandler,
72+
U: ToplevelFrameDataExt,
73+
{
74+
fn event(
75+
state: &mut D,
76+
proxy: &HyprlandToplevelExportFrameV1,
77+
event: Event,
78+
data: &U,
79+
_conn: &Connection,
80+
_qhandle: &QueueHandle<D>,
81+
) {
82+
match event {
83+
Event::LinuxDmabuf {
84+
format,
85+
width,
86+
height,
87+
} => {
88+
data.set_buffer_request(BufferRequest { format, width, height })
89+
},
90+
Event::BufferDone => {
91+
let Some(handle_id) = data.handle().and_then(|h| h.info()).map(|i| i.id) else {
92+
error!("Missing handle");
93+
return;
94+
};
95+
96+
match state.dma_buffer(data.buffer_request(), handle_id) {
97+
Ok(buffer) => {
98+
proxy.copy(&buffer.wl_buffer, !data.copied_first_frame() as i32);
99+
},
100+
Err(err) => { error!("failed to fetch buffer: {err:?}"); proxy.destroy() },
101+
}
102+
}
103+
Event::Flags { flags } => match flags {
104+
WEnum::Value(flags) => {
105+
if flags.contains(Flags::YInvert) {
106+
error!("Received unhandled YInvert transform flag");
107+
}
108+
}
109+
WEnum::Unknown(_) => {
110+
error!("Received unknown flags for toplevel frame");
111+
}
112+
},
113+
Event::Ready { .. } => {
114+
let handle = data.handle().unwrap();
115+
state.buffer_ready(handle);
116+
data.set_copied_first_frame();
117+
118+
proxy.destroy();
119+
}
120+
Event::Failed => {
121+
error!("Failed to capture frame");
122+
proxy.destroy();
123+
}
124+
Event::Buffer { .. /* shm ignored in favour of dmabuf */ } | Event::Damage { .. } => {}
125+
_ => warn!("Received unhandled toplevel frame event: {:?}", event),
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)