feat: window picker
This commit is contained in:
1 parent
c6736b0ac3
commit
7fc477b902
7 files changed
+530
-20
No files matched your search
+32
-13
@@ -13,8 +13,6 @@ mod lock;
|
|||||||
mod log;
|
mod log;
|
||||||
mod niri;
|
mod niri;
|
||||||
mod output;
|
mod output;
|
||||||
// The picker lands in pieces; Task 6 of the theme plan removes this.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
mod picker;
|
mod picker;
|
||||||
mod region;
|
mod region;
|
||||||
mod render;
|
mod render;
|
||||||
@@ -67,8 +65,11 @@ enum Command {
|
|||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
common: Common,
|
common: Common,
|
||||||
},
|
},
|
||||||
/// Click a window to capture it (Cmd+Shift+4, then Space).
|
/// Pick a window from cards over the paint shader (Alt+Tab-like).
|
||||||
Window {
|
Window {
|
||||||
|
/// Click the window on screen instead (niri's pick).
|
||||||
|
#[arg(long)]
|
||||||
|
pick: bool,
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
common: Common,
|
common: Common,
|
||||||
},
|
},
|
||||||
@@ -159,9 +160,14 @@ fn parse_delay(s: &str) -> Result<Duration, String> {
|
|||||||
type Shots = (Vec<(RgbaImage, Option<String>)>, usize, String);
|
type Shots = (Vec<(RgbaImage, Option<String>)>, usize, String);
|
||||||
|
|
||||||
enum Mode {
|
enum Mode {
|
||||||
Screen { all: bool },
|
Screen {
|
||||||
|
all: bool,
|
||||||
|
},
|
||||||
Region,
|
Region,
|
||||||
Window,
|
/// `pick`: niri's click-a-window instead of valw's picker.
|
||||||
|
Window {
|
||||||
|
pick: bool,
|
||||||
|
},
|
||||||
Zoom,
|
Zoom,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +212,7 @@ fn run(cli: Cli) -> Result<()> {
|
|||||||
Command::PreviewHost => host::run(),
|
Command::PreviewHost => host::run(),
|
||||||
Command::Screen { all, common } => capture(Mode::Screen { all }, common),
|
Command::Screen { all, common } => capture(Mode::Screen { all }, common),
|
||||||
Command::Region { common } => capture(Mode::Region, common),
|
Command::Region { common } => capture(Mode::Region, common),
|
||||||
Command::Window { common } => capture(Mode::Window, common),
|
Command::Window { pick, common } => capture(Mode::Window { pick }, common),
|
||||||
Command::Zoom { common } => capture(Mode::Zoom, common),
|
Command::Zoom { common } => capture(Mode::Zoom, common),
|
||||||
Command::Edit { file } => editor::run(&file),
|
Command::Edit { file } => editor::run(&file),
|
||||||
// Handled in main, before logging starts.
|
// Handled in main, before logging starts.
|
||||||
@@ -272,13 +278,18 @@ fn capture(mode: Mode, common: Common) -> Result<()> {
|
|||||||
region::Choice::Window => {
|
region::Choice::Window => {
|
||||||
drop(frames);
|
drop(frames);
|
||||||
windowed = true;
|
windowed = true;
|
||||||
window_shot(&outputs, cursor)?
|
window_shot(&outputs, cursor, None)?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Mode::Window => {
|
Mode::Window { pick } => {
|
||||||
windowed = true;
|
windowed = true;
|
||||||
window_shot(&outputs, cursor)?
|
let chosen = if pick {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(picker::run(&mut wl, &outputs[focused_output(&outputs)])?)
|
||||||
|
};
|
||||||
|
window_shot(&outputs, cursor, chosen)?
|
||||||
}
|
}
|
||||||
Mode::Zoom => {
|
Mode::Zoom => {
|
||||||
let focused = focused_output(&outputs);
|
let focused = focused_output(&outputs);
|
||||||
@@ -327,8 +338,15 @@ fn capture(mode: Mode, common: Common) -> Result<()> {
|
|||||||
|
|
||||||
/// A window shot, shaped like the other modes' results. The preview goes to
|
/// A window shot, shaped like the other modes' results. The preview goes to
|
||||||
/// the window's output, or niri's focused one if that is unknown.
|
/// the window's output, or niri's focused one if that is unknown.
|
||||||
fn window_shot(outputs: &[wayland::Output], cursor: bool) -> Result<Shots> {
|
fn window_shot(
|
||||||
let (image, output) = window::capture(cursor)?;
|
outputs: &[wayland::Output],
|
||||||
|
cursor: bool,
|
||||||
|
chosen: Option<niri_ipc::Window>,
|
||||||
|
) -> Result<Shots> {
|
||||||
|
let (image, output) = match chosen {
|
||||||
|
Some(w) => window::capture_window(&w, cursor)?,
|
||||||
|
None => window::capture(cursor)?,
|
||||||
|
};
|
||||||
let source = output.unwrap_or_else(|| outputs[focused_output(outputs)].geom.name.clone());
|
let source = output.unwrap_or_else(|| outputs[focused_output(outputs)].geom.name.clone());
|
||||||
Ok((vec![(image, None)], 0, source))
|
Ok((vec![(image, None)], 0, source))
|
||||||
}
|
}
|
||||||
@@ -337,7 +355,7 @@ fn window_shot(outputs: &[wayland::Output], cursor: bool) -> Result<Shots> {
|
|||||||
fn from_toolbar(pick: toolbar::Picked) -> (Mode, Common) {
|
fn from_toolbar(pick: toolbar::Picked) -> (Mode, Common) {
|
||||||
let mode = match pick.mode {
|
let mode = match pick.mode {
|
||||||
toolbar::state::Mode::Screen => Mode::Screen { all: false },
|
toolbar::state::Mode::Screen => Mode::Screen { all: false },
|
||||||
toolbar::state::Mode::Window => Mode::Window,
|
toolbar::state::Mode::Window => Mode::Window { pick: false },
|
||||||
toolbar::state::Mode::Region => Mode::Region,
|
toolbar::state::Mode::Region => Mode::Region,
|
||||||
toolbar::state::Mode::Zoom => Mode::Zoom,
|
toolbar::state::Mode::Zoom => Mode::Zoom,
|
||||||
};
|
};
|
||||||
@@ -452,6 +470,7 @@ mod tests {
|
|||||||
assert!(parses(&["__clipboard"]));
|
assert!(parses(&["__clipboard"]));
|
||||||
assert!(parses(&["toolbar"]));
|
assert!(parses(&["toolbar"]));
|
||||||
assert!(parses(&["backdrop"]));
|
assert!(parses(&["backdrop"]));
|
||||||
|
assert!(parses(&["window", "--pick"]));
|
||||||
assert!(parses(&["__combo-demo"]));
|
assert!(parses(&["__combo-demo"]));
|
||||||
assert!(parses(&["__check-config", "c.toml"]));
|
assert!(parses(&["__check-config", "c.toml"]));
|
||||||
assert!(!parses(&["__check-config"]));
|
assert!(!parses(&["__check-config"]));
|
||||||
@@ -474,7 +493,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
from_toolbar(pick(T::Window, false, true)).0,
|
from_toolbar(pick(T::Window, false, true)).0,
|
||||||
Mode::Window
|
Mode::Window { pick: false }
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
from_toolbar(pick(T::Region, false, true)).0,
|
from_toolbar(pick(T::Region, false, true)).0,
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ pub fn is_our_capture(event: &Event, path: &Path) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Every window niri has.
|
/// Every window niri has.
|
||||||
#[allow(dead_code)] // the picker (theme plan Task 6) removes this
|
|
||||||
pub fn windows() -> Result<Vec<Window>> {
|
pub fn windows() -> Result<Vec<Window>> {
|
||||||
match request(Request::Windows)? {
|
match request(Request::Windows)? {
|
||||||
Response::Windows(windows) => Ok(windows),
|
Response::Windows(windows) => Ok(windows),
|
||||||
@@ -118,7 +117,6 @@ pub fn windows() -> Result<Vec<Window>> {
|
|||||||
|
|
||||||
/// `windows` most recently used first: the focused one, then by focus
|
/// `windows` most recently used first: the focused one, then by focus
|
||||||
/// time, then those never focused, by id.
|
/// time, then those never focused, by id.
|
||||||
#[allow(dead_code)] // the picker (theme plan Task 6) removes this
|
|
||||||
pub fn mru(mut windows: Vec<Window>) -> Vec<Window> {
|
pub fn mru(mut windows: Vec<Window>) -> Vec<Window> {
|
||||||
windows.sort_by_key(|w| {
|
windows.sort_by_key(|w| {
|
||||||
let at = w.focus_timestamp.map(|t| (t.secs, t.nanos));
|
let at = w.focus_timestamp.map(|t| (t.secs, t.nanos));
|
||||||
|
|||||||
@@ -1,5 +1,471 @@
|
|||||||
//! `valw window`: an Alt+Tab-style window picker over the paint shader.
|
//! `valw window`: an Alt+Tab-style window picker over the paint shader.
|
||||||
|
//! Cards for every window (most recently used first); the shader takes the
|
||||||
|
//! selected window's icon colours; Enter or a click captures that window.
|
||||||
|
|
||||||
pub mod icons;
|
pub mod icons;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use image::RgbaImage;
|
||||||
|
use niri_ipc::Window;
|
||||||
|
use smithay_client_toolkit::{
|
||||||
|
compositor::FrameCallbackData,
|
||||||
|
seat::{
|
||||||
|
keyboard::{Keysym, Modifiers},
|
||||||
|
pointer::{BTN_LEFT, PointerEvent, PointerEventKind},
|
||||||
|
},
|
||||||
|
shell::{
|
||||||
|
WaylandSurface,
|
||||||
|
wlr_layer::{Anchor, KeyboardInteractivity, Layer, LayerSurface},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use tiny_skia::{FillRule, Paint as SkPaint, Pixmap, PixmapPaint, Transform};
|
||||||
|
use wayland_client::{Connection, QueueHandle, protocol::wl_surface::WlSurface};
|
||||||
|
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::{
|
||||||
|
Shape, WpCursorShapeDeviceV1,
|
||||||
|
};
|
||||||
|
use wayland_protocols::wp::viewporter::client::wp_viewport::WpViewport;
|
||||||
|
|
||||||
|
use self::layout::Rect;
|
||||||
|
use crate::editor::text;
|
||||||
|
use crate::error::{Cancelled, HintExt};
|
||||||
|
use crate::theme::Palette;
|
||||||
|
use crate::theme::gl::{Frame, Paint};
|
||||||
|
use crate::wayland::{Output, State, Wayland};
|
||||||
|
|
||||||
|
pub const NAMESPACE: &str = "valw-picker";
|
||||||
|
const ICON: f32 = 96.0;
|
||||||
|
const SPEED: f32 = 0.6;
|
||||||
|
const EASE: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
|
/// One window as a card.
|
||||||
|
struct Card {
|
||||||
|
window: Window,
|
||||||
|
icon: Option<Pixmap>,
|
||||||
|
palette: Palette,
|
||||||
|
title: String,
|
||||||
|
app: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Picker {
|
||||||
|
// Dropped first: the EGL surface goes before the wl_surface under it.
|
||||||
|
paint: Option<Paint>,
|
||||||
|
layer: LayerSurface,
|
||||||
|
viewport: WpViewport,
|
||||||
|
scale: f64,
|
||||||
|
/// Logical size once configured.
|
||||||
|
size: Option<(u32, u32)>,
|
||||||
|
cards: Vec<Card>,
|
||||||
|
selected: usize,
|
||||||
|
motion: f32,
|
||||||
|
start: Instant,
|
||||||
|
from: Palette,
|
||||||
|
changed: Instant,
|
||||||
|
overlay_dirty: bool,
|
||||||
|
shift: bool,
|
||||||
|
outcome: Option<Option<usize>>,
|
||||||
|
error: Option<anyhow::Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the picker on `output` and returns the chosen window.
|
||||||
|
pub fn run(wl: &mut Wayland, output: &Output) -> Result<Window> {
|
||||||
|
let windows = crate::niri::mru(crate::niri::windows()?);
|
||||||
|
anyhow::ensure!(!windows.is_empty(), "there are no windows to capture");
|
||||||
|
let px = (ICON as f64 * output.scale).round() as u32;
|
||||||
|
let dirs = icons::Dirs::from_env();
|
||||||
|
let cards: Vec<Card> = windows
|
||||||
|
.into_iter()
|
||||||
|
.map(|window| {
|
||||||
|
let app_id = window.app_id.clone().unwrap_or_default();
|
||||||
|
let image = icons::find(&app_id, &dirs).and_then(|p| icons::load(&p, px));
|
||||||
|
let palette = image
|
||||||
|
.as_ref()
|
||||||
|
.map(|i| crate::theme::from_image(i, true))
|
||||||
|
.unwrap_or(Palette::DEFAULT);
|
||||||
|
Card {
|
||||||
|
title: window.title.clone().unwrap_or_default(),
|
||||||
|
app: app_id,
|
||||||
|
icon: image.as_ref().and_then(to_pixmap),
|
||||||
|
palette,
|
||||||
|
window,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let qh = wl.queue.handle();
|
||||||
|
let s = &wl.state;
|
||||||
|
let layer_shell = s
|
||||||
|
.layer_shell
|
||||||
|
.as_ref()
|
||||||
|
.context("compositor does not support wlr-layer-shell")
|
||||||
|
.hint("run `valw doctor` to see what the compositor supports")?;
|
||||||
|
let viewporter = s
|
||||||
|
.viewporter
|
||||||
|
.as_ref()
|
||||||
|
.context("compositor does not support wp-viewporter")
|
||||||
|
.hint("run `valw doctor` to see what the compositor supports")?;
|
||||||
|
let surface = s.compositor.create_surface(&qh);
|
||||||
|
let layer = layer_shell.create_layer_surface(
|
||||||
|
&qh,
|
||||||
|
surface,
|
||||||
|
Layer::Overlay,
|
||||||
|
Some(NAMESPACE),
|
||||||
|
Some(&output.wl),
|
||||||
|
);
|
||||||
|
layer.set_anchor(Anchor::all());
|
||||||
|
layer.set_exclusive_zone(-1);
|
||||||
|
layer.set_keyboard_interactivity(KeyboardInteractivity::Exclusive);
|
||||||
|
layer.commit();
|
||||||
|
let viewport = viewporter.get_viewport(layer.wl_surface(), &qh, ());
|
||||||
|
let motion = state::next_motion(&state::default_path());
|
||||||
|
let now = Instant::now();
|
||||||
|
let first = cards[0].palette;
|
||||||
|
wl.state.picker = Some(Picker {
|
||||||
|
paint: None,
|
||||||
|
layer,
|
||||||
|
viewport,
|
||||||
|
scale: output.scale,
|
||||||
|
size: None,
|
||||||
|
cards,
|
||||||
|
selected: 0,
|
||||||
|
motion,
|
||||||
|
start: now,
|
||||||
|
from: first,
|
||||||
|
changed: now,
|
||||||
|
overlay_dirty: true,
|
||||||
|
shift: false,
|
||||||
|
outcome: None,
|
||||||
|
error: None,
|
||||||
|
});
|
||||||
|
let result = wl.dispatch_blocking(|s| {
|
||||||
|
s.picker
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|p| p.outcome.is_some() || p.error.is_some())
|
||||||
|
});
|
||||||
|
// Keep only the answer; dropping the picker destroys its surface, which
|
||||||
|
// must be gone before anything is captured.
|
||||||
|
let ended = wl.state.picker.take().map(|p| {
|
||||||
|
let chosen = p.outcome.flatten().map(|i| p.cards[i].window.clone());
|
||||||
|
(chosen, p.error)
|
||||||
|
});
|
||||||
|
let _ = wl.conn.flush();
|
||||||
|
let _ = wl.queue.roundtrip(&mut wl.state);
|
||||||
|
result?;
|
||||||
|
match ended {
|
||||||
|
Some((_, Some(e))) => Err(e),
|
||||||
|
Some((Some(window), None)) => Ok(window),
|
||||||
|
_ => Err(Cancelled.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_pixmap(img: &RgbaImage) -> Option<Pixmap> {
|
||||||
|
let mut p = Pixmap::new(img.width(), img.height())?;
|
||||||
|
for (dst, src) in p.pixels_mut().iter_mut().zip(img.pixels()) {
|
||||||
|
let [r, g, b, a] = src.0;
|
||||||
|
*dst = tiny_skia::ColorU8::from_rgba(r, g, b, a).premultiply();
|
||||||
|
}
|
||||||
|
Some(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Picker {
|
||||||
|
fn is(&self, surface: &WlSurface) -> bool {
|
||||||
|
self.layer.wl_surface() == surface
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rects(&self) -> Vec<Rect> {
|
||||||
|
let (w, h) = self.size.unwrap_or((1, 1));
|
||||||
|
layout::cards(self.cards.len(), (w as f32, h as f32), self.selected)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn palette(&self, now: Instant) -> Palette {
|
||||||
|
let to = self.cards[self.selected].palette;
|
||||||
|
self.from
|
||||||
|
.lerp(&to, (now - self.changed).as_secs_f32() / EASE.as_secs_f32())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select(&mut self, i: usize) {
|
||||||
|
if i != self.selected {
|
||||||
|
let now = Instant::now();
|
||||||
|
self.from = self.palette(now);
|
||||||
|
self.changed = now;
|
||||||
|
self.selected = i;
|
||||||
|
self.overlay_dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn configure(
|
||||||
|
&mut self,
|
||||||
|
conn: &Connection,
|
||||||
|
layer: &LayerSurface,
|
||||||
|
(w, h): (u32, u32),
|
||||||
|
qh: &QueueHandle<State>,
|
||||||
|
) {
|
||||||
|
if !self.is(layer.wl_surface()) || w == 0 || h == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.size = Some((w, h));
|
||||||
|
let px = (
|
||||||
|
(w as f64 * self.scale).round() as u32,
|
||||||
|
(h as f64 * self.scale).round() as u32,
|
||||||
|
);
|
||||||
|
self.viewport.set_destination(w as i32, h as i32);
|
||||||
|
match &mut self.paint {
|
||||||
|
Some(paint) => paint.resize(px),
|
||||||
|
None => match Paint::new(conn, self.layer.wl_surface(), px) {
|
||||||
|
Ok(paint) => self.paint = Some(paint),
|
||||||
|
Err(e) => {
|
||||||
|
self.error = Some(e.context("could not start OpenGL ES for the picker"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.overlay_dirty = true;
|
||||||
|
self.draw(qh);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn closed(&mut self, layer: &LayerSurface) {
|
||||||
|
if self.is(layer.wl_surface()) {
|
||||||
|
tracing::info!("picker closed by the compositor");
|
||||||
|
self.outcome.get_or_insert(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn frame_done(&mut self, surface: &WlSurface, qh: &QueueHandle<State>) {
|
||||||
|
if self.is(surface) {
|
||||||
|
self.draw(qh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, qh: &QueueHandle<State>) {
|
||||||
|
let (Some((w, h)), true) = (self.size, self.paint.is_some()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if self.overlay_dirty {
|
||||||
|
let px = (
|
||||||
|
(w as f64 * self.scale).round() as u32,
|
||||||
|
(h as f64 * self.scale).round() as u32,
|
||||||
|
);
|
||||||
|
let overlay = self.overlay(px);
|
||||||
|
if let Some(paint) = &mut self.paint {
|
||||||
|
paint.set_overlay(overlay.as_ref());
|
||||||
|
}
|
||||||
|
self.overlay_dirty = false;
|
||||||
|
}
|
||||||
|
let now = Instant::now();
|
||||||
|
let frame = Frame {
|
||||||
|
time: (now - self.start).as_secs_f32(),
|
||||||
|
motion: self.motion,
|
||||||
|
speed: SPEED,
|
||||||
|
scale: 1.0,
|
||||||
|
palette: self.palette(now),
|
||||||
|
};
|
||||||
|
let surface = self.layer.wl_surface();
|
||||||
|
surface.frame(qh, FrameCallbackData(surface.clone()));
|
||||||
|
if let Some(paint) = &self.paint
|
||||||
|
&& let Err(e) = paint.draw(&frame)
|
||||||
|
{
|
||||||
|
self.error = Some(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cards, drawn at buffer size `px`.
|
||||||
|
fn overlay(&self, px: (u32, u32)) -> Option<Pixmap> {
|
||||||
|
let mut p = Pixmap::new(px.0, px.1)?;
|
||||||
|
let s = self.scale as f32;
|
||||||
|
let highlight = self
|
||||||
|
.palette(Instant::now() + EASE)
|
||||||
|
.highlight
|
||||||
|
.map(|c| (c * 255.0) as u8);
|
||||||
|
for (i, (card, r)) in self.cards.iter().zip(self.rects()).enumerate() {
|
||||||
|
let selected = i == self.selected;
|
||||||
|
let k = r.w / layout::CARD.0;
|
||||||
|
let panel = crate::toolbar::layout::Rect {
|
||||||
|
x: r.x,
|
||||||
|
y: r.y,
|
||||||
|
w: r.w,
|
||||||
|
h: r.h,
|
||||||
|
};
|
||||||
|
let border = if selected {
|
||||||
|
[highlight[0], highlight[1], highlight[2], 255]
|
||||||
|
} else {
|
||||||
|
[255, 255, 255, 40]
|
||||||
|
};
|
||||||
|
let bw = if selected { 2.0 } else { 1.0 };
|
||||||
|
crate::toolbar::draw::rounded(&mut p, panel, 18.0 * k, border, s);
|
||||||
|
let inner = crate::toolbar::layout::Rect {
|
||||||
|
x: r.x + bw,
|
||||||
|
y: r.y + bw,
|
||||||
|
w: r.w - 2.0 * bw,
|
||||||
|
h: r.h - 2.0 * bw,
|
||||||
|
};
|
||||||
|
crate::toolbar::draw::rounded(
|
||||||
|
&mut p,
|
||||||
|
inner,
|
||||||
|
18.0 * k - bw,
|
||||||
|
[0, 0, 0, if selected { 140 } else { 90 }],
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
// The icon, centred near the top.
|
||||||
|
let icon = ICON * k;
|
||||||
|
let (ix, iy) = (r.x + (r.w - icon) / 2.0, r.y + 20.0 * k);
|
||||||
|
match &card.icon {
|
||||||
|
Some(img) => {
|
||||||
|
let t = Transform::from_scale(
|
||||||
|
icon * s / img.width() as f32,
|
||||||
|
icon * s / img.height() as f32,
|
||||||
|
)
|
||||||
|
.post_translate(ix * s, iy * s);
|
||||||
|
p.draw_pixmap(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
img.as_ref(),
|
||||||
|
&PixmapPaint {
|
||||||
|
quality: tiny_skia::FilterQuality::Bilinear,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
t,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let tile = crate::toolbar::layout::Rect {
|
||||||
|
x: ix,
|
||||||
|
y: iy,
|
||||||
|
w: icon,
|
||||||
|
h: icon,
|
||||||
|
};
|
||||||
|
let c = card.palette.body.map(|v| (v * 255.0) as u8);
|
||||||
|
crate::toolbar::draw::rounded(
|
||||||
|
&mut p,
|
||||||
|
tile,
|
||||||
|
20.0 * k,
|
||||||
|
[c[0], c[1], c[2], 255],
|
||||||
|
s,
|
||||||
|
);
|
||||||
|
let initial: String = card
|
||||||
|
.app
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.map(|c| c.to_uppercase().collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
centred(
|
||||||
|
&mut p,
|
||||||
|
&initial,
|
||||||
|
48.0 * k * s,
|
||||||
|
(ix + icon / 2.0) * s,
|
||||||
|
(iy + icon / 2.0) * s,
|
||||||
|
[255, 255, 255, 255],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let title = ellipsis(&card.title, 14.0 * k, r.w - 20.0 * k);
|
||||||
|
centred(
|
||||||
|
&mut p,
|
||||||
|
&title,
|
||||||
|
14.0 * k * s,
|
||||||
|
(r.x + r.w / 2.0) * s,
|
||||||
|
(iy + icon + 26.0 * k) * s,
|
||||||
|
[255, 255, 255, 255],
|
||||||
|
);
|
||||||
|
let app = ellipsis(&card.app, 12.0 * k, r.w - 20.0 * k);
|
||||||
|
centred(
|
||||||
|
&mut p,
|
||||||
|
&app,
|
||||||
|
12.0 * k * s,
|
||||||
|
(r.x + r.w / 2.0) * s,
|
||||||
|
(iy + icon + 48.0 * k) * s,
|
||||||
|
[255, 255, 255, 170],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pointer(&mut self, events: &[PointerEvent], cursor: Option<&WpCursorShapeDeviceV1>) {
|
||||||
|
for event in events {
|
||||||
|
if !self.is(&event.surface) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let p = (event.position.0 as f32, event.position.1 as f32);
|
||||||
|
match event.kind {
|
||||||
|
PointerEventKind::Enter { serial } => {
|
||||||
|
if let Some(cursor) = cursor {
|
||||||
|
cursor.set_shape(serial, Shape::Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PointerEventKind::Motion { .. } => {
|
||||||
|
if let Some(i) = layout::hit(&self.rects(), p) {
|
||||||
|
self.select(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PointerEventKind::Press {
|
||||||
|
button: BTN_LEFT, ..
|
||||||
|
} => {
|
||||||
|
let hit = layout::hit(&self.rects(), p);
|
||||||
|
tracing::info!("picker click at {p:?}: card {hit:?}");
|
||||||
|
self.outcome = Some(hit);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key(&mut self, key: Keysym) {
|
||||||
|
let n = self.cards.len();
|
||||||
|
match key {
|
||||||
|
Keysym::Escape => {
|
||||||
|
tracing::info!("picker: Esc");
|
||||||
|
self.outcome = Some(None);
|
||||||
|
}
|
||||||
|
Keysym::Return | Keysym::KP_Enter => self.outcome = Some(Some(self.selected)),
|
||||||
|
Keysym::Tab if self.shift => self.select(layout::step(self.selected, n, -1)),
|
||||||
|
Keysym::ISO_Left_Tab | Keysym::Left | Keysym::Up => {
|
||||||
|
self.select(layout::step(self.selected, n, -1))
|
||||||
|
}
|
||||||
|
Keysym::Tab | Keysym::Right | Keysym::Down => {
|
||||||
|
self.select(layout::step(self.selected, n, 1))
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn modifiers(&mut self, m: Modifiers) {
|
||||||
|
self.shift = m.shift;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `s` shortened with "…" to fit `width` logical px at `size`.
|
||||||
|
fn ellipsis(s: &str, size: f32, width: f32) -> String {
|
||||||
|
if text::layout(s, size).width <= width {
|
||||||
|
return s.to_string();
|
||||||
|
}
|
||||||
|
let mut chars: Vec<char> = s.chars().collect();
|
||||||
|
while !chars.is_empty() {
|
||||||
|
chars.pop();
|
||||||
|
let t: String = chars.iter().collect::<String>() + "…";
|
||||||
|
if text::layout(&t, size).width <= width {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `s` centred on (`cx`, `cy`), in buffer px.
|
||||||
|
fn centred(p: &mut Pixmap, s: &str, size: f32, cx: f32, cy: f32, color: [u8; 4]) {
|
||||||
|
let l = text::layout(s, size);
|
||||||
|
let origin = (cx - l.width / 2.0, cy - l.height / 2.0);
|
||||||
|
if let Some(path) = text::path(s, size, origin) {
|
||||||
|
let mut paint = SkPaint::default();
|
||||||
|
paint.set_color_rgba8(color[0], color[1], color[2], color[3]);
|
||||||
|
paint.anti_alias = true;
|
||||||
|
p.fill_path(
|
||||||
|
&path,
|
||||||
|
&paint,
|
||||||
|
FillRule::Winding,
|
||||||
|
Transform::identity(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,8 +48,6 @@ struct Uniforms {
|
|||||||
pub struct Paint {
|
pub struct Paint {
|
||||||
gl: glow::Context,
|
gl: glow::Context,
|
||||||
u: Uniforms,
|
u: Uniforms,
|
||||||
// Read by set_overlay, which the picker (theme plan Task 6) starts using.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
overlay: glow::Texture,
|
overlay: glow::Texture,
|
||||||
size: (u32, u32),
|
size: (u32, u32),
|
||||||
egl: Egl,
|
egl: Egl,
|
||||||
@@ -130,7 +128,6 @@ impl Paint {
|
|||||||
|
|
||||||
/// Shows `pixmap` (premultiplied RGBA, the surface's size) over the
|
/// Shows `pixmap` (premultiplied RGBA, the surface's size) over the
|
||||||
/// paint, or nothing.
|
/// paint, or nothing.
|
||||||
#[allow(dead_code)] // the picker (theme plan Task 6) removes this
|
|
||||||
pub fn set_overlay(&mut self, pixmap: Option<&tiny_skia::Pixmap>) {
|
pub fn set_overlay(&mut self, pixmap: Option<&tiny_skia::Pixmap>) {
|
||||||
if self.current().is_err() {
|
if self.current().is_err() {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ fn paint(c: [u8; 4]) -> Paint<'static> {
|
|||||||
p
|
p
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rounded(pixmap: &mut Pixmap, r: Rect, radius: f32, color: [u8; 4], scale: f32) {
|
pub(crate) fn rounded(pixmap: &mut Pixmap, r: Rect, radius: f32, color: [u8; 4], scale: f32) {
|
||||||
let (x, y, w, h, k) = (
|
let (x, y, w, h, k) = (
|
||||||
r.x * scale,
|
r.x * scale,
|
||||||
r.y * scale,
|
r.y * scale,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ use crate::dnd::Outcome;
|
|||||||
use crate::error::HintExt;
|
use crate::error::HintExt;
|
||||||
use crate::frame::OutputGeom;
|
use crate::frame::OutputGeom;
|
||||||
use crate::host::Host;
|
use crate::host::Host;
|
||||||
|
use crate::picker::Picker;
|
||||||
use crate::region::Overlay;
|
use crate::region::Overlay;
|
||||||
use crate::toolbar::{Pill, Toolbar};
|
use crate::toolbar::{Pill, Toolbar};
|
||||||
use crate::zoom::Zoom;
|
use crate::zoom::Zoom;
|
||||||
@@ -90,6 +91,8 @@ pub struct State {
|
|||||||
pub pill: Option<Pill>,
|
pub pill: Option<Pill>,
|
||||||
/// `valw backdrop`'s surfaces.
|
/// `valw backdrop`'s surfaces.
|
||||||
pub backdrop: Option<Backdrop>,
|
pub backdrop: Option<Backdrop>,
|
||||||
|
/// `valw window`'s picker, while it is open.
|
||||||
|
pub picker: Option<Picker>,
|
||||||
/// The preview thumbnails, in the preview host process.
|
/// The preview thumbnails, in the preview host process.
|
||||||
pub preview: Option<Host>,
|
pub preview: Option<Host>,
|
||||||
}
|
}
|
||||||
@@ -125,6 +128,7 @@ impl Wayland {
|
|||||||
toolbar: None,
|
toolbar: None,
|
||||||
pill: None,
|
pill: None,
|
||||||
backdrop: None,
|
backdrop: None,
|
||||||
|
picker: None,
|
||||||
preview: None,
|
preview: None,
|
||||||
};
|
};
|
||||||
// Two round trips: one for wl_output, one for the xdg-output details.
|
// Two round trips: one for wl_output, one for the xdg-output details.
|
||||||
@@ -317,6 +321,9 @@ impl CompositorHandler for State {
|
|||||||
if let Some(backdrop) = &mut self.backdrop {
|
if let Some(backdrop) = &mut self.backdrop {
|
||||||
backdrop.frame_done(surface, qh);
|
backdrop.frame_done(surface, qh);
|
||||||
}
|
}
|
||||||
|
if let Some(picker) = &mut self.picker {
|
||||||
|
picker.frame_done(surface, qh);
|
||||||
|
}
|
||||||
if let Some(preview) = &mut self.preview {
|
if let Some(preview) = &mut self.preview {
|
||||||
preview.frame_done(surface, qh);
|
preview.frame_done(surface, qh);
|
||||||
}
|
}
|
||||||
@@ -383,6 +390,9 @@ impl LayerShellHandler for State {
|
|||||||
if let Some(backdrop) = &mut self.backdrop {
|
if let Some(backdrop) = &mut self.backdrop {
|
||||||
backdrop.closed(layer);
|
backdrop.closed(layer);
|
||||||
}
|
}
|
||||||
|
if let Some(picker) = &mut self.picker {
|
||||||
|
picker.closed(layer);
|
||||||
|
}
|
||||||
if let Some(preview) = &mut self.preview {
|
if let Some(preview) = &mut self.preview {
|
||||||
preview.closed(layer);
|
preview.closed(layer);
|
||||||
}
|
}
|
||||||
@@ -411,6 +421,9 @@ impl LayerShellHandler for State {
|
|||||||
if let Some(backdrop) = &mut self.backdrop {
|
if let Some(backdrop) = &mut self.backdrop {
|
||||||
backdrop.configure(conn, layer, configure.new_size, qh);
|
backdrop.configure(conn, layer, configure.new_size, qh);
|
||||||
}
|
}
|
||||||
|
if let Some(picker) = &mut self.picker {
|
||||||
|
picker.configure(conn, layer, configure.new_size, qh);
|
||||||
|
}
|
||||||
if let Some(preview) = &mut self.preview {
|
if let Some(preview) = &mut self.preview {
|
||||||
preview.configure(layer, qh);
|
preview.configure(layer, qh);
|
||||||
}
|
}
|
||||||
@@ -519,6 +532,9 @@ impl KeyboardHandler for State {
|
|||||||
if let Some(toolbar) = &mut self.toolbar {
|
if let Some(toolbar) = &mut self.toolbar {
|
||||||
toolbar.key(event.keysym);
|
toolbar.key(event.keysym);
|
||||||
}
|
}
|
||||||
|
if let Some(picker) = &mut self.picker {
|
||||||
|
picker.key(event.keysym);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repeat_key(
|
fn repeat_key(
|
||||||
@@ -562,6 +578,9 @@ impl KeyboardHandler for State {
|
|||||||
if let Some(overlay) = &mut self.overlay {
|
if let Some(overlay) = &mut self.overlay {
|
||||||
overlay.modifiers(modifiers, qh);
|
overlay.modifiers(modifiers, qh);
|
||||||
}
|
}
|
||||||
|
if let Some(picker) = &mut self.picker {
|
||||||
|
picker.modifiers(modifiers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -585,6 +604,9 @@ impl PointerHandler for State {
|
|||||||
if let Some(pill) = &mut self.pill {
|
if let Some(pill) = &mut self.pill {
|
||||||
pill.pointer(events);
|
pill.pointer(events);
|
||||||
}
|
}
|
||||||
|
if let Some(picker) = &mut self.picker {
|
||||||
|
picker.pointer(events, self.cursor_device.as_ref());
|
||||||
|
}
|
||||||
if self.preview.is_some() {
|
if self.preview.is_some() {
|
||||||
crate::host::pointer(self, events, qh);
|
crate::host::pointer(self, events, qh);
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-1
@@ -16,7 +16,15 @@ const TIMEOUT: Duration = Duration::from_secs(5);
|
|||||||
/// on, if niri knows. Esc during the pick is `Cancelled`.
|
/// on, if niri knows. Esc during the pick is `Cancelled`.
|
||||||
pub fn capture(cursor: bool) -> Result<(RgbaImage, Option<String>)> {
|
pub fn capture(cursor: bool) -> Result<(RgbaImage, Option<String>)> {
|
||||||
let window = niri::pick_window()?.ok_or(Cancelled)?;
|
let window = niri::pick_window()?.ok_or(Cancelled)?;
|
||||||
tracing::info!("picked window {} ({:?})", window.id, window.app_id);
|
capture_window(&window, cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Captures `window` (chosen by niri's pick or by valw's picker).
|
||||||
|
pub fn capture_window(
|
||||||
|
window: &niri_ipc::Window,
|
||||||
|
cursor: bool,
|
||||||
|
) -> Result<(RgbaImage, Option<String>)> {
|
||||||
|
tracing::info!("capturing window {} ({:?})", window.id, window.app_id);
|
||||||
let tmp = TempFile(temp_path());
|
let tmp = TempFile(temp_path());
|
||||||
niri::screenshot_window(window.id, &tmp.0, cursor, TIMEOUT)?;
|
niri::screenshot_window(window.id, &tmp.0, cursor, TIMEOUT)?;
|
||||||
let image = crate::thumbnail::load(&tmp.0)?;
|
let image = crate::thumbnail::load(&tmp.0)?;
|
||||||
|
|||||||
Reference in new issue
Block a user