aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cli.rs2
-rw-r--r--src/dbus/gnome_shell_screenshot.rs32
-rw-r--r--src/input/mod.rs6
-rw-r--r--src/input/pick_color_grab.rs223
-rw-r--r--src/ipc/client.rs21
-rw-r--r--src/ipc/server.rs9
-rw-r--r--src/niri.rs41
7 files changed, 331 insertions, 3 deletions
diff --git a/src/cli.rs b/src/cli.rs
index a01bc109..ea8c422d 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -77,6 +77,8 @@ pub enum Msg {
FocusedWindow,
/// Pick a window with the mouse and print information about it.
PickWindow,
+ /// Pick a color from the screen with the mouse.
+ PickColor,
/// Perform an action.
Action {
#[command(subcommand)]
diff --git a/src/dbus/gnome_shell_screenshot.rs b/src/dbus/gnome_shell_screenshot.rs
index 742f72b2..dd762498 100644
--- a/src/dbus/gnome_shell_screenshot.rs
+++ b/src/dbus/gnome_shell_screenshot.rs
@@ -1,7 +1,10 @@
+use std::collections::HashMap;
use std::path::PathBuf;
+use niri_ipc::PickedColor;
use zbus::fdo::{self, RequestNameFlags};
use zbus::interface;
+use zbus::zvariant::OwnedValue;
use super::Start;
@@ -12,6 +15,7 @@ pub struct Screenshot {
pub enum ScreenshotToNiri {
TakeScreenshot { include_cursor: bool },
+ PickColor(async_channel::Sender<Option<PickedColor>>),
}
pub enum NiriToScreenshot {
@@ -47,6 +51,34 @@ impl Screenshot {
Ok((true, filename))
}
+
+ async fn pick_color(&self) -> fdo::Result<HashMap<String, OwnedValue>> {
+ let (tx, rx) = async_channel::bounded(1);
+ if let Err(err) = self.to_niri.send(ScreenshotToNiri::PickColor(tx)) {
+ warn!("error sending pick color message to niri: {err:?}");
+ return Err(fdo::Error::Failed("internal error".to_owned()));
+ }
+
+ let color = match rx.recv().await {
+ Ok(Some(color)) => color,
+ Ok(None) => {
+ return Err(fdo::Error::Failed("no color picked".to_owned()));
+ }
+ Err(err) => {
+ warn!("error receiving message from niri: {err:?}");
+ return Err(fdo::Error::Failed("internal error".to_owned()));
+ }
+ };
+
+ let mut result = HashMap::new();
+ let rgb_slice: &[f64] = &color.rgb;
+ result.insert(
+ "color".to_string(),
+ zbus::zvariant::Value::from(rgb_slice).try_into().unwrap(),
+ );
+
+ Ok(result)
+ }
}
impl Screenshot {
diff --git a/src/input/mod.rs b/src/input/mod.rs
index 8fb5acb6..2744561c 100644
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -48,6 +48,7 @@ use crate::utils::{center, get_monotonic_time, ResizeEdge};
pub mod backend_ext;
pub mod move_grab;
+pub mod pick_color_grab;
pub mod pick_window_grab;
pub mod resize_grab;
pub mod scroll_tracker;
@@ -377,7 +378,10 @@ impl State {
}
}
- if pressed && raw == Some(Keysym::Escape) && this.niri.pick_window.is_some() {
+ if pressed
+ && raw == Some(Keysym::Escape)
+ && (this.niri.pick_window.is_some() || this.niri.pick_color.is_some())
+ {
// We window picking state so the pick window grab must be active.
// Unsetting it cancels window picking.
this.niri
diff --git a/src/input/pick_color_grab.rs b/src/input/pick_color_grab.rs
new file mode 100644
index 00000000..3484b66e
--- /dev/null
+++ b/src/input/pick_color_grab.rs
@@ -0,0 +1,223 @@
+use niri_ipc::PickedColor;
+use smithay::backend::allocator::Fourcc;
+use smithay::backend::input::ButtonState;
+use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
+use smithay::input::pointer::{
+ AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
+ GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
+ GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
+ MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
+};
+use smithay::input::SeatHandler;
+use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
+
+use crate::niri::State;
+use crate::render_helpers::render_to_vec;
+
+pub struct PickColorGrab {
+ start_data: PointerGrabStartData<State>,
+}
+
+impl PickColorGrab {
+ pub fn new(start_data: PointerGrabStartData<State>) -> Self {
+ Self { start_data }
+ }
+
+ fn on_ungrab(&mut self, state: &mut State) {
+ if let Some(tx) = state.niri.pick_color.take() {
+ let _ = tx.send_blocking(None);
+ }
+ state
+ .niri
+ .cursor_manager
+ .set_cursor_image(CursorImageStatus::default_named());
+ state.niri.queue_redraw_all();
+ }
+
+ fn pick_color_at_point(location: Point<f64, Logical>, data: &mut State) -> Option<PickedColor> {
+ let (output, pos_within_output) = data.niri.output_under(location)?;
+ let output = output.clone();
+
+ data.backend
+ .with_primary_renderer(|renderer| {
+ data.niri.update_render_elements(Some(&output));
+
+ let scale = Scale::from(output.current_scale().fractional_scale());
+ // FIXME: perhaps replace floor with round once we figure out the pointer behavior
+ // at the bottom/right edges of the monitors.
+ let pos = pos_within_output.to_physical_precise_floor(scale);
+ let size = Size::<i32, Physical>::from((1, 1));
+
+ let elements = data.niri.render(
+ renderer,
+ &output,
+ false,
+ crate::render_helpers::RenderTarget::ScreenCapture,
+ );
+
+ let pixels = match render_to_vec(
+ renderer,
+ size,
+ scale,
+ Transform::Normal,
+ Fourcc::Abgr8888,
+ elements.iter().rev().map(|elem| {
+ let offset = pos.upscale(-1);
+ RelocateRenderElement::from_element(elem, offset, Relocate::Relative)
+ }),
+ ) {
+ Ok(pixels) => pixels,
+ Err(_) => return None,
+ };
+
+ if pixels.len() == 4 {
+ let rgb = [
+ f64::from(pixels[0]) / 255.0,
+ f64::from(pixels[1]) / 255.0,
+ f64::from(pixels[2]) / 255.0,
+ ];
+ Some(PickedColor { rgb })
+ } else {
+ error!(
+ "unexpected pixel data length: {} (expected 4)",
+ pixels.len()
+ );
+ None
+ }
+ })
+ .flatten()
+ }
+}
+
+impl PointerGrab<State> for PickColorGrab {
+ fn motion(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ _focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
+ event: &MotionEvent,
+ ) {
+ handle.motion(data, None, event);
+ }
+
+ fn relative_motion(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ _focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
+ event: &RelativeMotionEvent,
+ ) {
+ handle.relative_motion(data, None, event);
+ }
+
+ fn button(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ event: &ButtonEvent,
+ ) {
+ if event.state != ButtonState::Pressed {
+ return;
+ }
+
+ if let Some(tx) = data.niri.pick_color.take() {
+ let color = Self::pick_color_at_point(handle.current_location(), data);
+ let _ = tx.send_blocking(color);
+ }
+
+ handle.unset_grab(self, data, event.serial, event.time, true);
+ }
+
+ fn axis(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ details: AxisFrame,
+ ) {
+ handle.axis(data, details);
+ }
+
+ fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) {
+ handle.frame(data);
+ }
+
+ fn gesture_swipe_begin(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ event: &GestureSwipeBeginEvent,
+ ) {
+ handle.gesture_swipe_begin(data, event);
+ }
+
+ fn gesture_swipe_update(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ event: &GestureSwipeUpdateEvent,
+ ) {
+ handle.gesture_swipe_update(data, event);
+ }
+
+ fn gesture_swipe_end(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ event: &GestureSwipeEndEvent,
+ ) {
+ handle.gesture_swipe_end(data, event);
+ }
+
+ fn gesture_pinch_begin(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ event: &GesturePinchBeginEvent,
+ ) {
+ handle.gesture_pinch_begin(data, event);
+ }
+
+ fn gesture_pinch_update(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ event: &GesturePinchUpdateEvent,
+ ) {
+ handle.gesture_pinch_update(data, event);
+ }
+
+ fn gesture_pinch_end(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ event: &GesturePinchEndEvent,
+ ) {
+ handle.gesture_pinch_end(data, event);
+ }
+
+ fn gesture_hold_begin(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ event: &GestureHoldBeginEvent,
+ ) {
+ handle.gesture_hold_begin(data, event);
+ }
+
+ fn gesture_hold_end(
+ &mut self,
+ data: &mut State,
+ handle: &mut PointerInnerHandle<'_, State>,
+ event: &GestureHoldEndEvent,
+ ) {
+ handle.gesture_hold_end(data, event);
+ }
+
+ fn start_data(&self) -> &PointerGrabStartData<State> {
+ &self.start_data
+ }
+
+ fn unset(&mut self, data: &mut State) {
+ self.on_ungrab(data);
+ }
+}
diff --git a/src/ipc/client.rs b/src/ipc/client.rs
index 66cdaf7d..d4165733 100644
--- a/src/ipc/client.rs
+++ b/src/ipc/client.rs
@@ -20,6 +20,7 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
Msg::FocusedWindow => Request::FocusedWindow,
Msg::FocusedOutput => Request::FocusedOutput,
Msg::PickWindow => Request::PickWindow,
+ Msg::PickColor => Request::PickColor,
Msg::Action { action } => Request::Action(action.clone()),
Msg::Output { output, action } => Request::Output {
output: output.clone(),
@@ -270,6 +271,26 @@ pub fn handle_msg(msg: Msg, json: bool) -> anyhow::Result<()> {
println!("No window selected.");
}
}
+ Msg::PickColor => {
+ let Response::PickedColor(color) = response else {
+ bail!("unexpected response: expected PickedColor, got {response:?}");
+ };
+
+ if json {
+ let color = serde_json::to_string(&color).context("error formatting response")?;
+ println!("{color}");
+ return Ok(());
+ }
+
+ if let Some(color) = color {
+ let [r, g, b] = color.rgb.map(|v| (v.clamp(0., 1.) * 255.).round() as u8);
+
+ println!("Picked color: rgb({r}, {g}, {b})",);
+ println!("Hex: #{:02x}{:02x}{:02x}", r, g, b);
+ } else {
+ println!("No color was picked.");
+ }
+ }
Msg::Action { .. } => {
let Response::Handled = response else {
bail!("unexpected response: expected Handled, got {response:?}");
diff --git a/src/ipc/server.rs b/src/ipc/server.rs
index c8f8edcf..2c948cac 100644
--- a/src/ipc/server.rs
+++ b/src/ipc/server.rs
@@ -356,6 +356,15 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
});
Response::PickedWindow(window)
}
+ Request::PickColor => {
+ let (tx, rx) = async_channel::bounded(1);
+ ctx.event_loop.insert_idle(move |state| {
+ state.handle_pick_color(tx);
+ });
+ let result = rx.recv().await;
+ let color = result.map_err(|_| String::from("error getting picked color"))?;
+ Response::PickedColor(color)
+ }
Request::Action(action) => {
let (tx, rx) = async_channel::bounded(1);
diff --git a/src/niri.rs b/src/niri.rs
index 30042fc9..35c8b2d7 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -45,7 +45,10 @@ use smithay::desktop::{
PopupUngrabStrategy, Space, Window, WindowSurfaceType,
};
use smithay::input::keyboard::Layout as KeyboardLayout;
-use smithay::input::pointer::{CursorIcon, CursorImageStatus, CursorImageSurfaceData, MotionEvent};
+use smithay::input::pointer::{
+ CursorIcon, CursorImageStatus, CursorImageSurfaceData, Focus,
+ GrabStartData as PointerGrabStartData, MotionEvent,
+};
use smithay::input::{Seat, SeatState};
use smithay::output::{self, Output, OutputModeSource, PhysicalProperties, Subpixel, WeakOutput};
use smithay::reexports::calloop::generic::Generic;
@@ -118,6 +121,7 @@ use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri};
use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri};
use crate::frame_clock::FrameClock;
use crate::handlers::{configure_lock_surface, XDG_ACTIVATION_TOKEN_TIMEOUT};
+use crate::input::pick_color_grab::PickColorGrab;
use crate::input::scroll_tracker::ScrollTracker;
use crate::input::{
apply_libinput_settings, mods_with_finger_scroll_binds, mods_with_mouse_binds,
@@ -358,6 +362,7 @@ pub struct Niri {
pub exit_confirm_dialog: Option<ExitConfirmDialog>,
pub pick_window: Option<async_channel::Sender<Option<MappedId>>>,
+ pub pick_color: Option<async_channel::Sender<Option<niri_ipc::PickedColor>>>,
pub debug_draw_opaque_regions: bool,
pub debug_draw_damage: bool,
@@ -1612,6 +1617,22 @@ impl State {
self.niri.queue_redraw_all();
}
+ pub fn handle_pick_color(&mut self, tx: async_channel::Sender<Option<niri_ipc::PickedColor>>) {
+ let pointer = self.niri.seat.get_pointer().unwrap();
+ let start_data = PointerGrabStartData {
+ focus: None,
+ button: 0,
+ location: pointer.current_location(),
+ };
+ let grab = PickColorGrab::new(start_data);
+ pointer.set_grab(self, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
+ self.niri.pick_color = Some(tx);
+ self.niri
+ .cursor_manager
+ .set_cursor_image(CursorImageStatus::Named(CursorIcon::Crosshair));
+ self.niri.queue_redraw_all();
+ }
+
#[cfg(feature = "xdp-gnome-screencast")]
pub fn on_pw_msg(&mut self, msg: PwToNiri) {
match msg {
@@ -1903,7 +1924,22 @@ impl State {
to_screenshot: &async_channel::Sender<NiriToScreenshot>,
msg: ScreenshotToNiri,
) {
- let ScreenshotToNiri::TakeScreenshot { include_cursor } = msg;
+ match msg {
+ ScreenshotToNiri::TakeScreenshot { include_cursor } => {
+ self.handle_take_screenshot(to_screenshot, include_cursor);
+ }
+ ScreenshotToNiri::PickColor(tx) => {
+ self.handle_pick_color(tx);
+ }
+ }
+ }
+
+ #[cfg(feature = "dbus")]
+ fn handle_take_screenshot(
+ &mut self,
+ to_screenshot: &async_channel::Sender<NiriToScreenshot>,
+ include_cursor: bool,
+ ) {
let _span = tracy_client::span!("TakeScreenshot");
let rv = self.backend.with_primary_renderer(|renderer| {
@@ -2351,6 +2387,7 @@ impl Niri {
exit_confirm_dialog,
pick_window: None,
+ pick_color: None,
debug_draw_opaque_regions: false,
debug_draw_damage: false,