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 niri;
|
||||
mod output;
|
||||
// The picker lands in pieces; Task 6 of the theme plan removes this.
|
||||
#[allow(dead_code)]
|
||||
mod picker;
|
||||
mod region;
|
||||
mod render;
|
||||
@@ -67,8 +65,11 @@ enum Command {
|
||||
#[command(flatten)]
|
||||
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 {
|
||||
/// Click the window on screen instead (niri's pick).
|
||||
#[arg(long)]
|
||||
pick: bool,
|
||||
#[command(flatten)]
|
||||
common: Common,
|
||||
},
|
||||
@@ -159,9 +160,14 @@ fn parse_delay(s: &str) -> Result<Duration, String> {
|
||||
type Shots = (Vec<(RgbaImage, Option<String>)>, usize, String);
|
||||
|
||||
enum Mode {
|
||||
Screen { all: bool },
|
||||
Screen {
|
||||
all: bool,
|
||||
},
|
||||
Region,
|
||||
Window,
|
||||
/// `pick`: niri's click-a-window instead of valw's picker.
|
||||
Window {
|
||||
pick: bool,
|
||||
},
|
||||
Zoom,
|
||||
}
|
||||
|
||||
@@ -206,7 +212,7 @@ fn run(cli: Cli) -> Result<()> {
|
||||
Command::PreviewHost => host::run(),
|
||||
Command::Screen { all, common } => capture(Mode::Screen { all }, 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::Edit { file } => editor::run(&file),
|
||||
// Handled in main, before logging starts.
|
||||
@@ -272,13 +278,18 @@ fn capture(mode: Mode, common: Common) -> Result<()> {
|
||||
region::Choice::Window => {
|
||||
drop(frames);
|
||||
windowed = true;
|
||||
window_shot(&outputs, cursor)?
|
||||
window_shot(&outputs, cursor, None)?
|
||||
}
|
||||
}
|
||||
}
|
||||
Mode::Window => {
|
||||
Mode::Window { pick } => {
|
||||
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 => {
|
||||
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
|
||||
/// the window's output, or niri's focused one if that is unknown.
|
||||
fn window_shot(outputs: &[wayland::Output], cursor: bool) -> Result<Shots> {
|
||||
let (image, output) = window::capture(cursor)?;
|
||||
fn window_shot(
|
||||
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());
|
||||
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) {
|
||||
let mode = match pick.mode {
|
||||
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::Zoom => Mode::Zoom,
|
||||
};
|
||||
@@ -452,6 +470,7 @@ mod tests {
|
||||
assert!(parses(&["__clipboard"]));
|
||||
assert!(parses(&["toolbar"]));
|
||||
assert!(parses(&["backdrop"]));
|
||||
assert!(parses(&["window", "--pick"]));
|
||||
assert!(parses(&["__combo-demo"]));
|
||||
assert!(parses(&["__check-config", "c.toml"]));
|
||||
assert!(!parses(&["__check-config"]));
|
||||
@@ -474,7 +493,7 @@ mod tests {
|
||||
);
|
||||
assert!(matches!(
|
||||
from_toolbar(pick(T::Window, false, true)).0,
|
||||
Mode::Window
|
||||
Mode::Window { pick: false }
|
||||
));
|
||||
assert!(matches!(
|
||||
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.
|
||||
#[allow(dead_code)] // the picker (theme plan Task 6) removes this
|
||||
pub fn windows() -> Result<Vec<Window>> {
|
||||
match request(Request::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
|
||||
/// 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> {
|
||||
windows.sort_by_key(|w| {
|
||||
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.
|
||||
//! 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 layout;
|
||||
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 {
|
||||
gl: glow::Context,
|
||||
u: Uniforms,
|
||||
// Read by set_overlay, which the picker (theme plan Task 6) starts using.
|
||||
#[allow(dead_code)]
|
||||
overlay: glow::Texture,
|
||||
size: (u32, u32),
|
||||
egl: Egl,
|
||||
@@ -130,7 +128,6 @@ impl Paint {
|
||||
|
||||
/// Shows `pixmap` (premultiplied RGBA, the surface's size) over the
|
||||
/// 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>) {
|
||||
if self.current().is_err() {
|
||||
return;
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ fn paint(c: [u8; 4]) -> Paint<'static> {
|
||||
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) = (
|
||||
r.x * scale,
|
||||
r.y * scale,
|
||||
|
||||
@@ -42,6 +42,7 @@ use crate::dnd::Outcome;
|
||||
use crate::error::HintExt;
|
||||
use crate::frame::OutputGeom;
|
||||
use crate::host::Host;
|
||||
use crate::picker::Picker;
|
||||
use crate::region::Overlay;
|
||||
use crate::toolbar::{Pill, Toolbar};
|
||||
use crate::zoom::Zoom;
|
||||
@@ -90,6 +91,8 @@ pub struct State {
|
||||
pub pill: Option<Pill>,
|
||||
/// `valw backdrop`'s surfaces.
|
||||
pub backdrop: Option<Backdrop>,
|
||||
/// `valw window`'s picker, while it is open.
|
||||
pub picker: Option<Picker>,
|
||||
/// The preview thumbnails, in the preview host process.
|
||||
pub preview: Option<Host>,
|
||||
}
|
||||
@@ -125,6 +128,7 @@ impl Wayland {
|
||||
toolbar: None,
|
||||
pill: None,
|
||||
backdrop: None,
|
||||
picker: None,
|
||||
preview: None,
|
||||
};
|
||||
// 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 {
|
||||
backdrop.frame_done(surface, qh);
|
||||
}
|
||||
if let Some(picker) = &mut self.picker {
|
||||
picker.frame_done(surface, qh);
|
||||
}
|
||||
if let Some(preview) = &mut self.preview {
|
||||
preview.frame_done(surface, qh);
|
||||
}
|
||||
@@ -383,6 +390,9 @@ impl LayerShellHandler for State {
|
||||
if let Some(backdrop) = &mut self.backdrop {
|
||||
backdrop.closed(layer);
|
||||
}
|
||||
if let Some(picker) = &mut self.picker {
|
||||
picker.closed(layer);
|
||||
}
|
||||
if let Some(preview) = &mut self.preview {
|
||||
preview.closed(layer);
|
||||
}
|
||||
@@ -411,6 +421,9 @@ impl LayerShellHandler for State {
|
||||
if let Some(backdrop) = &mut self.backdrop {
|
||||
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 {
|
||||
preview.configure(layer, qh);
|
||||
}
|
||||
@@ -519,6 +532,9 @@ impl KeyboardHandler for State {
|
||||
if let Some(toolbar) = &mut self.toolbar {
|
||||
toolbar.key(event.keysym);
|
||||
}
|
||||
if let Some(picker) = &mut self.picker {
|
||||
picker.key(event.keysym);
|
||||
}
|
||||
}
|
||||
|
||||
fn repeat_key(
|
||||
@@ -562,6 +578,9 @@ impl KeyboardHandler for State {
|
||||
if let Some(overlay) = &mut self.overlay {
|
||||
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 {
|
||||
pill.pointer(events);
|
||||
}
|
||||
if let Some(picker) = &mut self.picker {
|
||||
picker.pointer(events, self.cursor_device.as_ref());
|
||||
}
|
||||
if self.preview.is_some() {
|
||||
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`.
|
||||
pub fn capture(cursor: bool) -> Result<(RgbaImage, Option<String>)> {
|
||||
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());
|
||||
niri::screenshot_window(window.id, &tmp.0, cursor, TIMEOUT)?;
|
||||
let image = crate::thumbnail::load(&tmp.0)?;
|
||||
|
||||
Reference in new issue
Block a user