mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Implement an Overview
This commit is contained in:
@@ -62,6 +62,8 @@ pub struct Config {
|
||||
#[knuffel(child, default)]
|
||||
pub gestures: Gestures,
|
||||
#[knuffel(child, default)]
|
||||
pub overview: Overview,
|
||||
#[knuffel(child, default)]
|
||||
pub environment: Environment,
|
||||
#[knuffel(children(name = "window-rule"))]
|
||||
pub window_rules: Vec<WindowRule>,
|
||||
@@ -988,6 +990,8 @@ pub struct Animations {
|
||||
pub config_notification_open_close: ConfigNotificationOpenCloseAnim,
|
||||
#[knuffel(child, default)]
|
||||
pub screenshot_ui_open: ScreenshotUiOpenAnim,
|
||||
#[knuffel(child, default)]
|
||||
pub overview_open_close: OverviewOpenCloseAnim,
|
||||
}
|
||||
|
||||
impl Default for Animations {
|
||||
@@ -1003,6 +1007,7 @@ impl Default for Animations {
|
||||
window_resize: Default::default(),
|
||||
config_notification_open_close: Default::default(),
|
||||
screenshot_ui_open: Default::default(),
|
||||
overview_open_close: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1150,6 +1155,22 @@ impl Default for ScreenshotUiOpenAnim {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct OverviewOpenCloseAnim(pub Animation);
|
||||
|
||||
impl Default for OverviewOpenCloseAnim {
|
||||
fn default() -> Self {
|
||||
Self(Animation {
|
||||
off: false,
|
||||
kind: AnimationKind::Spring(SpringParams {
|
||||
damping_ratio: 1.,
|
||||
stiffness: 800,
|
||||
epsilon: 0.0001,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Animation {
|
||||
pub off: bool,
|
||||
@@ -1209,6 +1230,20 @@ impl Default for DndEdgeViewScroll {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Overview {
|
||||
#[knuffel(child, unwrap(argument), default = Self::default().zoom)]
|
||||
pub zoom: FloatOrInt<0, 1>,
|
||||
}
|
||||
|
||||
impl Default for Overview {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
zoom: FloatOrInt(0.5),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(knuffel::Decode, Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Environment(#[knuffel(children)] pub Vec<EnvironmentVariable>);
|
||||
|
||||
@@ -1720,6 +1755,9 @@ pub enum Action {
|
||||
SetDynamicCastWindowById(u64),
|
||||
SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
|
||||
ClearDynamicCastTarget,
|
||||
ToggleOverview,
|
||||
OpenOverview,
|
||||
CloseOverview,
|
||||
}
|
||||
|
||||
impl From<niri_ipc::Action> for Action {
|
||||
@@ -1984,6 +2022,9 @@ impl From<niri_ipc::Action> for Action {
|
||||
Self::SetDynamicCastMonitor(output)
|
||||
}
|
||||
niri_ipc::Action::ClearDynamicCastTarget {} => Self::ClearDynamicCastTarget,
|
||||
niri_ipc::Action::ToggleOverview {} => Self::ToggleOverview,
|
||||
niri_ipc::Action::OpenOverview {} => Self::OpenOverview,
|
||||
niri_ipc::Action::CloseOverview {} => Self::CloseOverview,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2968,6 +3009,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> knuffel::Decode<S> for OverviewOpenCloseAnim
|
||||
where
|
||||
S: knuffel::traits::ErrorSpan,
|
||||
{
|
||||
fn decode_node(
|
||||
node: &knuffel::ast::SpannedNode<S>,
|
||||
ctx: &mut knuffel::decode::Context<S>,
|
||||
) -> Result<Self, DecodeError<S>> {
|
||||
let default = Self::default().0;
|
||||
Ok(Self(Animation::decode_node(node, ctx, default, |_, _| {
|
||||
Ok(false)
|
||||
})?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
pub fn new_off() -> Self {
|
||||
Self {
|
||||
@@ -4469,6 +4525,18 @@ mod tests {
|
||||
),
|
||||
},
|
||||
),
|
||||
overview_open_close: OverviewOpenCloseAnim(
|
||||
Animation {
|
||||
off: false,
|
||||
kind: Spring(
|
||||
SpringParams {
|
||||
damping_ratio: 1.0,
|
||||
stiffness: 800,
|
||||
epsilon: 0.0001,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
gestures: Gestures {
|
||||
dnd_edge_view_scroll: DndEdgeViewScroll {
|
||||
@@ -4481,6 +4549,11 @@ mod tests {
|
||||
),
|
||||
},
|
||||
},
|
||||
overview: Overview {
|
||||
zoom: FloatOrInt(
|
||||
0.5,
|
||||
),
|
||||
},
|
||||
environment: Environment(
|
||||
[
|
||||
EnvironmentVariable {
|
||||
|
||||
@@ -764,6 +764,12 @@ pub enum Action {
|
||||
},
|
||||
/// Clear the dynamic cast target, making it show nothing.
|
||||
ClearDynamicCastTarget {},
|
||||
/// Toggle (open/close) the Overview.
|
||||
ToggleOverview {},
|
||||
/// Open the Overview.
|
||||
OpenOverview {},
|
||||
/// Close the Overview.
|
||||
CloseOverview {},
|
||||
}
|
||||
|
||||
/// Change in window or column size.
|
||||
|
||||
@@ -153,7 +153,7 @@ impl XdgShellHandler for State {
|
||||
|
||||
match start_data {
|
||||
PointerOrTouchStartData::Pointer(start_data) => {
|
||||
let grab = MoveGrab::new(start_data, window);
|
||||
let grab = MoveGrab::new(start_data, window, false);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
PointerOrTouchStartData::Touch(start_data) => {
|
||||
@@ -316,6 +316,9 @@ impl XdgShellHandler for State {
|
||||
} else if let Some(output) = self.niri.layout.active_output() {
|
||||
let layers = layer_map_for_output(output);
|
||||
|
||||
// FIXME: somewhere here we probably need to check is_overview_open to match the logic
|
||||
// in update_keyboard_focus().
|
||||
|
||||
if layers
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
|
||||
.is_none()
|
||||
|
||||
+192
-18
@@ -394,7 +394,7 @@ impl State {
|
||||
}
|
||||
|
||||
let bindings = &this.niri.config.borrow().binds;
|
||||
should_intercept_key(
|
||||
let res = should_intercept_key(
|
||||
&mut this.niri.suppressed_keys,
|
||||
bindings,
|
||||
mod_key,
|
||||
@@ -406,7 +406,20 @@ impl State {
|
||||
&this.niri.screenshot_ui,
|
||||
this.niri.config.borrow().input.disable_power_key_handling,
|
||||
is_inhibiting_shortcuts,
|
||||
)
|
||||
);
|
||||
|
||||
if matches!(res, FilterResult::Forward) {
|
||||
// If we didn't find any bind, try other hardcoded keys.
|
||||
if this.niri.keyboard_focus.is_overview() && pressed {
|
||||
if let Some(bind) = raw.and_then(|raw| hardcoded_overview_bind(raw, *mods))
|
||||
{
|
||||
this.niri.suppressed_keys.insert(key_code);
|
||||
return FilterResult::Intercept(Some(bind));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
},
|
||||
) else {
|
||||
return;
|
||||
@@ -1915,6 +1928,20 @@ impl State {
|
||||
Action::ClearDynamicCastTarget => {
|
||||
self.set_dynamic_cast_target(CastTarget::Nothing);
|
||||
}
|
||||
Action::ToggleOverview => {
|
||||
self.niri.layout.toggle_overview();
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
Action::OpenOverview => {
|
||||
if self.niri.layout.open_overview() {
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
Action::CloseOverview => {
|
||||
if self.niri.layout.close_overview() {
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2235,13 +2262,49 @@ impl State {
|
||||
self.niri.pointer_hidden = false;
|
||||
self.niri.tablet_cursor_location = None;
|
||||
|
||||
let is_overview_open = self.niri.layout.is_overview_open();
|
||||
|
||||
if is_overview_open && !pointer.is_grabbed() && button == Some(MouseButton::Right) {
|
||||
if let Some((output, ws)) = self.niri.workspace_under_cursor(true) {
|
||||
let ws_id = ws.id();
|
||||
let ws_idx = self.niri.layout.find_workspace_by_id(ws_id).unwrap().0;
|
||||
|
||||
self.niri.layout.focus_output(&output);
|
||||
|
||||
let location = pointer.current_location();
|
||||
let start_data = PointerGrabStartData {
|
||||
focus: None,
|
||||
button: button_code,
|
||||
location,
|
||||
};
|
||||
self.niri
|
||||
.layout
|
||||
.view_offset_gesture_begin(&output, Some(ws_idx), false);
|
||||
let grab = SpatialMovementGrab::new(start_data, output, ws_id, true);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::AllScroll));
|
||||
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if button == Some(MouseButton::Middle) && !pointer.is_grabbed() {
|
||||
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
|
||||
if mod_down {
|
||||
let output_ws = self.niri.output_under_cursor().and_then(|output| {
|
||||
let mon = self.niri.layout.monitor_for_output(&output)?;
|
||||
Some((output, mon.active_workspace_ref()))
|
||||
});
|
||||
let output_ws = if is_overview_open {
|
||||
self.niri.workspace_under_cursor(true)
|
||||
} else {
|
||||
// We don't want to accidentally "catch" the wrong workspace during
|
||||
// animations.
|
||||
self.niri.output_under_cursor().and_then(|output| {
|
||||
let mon = self.niri.layout.monitor_for_output(&output)?;
|
||||
Some((output, mon.active_workspace_ref()))
|
||||
})
|
||||
};
|
||||
|
||||
if let Some((output, ws)) = output_ws {
|
||||
let ws_id = ws.id();
|
||||
@@ -2254,7 +2317,7 @@ impl State {
|
||||
button: button_code,
|
||||
location,
|
||||
};
|
||||
let grab = SpatialMovementGrab::new(start_data, output, ws_id);
|
||||
let grab = SpatialMovementGrab::new(start_data, output, ws_id, false);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri
|
||||
.cursor_manager
|
||||
@@ -2276,12 +2339,14 @@ impl State {
|
||||
// Check if we need to start an interactive move.
|
||||
if button == Some(MouseButton::Left) && !pointer.is_grabbed() {
|
||||
let mod_down = modifiers_from_state(mods).contains(mod_key.to_modifiers());
|
||||
if mod_down {
|
||||
if is_overview_open || mod_down {
|
||||
let location = pointer.current_location();
|
||||
let (output, pos_within_output) = self.niri.output_under(location).unwrap();
|
||||
let output = output.clone();
|
||||
|
||||
self.niri.layout.activate_window(&window);
|
||||
if !is_overview_open {
|
||||
self.niri.layout.activate_window(&window);
|
||||
}
|
||||
|
||||
if self.niri.layout.interactive_move_begin(
|
||||
window.clone(),
|
||||
@@ -2293,11 +2358,14 @@ impl State {
|
||||
button: button_code,
|
||||
location,
|
||||
};
|
||||
let grab = MoveGrab::new(start_data, window.clone());
|
||||
let grab = MoveGrab::new(start_data, window.clone(), is_overview_open);
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
|
||||
|
||||
if !is_overview_open {
|
||||
self.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2372,7 +2440,20 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.layout.activate_window(&window);
|
||||
if !is_overview_open {
|
||||
self.niri.layout.activate_window(&window);
|
||||
}
|
||||
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
} else if let Some((output, ws)) = is_overview_open
|
||||
.then(|| self.niri.workspace_under_cursor(false))
|
||||
.flatten()
|
||||
{
|
||||
let ws_idx = self.niri.layout.find_workspace_by_id(ws.id()).unwrap().0;
|
||||
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.niri.layout.toggle_overview_to_workspace(ws_idx);
|
||||
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
@@ -2684,6 +2765,8 @@ impl State {
|
||||
let tool = self.niri.seat.tablet_seat().get_tool(&event.tool());
|
||||
|
||||
if let Some(tool) = tool {
|
||||
let is_overview_open = self.niri.layout.is_overview_open();
|
||||
|
||||
match event.tip_state() {
|
||||
TabletToolTipState::Down => {
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
@@ -2692,8 +2775,31 @@ impl State {
|
||||
if let Some(pos) = self.niri.tablet_cursor_location {
|
||||
let under = self.niri.contents_under(pos);
|
||||
if let Some((window, _)) = under.window {
|
||||
if let Some(output) = is_overview_open.then_some(under.output).flatten()
|
||||
{
|
||||
let mut workspaces = self.niri.layout.workspaces();
|
||||
if let Some(ws_idx) = workspaces.find_map(|(_, ws_idx, ws)| {
|
||||
ws.windows().any(|w| w.window == window).then_some(ws_idx)
|
||||
}) {
|
||||
drop(workspaces);
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.niri.layout.toggle_overview_to_workspace(ws_idx);
|
||||
}
|
||||
}
|
||||
|
||||
self.niri.layout.activate_window(&window);
|
||||
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
} else if let Some((output, ws)) = is_overview_open
|
||||
.then(|| self.niri.workspace_under(false, pos))
|
||||
.flatten()
|
||||
{
|
||||
let ws_idx = self.niri.layout.find_workspace_by_id(ws.id()).unwrap().0;
|
||||
|
||||
self.niri.layout.focus_output(&output);
|
||||
self.niri.layout.toggle_overview_to_workspace(ws_idx);
|
||||
|
||||
// FIXME: granular.
|
||||
self.niri.queue_redraw_all();
|
||||
} else if let Some(output) = under.output {
|
||||
@@ -2779,6 +2885,12 @@ impl State {
|
||||
if event.fingers() == 3 {
|
||||
self.niri.gesture_swipe_3f_cumulative = Some((0., 0.));
|
||||
|
||||
// We handled this event.
|
||||
return;
|
||||
} else if event.fingers() == 4 {
|
||||
self.niri.layout.overview_gesture_begin();
|
||||
self.niri.queue_redraw_all();
|
||||
|
||||
// We handled this event.
|
||||
return;
|
||||
}
|
||||
@@ -2816,6 +2928,8 @@ impl State {
|
||||
delta_y = libinput_event.dy_unaccelerated();
|
||||
}
|
||||
|
||||
let uninverted_delta_y = delta_y;
|
||||
|
||||
let device = event.device();
|
||||
if let Some(device) = (&device as &dyn Any).downcast_ref::<input::Device>() {
|
||||
if device.config_scroll_natural_scroll_enabled() {
|
||||
@@ -2824,6 +2938,8 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
let is_overview_open = self.niri.layout.is_overview_open();
|
||||
|
||||
if let Some((cx, cy)) = &mut self.niri.gesture_swipe_3f_cumulative {
|
||||
*cx += delta_x;
|
||||
*cy += delta_y;
|
||||
@@ -2835,10 +2951,16 @@ impl State {
|
||||
|
||||
if let Some(output) = self.niri.output_under_cursor() {
|
||||
if cx.abs() > cy.abs() {
|
||||
let output_ws = self.niri.output_under_cursor().and_then(|output| {
|
||||
let mon = self.niri.layout.monitor_for_output(&output)?;
|
||||
Some((output, mon.active_workspace_ref()))
|
||||
});
|
||||
let output_ws = if is_overview_open {
|
||||
self.niri.workspace_under_cursor(true)
|
||||
} else {
|
||||
// We don't want to accidentally "catch" the wrong workspace during
|
||||
// animations.
|
||||
self.niri.output_under_cursor().and_then(|output| {
|
||||
let mon = self.niri.layout.monitor_for_output(&output)?;
|
||||
Some((output, mon.active_workspace_ref()))
|
||||
})
|
||||
};
|
||||
|
||||
if let Some((output, ws)) = output_ws {
|
||||
let ws_idx = self.niri.layout.find_workspace_by_id(ws.id()).unwrap().0;
|
||||
@@ -2880,6 +3002,17 @@ impl State {
|
||||
handled = true;
|
||||
}
|
||||
|
||||
let res = self
|
||||
.niri
|
||||
.layout
|
||||
.overview_gesture_update(-uninverted_delta_y, timestamp);
|
||||
if let Some(redraw) = res {
|
||||
if redraw {
|
||||
self.niri.queue_redraw_all();
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if handled {
|
||||
// We handled this event.
|
||||
return;
|
||||
@@ -2916,6 +3049,12 @@ impl State {
|
||||
handled = true;
|
||||
}
|
||||
|
||||
let res = self.niri.layout.overview_gesture_end();
|
||||
if res {
|
||||
self.niri.queue_redraw_all();
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if handled {
|
||||
// We handled this event.
|
||||
return;
|
||||
@@ -3512,6 +3651,41 @@ fn allowed_during_screenshot(action: &Action) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
fn hardcoded_overview_bind(raw: Keysym, mods: ModifiersState) -> Option<Bind> {
|
||||
let mods = modifiers_from_state(mods);
|
||||
if !mods.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut repeat = true;
|
||||
let action = match raw {
|
||||
Keysym::Escape | Keysym::Return => {
|
||||
repeat = false;
|
||||
Action::ToggleOverview
|
||||
}
|
||||
Keysym::Left => Action::FocusColumnLeft,
|
||||
Keysym::Right => Action::FocusColumnRight,
|
||||
Keysym::Up => Action::FocusWindowOrWorkspaceUp,
|
||||
Keysym::Down => Action::FocusWindowOrWorkspaceDown,
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(Bind {
|
||||
key: Key {
|
||||
trigger: Trigger::Keysym(raw),
|
||||
modifiers: Modifiers::empty(),
|
||||
},
|
||||
action,
|
||||
repeat,
|
||||
cooldown: None,
|
||||
allow_when_locked: false,
|
||||
allow_inhibiting: false,
|
||||
hotkey_overlay_title: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply_libinput_settings(config: &niri_config::Input, device: &mut input::Device) {
|
||||
// According to Mutter code, this setting is specific to touchpads.
|
||||
let is_touchpad = device.config_tap_finger_count() > 0;
|
||||
|
||||
+42
-5
@@ -1,10 +1,11 @@
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, CursorImageStatus, GestureHoldBeginEvent, GestureHoldEndEvent,
|
||||
GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent,
|
||||
GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent,
|
||||
GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
|
||||
GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle,
|
||||
RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::SeatHandler;
|
||||
use smithay::utils::{IsAlive, Logical, Point};
|
||||
@@ -15,14 +16,32 @@ pub struct MoveGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
last_location: Point<f64, Logical>,
|
||||
window: Window,
|
||||
gesture: GestureState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GestureState {
|
||||
Recognizing,
|
||||
Move,
|
||||
}
|
||||
|
||||
impl MoveGrab {
|
||||
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
|
||||
pub fn new(
|
||||
start_data: PointerGrabStartData<State>,
|
||||
window: Window,
|
||||
use_threshold: bool,
|
||||
) -> Self {
|
||||
let gesture = if use_threshold {
|
||||
GestureState::Recognizing
|
||||
} else {
|
||||
GestureState::Move
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
window,
|
||||
gesture,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +72,24 @@ impl PointerGrab<State> for MoveGrab {
|
||||
let output = output.clone();
|
||||
let event_delta = event.location - self.last_location;
|
||||
self.last_location = event.location;
|
||||
|
||||
if self.gesture == GestureState::Recognizing {
|
||||
let c = event.location - self.start_data.location;
|
||||
|
||||
// Check if the gesture moved far enough to decide.
|
||||
if c.x * c.x + c.y * c.y >= 8. * 8. {
|
||||
self.gesture = GestureState::Move;
|
||||
|
||||
data.niri
|
||||
.cursor_manager
|
||||
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
|
||||
}
|
||||
}
|
||||
|
||||
if self.gesture != GestureState::Move {
|
||||
return;
|
||||
}
|
||||
|
||||
let ongoing = data.niri.layout.interactive_move_update(
|
||||
&self.window,
|
||||
event_delta,
|
||||
|
||||
@@ -33,13 +33,20 @@ impl SpatialMovementGrab {
|
||||
start_data: PointerGrabStartData<State>,
|
||||
output: Output,
|
||||
workspace_id: WorkspaceId,
|
||||
is_view_offset: bool,
|
||||
) -> Self {
|
||||
let gesture = if is_view_offset {
|
||||
GestureState::ViewOffset
|
||||
} else {
|
||||
GestureState::Recognizing
|
||||
};
|
||||
|
||||
Self {
|
||||
last_location: start_data.location,
|
||||
start_data,
|
||||
output,
|
||||
workspace_id,
|
||||
gesture: GestureState::Recognizing,
|
||||
gesture,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+394
-44
@@ -45,6 +45,7 @@ use niri_config::{
|
||||
use niri_ipc::{ColumnDisplay, PositionChange, SizeChange};
|
||||
use scrolling::{Column, ColumnWidth};
|
||||
use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement;
|
||||
use smithay::backend::renderer::element::utils::RescaleRenderElement;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::output::{self, Output};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
@@ -55,7 +56,8 @@ use workspace::{WorkspaceAddWindowTarget, WorkspaceId};
|
||||
pub use self::monitor::MonitorRenderElement;
|
||||
use self::monitor::{Monitor, WorkspaceSwitch};
|
||||
use self::workspace::{OutputId, Workspace};
|
||||
use crate::animation::Clock;
|
||||
use crate::animation::{Animation, Clock};
|
||||
use crate::input::swipe_tracker::SwipeTracker;
|
||||
use crate::layout::scrolling::ScrollDirection;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::offscreen::OffscreenData;
|
||||
@@ -96,6 +98,14 @@ const INTERACTIVE_MOVE_START_THRESHOLD: f64 = 256. * 256.;
|
||||
/// Opacity of interactively moved tiles targeting the scrolling layout.
|
||||
const INTERACTIVE_MOVE_ALPHA: f64 = 0.75;
|
||||
|
||||
/// Amount of touchpad movement to toggle the overview.
|
||||
const OVERVIEW_GESTURE_MOVEMENT: f64 = 300.;
|
||||
|
||||
const OVERVIEW_GESTURE_RUBBER_BAND: RubberBand = RubberBand {
|
||||
stiffness: 0.5,
|
||||
limit: 0.05,
|
||||
};
|
||||
|
||||
/// Size-relative units.
|
||||
pub struct SizeFrac;
|
||||
|
||||
@@ -293,6 +303,13 @@ pub struct Layout<W: LayoutElement> {
|
||||
clock: Clock,
|
||||
/// Time that we last updated render elements for.
|
||||
update_render_elements_time: Duration,
|
||||
/// Whether the overview is open.
|
||||
///
|
||||
/// This is a boolean flag that controls things like where input goes to. The actual animation
|
||||
/// is controlled by overview_progress.
|
||||
overview_open: bool,
|
||||
/// The overview zoom progress.
|
||||
overview_progress: Option<OverviewProgress>,
|
||||
/// Configurable properties of the layout.
|
||||
options: Rc<Options>,
|
||||
}
|
||||
@@ -338,6 +355,7 @@ pub struct Options {
|
||||
pub preset_window_heights: Vec<PresetSize>,
|
||||
pub animations: niri_config::Animations,
|
||||
pub gestures: niri_config::Gestures,
|
||||
pub overview: niri_config::Overview,
|
||||
// Debug flags.
|
||||
pub disable_resize_throttling: bool,
|
||||
pub disable_transactions: bool,
|
||||
@@ -365,6 +383,7 @@ impl Default for Options {
|
||||
default_column_width: None,
|
||||
animations: Default::default(),
|
||||
gestures: Default::default(),
|
||||
overview: Default::default(),
|
||||
disable_resize_throttling: false,
|
||||
disable_transactions: false,
|
||||
preset_window_heights: vec![
|
||||
@@ -493,6 +512,21 @@ pub enum HitType {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum OverviewProgress {
|
||||
Animation(Animation),
|
||||
Gesture(OverviewGesture),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct OverviewGesture {
|
||||
tracker: SwipeTracker,
|
||||
/// Start point.
|
||||
start: f64,
|
||||
/// Current progress.
|
||||
value: f64,
|
||||
}
|
||||
|
||||
impl<W: LayoutElement> InteractiveMoveState<W> {
|
||||
fn moving(&self) -> Option<&InteractiveMoveData<W>> {
|
||||
match self {
|
||||
@@ -510,16 +544,16 @@ impl<W: LayoutElement> InteractiveMoveState<W> {
|
||||
}
|
||||
|
||||
impl<W: LayoutElement> InteractiveMoveData<W> {
|
||||
fn tile_render_location(&self) -> Point<f64, Logical> {
|
||||
fn tile_render_location(&self, zoom: f64) -> Point<f64, Logical> {
|
||||
let scale = Scale::from(self.output.current_scale().fractional_scale());
|
||||
let window_size = self.tile.window_size();
|
||||
let pointer_offset_within_window = Point::from((
|
||||
window_size.w * self.pointer_ratio_within_window.0,
|
||||
window_size.h * self.pointer_ratio_within_window.1,
|
||||
));
|
||||
let pos =
|
||||
self.pointer_pos_within_output - pointer_offset_within_window - self.tile.window_loc()
|
||||
+ self.tile.render_offset();
|
||||
let pos = self.pointer_pos_within_output
|
||||
- (pointer_offset_within_window + self.tile.window_loc() - self.tile.render_offset())
|
||||
.upscale(zoom);
|
||||
// Round to physical pixels.
|
||||
pos.to_physical_precise_round(scale).to_logical(scale)
|
||||
}
|
||||
@@ -553,6 +587,15 @@ impl HitType {
|
||||
tile.hit(pos_within_tile)
|
||||
.map(|hit| (tile.window(), hit.offset_win_pos(tile_pos)))
|
||||
}
|
||||
|
||||
pub fn to_activate(self) -> Self {
|
||||
match self {
|
||||
HitType::Input { .. } => HitType::Activate {
|
||||
is_tab_indicator: false,
|
||||
},
|
||||
HitType::Activate { .. } => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Options {
|
||||
@@ -594,6 +637,7 @@ impl Options {
|
||||
default_column_width,
|
||||
animations: config.animations.clone(),
|
||||
gestures: config.gestures,
|
||||
overview: config.overview,
|
||||
disable_resize_throttling: config.debug.disable_resize_throttling,
|
||||
disable_transactions: config.debug.disable_transactions,
|
||||
preset_window_heights,
|
||||
@@ -611,6 +655,19 @@ impl Options {
|
||||
}
|
||||
}
|
||||
|
||||
impl OverviewProgress {
|
||||
fn value(&self) -> f64 {
|
||||
match self {
|
||||
OverviewProgress::Animation(anim) => anim.value(),
|
||||
OverviewProgress::Gesture(gesture) => gesture.value,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_animation(&self) -> bool {
|
||||
matches!(self, OverviewProgress::Animation(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: LayoutElement> Layout<W> {
|
||||
pub fn new(clock: Clock, config: &Config) -> Self {
|
||||
Self::with_options_and_workspaces(clock, config, Options::from_config(config))
|
||||
@@ -625,6 +682,8 @@ impl<W: LayoutElement> Layout<W> {
|
||||
dnd: None,
|
||||
clock,
|
||||
update_render_elements_time: Duration::ZERO,
|
||||
overview_open: false,
|
||||
overview_progress: None,
|
||||
options: Rc::new(options),
|
||||
}
|
||||
}
|
||||
@@ -648,6 +707,8 @@ impl<W: LayoutElement> Layout<W> {
|
||||
dnd: None,
|
||||
clock,
|
||||
update_render_elements_time: Duration::ZERO,
|
||||
overview_open: false,
|
||||
overview_progress: None,
|
||||
options: opts,
|
||||
}
|
||||
}
|
||||
@@ -751,6 +812,8 @@ impl<W: LayoutElement> Layout<W> {
|
||||
let mut monitor =
|
||||
Monitor::new(output, workspaces, self.clock.clone(), self.options.clone());
|
||||
monitor.active_workspace_idx = active_workspace_idx;
|
||||
monitor.overview_open = self.overview_open;
|
||||
monitor.set_overview_progress(self.overview_progress.as_ref());
|
||||
monitors.push(monitor);
|
||||
|
||||
MonitorSet::Normal {
|
||||
@@ -789,6 +852,8 @@ impl<W: LayoutElement> Layout<W> {
|
||||
let mut monitor =
|
||||
Monitor::new(output, workspaces, self.clock.clone(), self.options.clone());
|
||||
monitor.active_workspace_idx = active_workspace_idx;
|
||||
monitor.overview_open = self.overview_open;
|
||||
monitor.set_overview_progress(self.overview_progress.as_ref());
|
||||
|
||||
MonitorSet::Normal {
|
||||
monitors: vec![monitor],
|
||||
@@ -1418,7 +1483,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
let mut target = Rectangle::from_size(Size::from((width, height)));
|
||||
// FIXME: ideally this shouldn't include the tile render offset, but the code
|
||||
// duplication would be a bit annoying for this edge case.
|
||||
target.loc.y -= move_.tile_render_location().y;
|
||||
target.loc.y -= move_.tile_render_location(1.).y;
|
||||
target.loc.y -= move_.tile.window_loc().y;
|
||||
return target;
|
||||
}
|
||||
@@ -2279,8 +2344,19 @@ impl<W: LayoutElement> Layout<W> {
|
||||
) -> Option<(&W, HitType)> {
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
|
||||
if move_.output == *output {
|
||||
let tile_pos = move_.tile_render_location();
|
||||
HitType::hit_tile(&move_.tile, tile_pos, pos_within_output)
|
||||
if self.overview_progress.is_some() {
|
||||
let zoom = self.overview_zoom();
|
||||
let tile_pos = move_.tile_render_location(zoom);
|
||||
let pos_within_tile = (pos_within_output - tile_pos).downscale(zoom);
|
||||
// During the overview animation, we cannot do input hits because we cannot
|
||||
// really represent scaled windows properly.
|
||||
let (win, hit) =
|
||||
HitType::hit_tile(&move_.tile, Point::from((0., 0.)), pos_within_tile)?;
|
||||
Some((win, hit.to_activate()))
|
||||
} else {
|
||||
let tile_pos = move_.tile_render_location(1.);
|
||||
HitType::hit_tile(&move_.tile, tile_pos, pos_within_output)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -2316,6 +2392,36 @@ impl<W: LayoutElement> Layout<W> {
|
||||
mon.resize_edges_under(pos_within_output)
|
||||
}
|
||||
|
||||
pub fn workspace_under(
|
||||
&self,
|
||||
extended_bounds: bool,
|
||||
output: &Output,
|
||||
pos_within_output: Point<f64, Logical>,
|
||||
) -> Option<&Workspace<W>> {
|
||||
if self
|
||||
.interactive_moved_window_under(output, pos_within_output)
|
||||
.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mon = monitors.iter().find(|mon| &mon.output == output)?;
|
||||
if extended_bounds {
|
||||
mon.workspace_under(pos_within_output).map(|(ws, _)| ws)
|
||||
} else {
|
||||
mon.workspace_under_narrow(pos_within_output)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overview_zoom(&self) -> f64 {
|
||||
let progress = self.overview_progress.as_ref().map(|p| p.value());
|
||||
compute_overview_zoom(&self.options, progress)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn verify_invariants(&self) {
|
||||
use std::collections::HashSet;
|
||||
@@ -2324,6 +2430,8 @@ impl<W: LayoutElement> Layout<W> {
|
||||
|
||||
use crate::layout::monitor::WorkspaceSwitch;
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
|
||||
let mut move_win_id = None;
|
||||
if let Some(state) = &self.interactive_move {
|
||||
match state {
|
||||
@@ -2352,7 +2460,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
base options adjusted for output scale"
|
||||
);
|
||||
|
||||
let tile_pos = move_.tile_render_location();
|
||||
let tile_pos = move_.tile_render_location(zoom);
|
||||
let rounded_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale);
|
||||
|
||||
// Tile position must be rounded to physical pixels.
|
||||
@@ -2460,6 +2568,12 @@ impl<W: LayoutElement> Layout<W> {
|
||||
"monitor options must be synchronized with layout"
|
||||
);
|
||||
|
||||
assert_eq!(self.overview_open, monitor.overview_open);
|
||||
assert_eq!(
|
||||
self.overview_progress.as_ref().map(|p| p.value()),
|
||||
monitor.overview_progress_value()
|
||||
);
|
||||
|
||||
if let Some(WorkspaceSwitch::Animation(anim)) = &monitor.workspace_switch {
|
||||
let before_idx = anim.from() as usize;
|
||||
let after_idx = anim.to() as usize;
|
||||
@@ -2650,6 +2764,8 @@ impl<W: LayoutElement> Layout<W> {
|
||||
// Scroll the view if needed.
|
||||
if let Some((output, pos_within_output)) = dnd_scroll {
|
||||
if let Some(mon) = self.monitor_for_output_mut(&output) {
|
||||
let zoom = mon.overview_zoom();
|
||||
|
||||
if let Some((ws, geo)) = mon.workspace_under(pos_within_output) {
|
||||
let ws_id = ws.id();
|
||||
let ws = mon
|
||||
@@ -2657,7 +2773,18 @@ impl<W: LayoutElement> Layout<W> {
|
||||
.iter_mut()
|
||||
.find(|ws| ws.id() == ws_id)
|
||||
.unwrap();
|
||||
ws.dnd_scroll_gesture_scroll(pos_within_output - geo.loc);
|
||||
// As far as the DnD scroll gesture is concerned, the workspace spans across
|
||||
// the whole monitor horizontally.
|
||||
let ws_pos = Point::from((0., geo.loc.y));
|
||||
ws.dnd_scroll_gesture_scroll(pos_within_output - ws_pos, 1. / zoom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !self.overview_open {
|
||||
if let Some(OverviewProgress::Animation(anim)) = &mut self.overview_progress {
|
||||
if anim.is_done() {
|
||||
self.overview_progress = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2665,6 +2792,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
match &mut self.monitor_set {
|
||||
MonitorSet::Normal { monitors, .. } => {
|
||||
for mon in monitors {
|
||||
mon.set_overview_progress(self.overview_progress.as_ref());
|
||||
mon.advance_animations();
|
||||
}
|
||||
}
|
||||
@@ -2697,6 +2825,14 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
if self
|
||||
.overview_progress
|
||||
.as_ref()
|
||||
.is_some_and(|p| p.is_animation())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
let MonitorSet::Normal { monitors, .. } = &self.monitor_set else {
|
||||
return false;
|
||||
};
|
||||
@@ -2719,9 +2855,10 @@ impl<W: LayoutElement> Layout<W> {
|
||||
|
||||
self.update_render_elements_time = self.clock.now();
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
|
||||
if output.map_or(true, |output| move_.output == *output) {
|
||||
let pos_within_output = move_.tile_render_location();
|
||||
let pos_within_output = move_.tile_render_location(zoom);
|
||||
let view_rect =
|
||||
Rectangle::new(pos_within_output.upscale(-1.), output_size(&move_.output));
|
||||
move_.tile.update_render_elements(true, view_rect);
|
||||
@@ -2745,6 +2882,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
let is_active = self.is_active
|
||||
&& idx == *active_monitor_idx
|
||||
&& !matches!(self.interactive_move, Some(InteractiveMoveState::Moving(_)));
|
||||
mon.set_overview_progress(self.overview_progress.as_ref());
|
||||
mon.update_render_elements(is_active);
|
||||
}
|
||||
}
|
||||
@@ -2798,6 +2936,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
let _span = tracy_client::span!("Layout::update_insert_hint::update");
|
||||
|
||||
if let Some(mon) = self.monitor_for_output_mut(&move_.output) {
|
||||
let zoom = mon.overview_zoom();
|
||||
if let Some((ws, geo)) = mon.workspace_under(move_.pointer_pos_within_output) {
|
||||
let ws_id = ws.id();
|
||||
let ws = mon
|
||||
@@ -2805,8 +2944,8 @@ impl<W: LayoutElement> Layout<W> {
|
||||
.iter_mut()
|
||||
.find(|ws| ws.id() == ws_id)
|
||||
.unwrap();
|
||||
|
||||
let pos_within_workspace = move_.pointer_pos_within_output - geo.loc;
|
||||
let pos_within_workspace =
|
||||
(move_.pointer_pos_within_output - geo.loc).downscale(zoom);
|
||||
let position = ws.scrolling_insert_position(pos_within_workspace);
|
||||
|
||||
let rules = move_.tile.window().rules();
|
||||
@@ -3645,6 +3784,9 @@ impl<W: LayoutElement> Layout<W> {
|
||||
timestamp: Duration,
|
||||
is_touchpad: bool,
|
||||
) -> Option<Option<Output>> {
|
||||
let zoom = self.overview_zoom();
|
||||
let delta_x = delta_x / zoom;
|
||||
|
||||
let monitors = match &mut self.monitor_set {
|
||||
MonitorSet::Normal { monitors, .. } => monitors,
|
||||
MonitorSet::NoOutputs { .. } => return None,
|
||||
@@ -3684,6 +3826,77 @@ impl<W: LayoutElement> Layout<W> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn overview_gesture_begin(&mut self) {
|
||||
self.overview_open = true;
|
||||
|
||||
let value = self.overview_progress.take().map_or(0., |p| p.value());
|
||||
let gesture = OverviewGesture {
|
||||
tracker: SwipeTracker::new(),
|
||||
start: value,
|
||||
value,
|
||||
};
|
||||
self.overview_progress = Some(OverviewProgress::Gesture(gesture));
|
||||
|
||||
self.set_monitors_overview_state();
|
||||
}
|
||||
|
||||
pub fn overview_gesture_update(&mut self, delta_y: f64, timestamp: Duration) -> Option<bool> {
|
||||
let Some(OverviewProgress::Gesture(gesture)) = &mut self.overview_progress else {
|
||||
return None;
|
||||
};
|
||||
|
||||
gesture.tracker.push(delta_y, timestamp);
|
||||
|
||||
let total_height = OVERVIEW_GESTURE_MOVEMENT;
|
||||
let pos = gesture.tracker.pos() / total_height;
|
||||
let new_value = gesture.start + pos;
|
||||
let new_value = OVERVIEW_GESTURE_RUBBER_BAND.clamp(0., 1., new_value);
|
||||
|
||||
if gesture.value == new_value {
|
||||
return Some(false);
|
||||
}
|
||||
|
||||
gesture.value = new_value;
|
||||
self.set_monitors_overview_state();
|
||||
|
||||
Some(true)
|
||||
}
|
||||
|
||||
pub fn overview_gesture_end(&mut self) -> bool {
|
||||
let Some(OverviewProgress::Gesture(gesture)) = &mut self.overview_progress else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Take into account any idle time between the last event and now.
|
||||
let now = self.clock.now_unadjusted();
|
||||
gesture.tracker.push(0., now);
|
||||
|
||||
let total_height = OVERVIEW_GESTURE_MOVEMENT;
|
||||
|
||||
let mut velocity = gesture.tracker.velocity() / total_height;
|
||||
let current_pos = gesture.tracker.pos() / total_height;
|
||||
let pos = gesture.tracker.projected_end_pos() / total_height;
|
||||
|
||||
let new_value = gesture.start + pos;
|
||||
let new_value = new_value.clamp(0., 1.).round();
|
||||
|
||||
velocity *=
|
||||
OVERVIEW_GESTURE_RUBBER_BAND.clamp_derivative(0., 1., gesture.start + current_pos);
|
||||
|
||||
self.overview_open = new_value == 1.;
|
||||
self.overview_progress = Some(OverviewProgress::Animation(Animation::new(
|
||||
self.clock.clone(),
|
||||
gesture.value,
|
||||
new_value,
|
||||
velocity,
|
||||
self.options.animations.overview_open_close.0,
|
||||
)));
|
||||
|
||||
self.set_monitors_overview_state();
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn interactive_move_begin(
|
||||
&mut self,
|
||||
window_id: W::Id,
|
||||
@@ -3710,6 +3923,8 @@ impl<W: LayoutElement> Layout<W> {
|
||||
return false;
|
||||
}
|
||||
|
||||
let zoom = mon.overview_zoom();
|
||||
|
||||
let is_floating = ws.is_floating(&window_id);
|
||||
let (tile, tile_offset, _visible) = ws
|
||||
.tiles_with_render_positions()
|
||||
@@ -3717,10 +3932,11 @@ impl<W: LayoutElement> Layout<W> {
|
||||
.unwrap();
|
||||
let window_offset = tile.window_loc();
|
||||
|
||||
let tile_pos = ws_geo.loc + tile_offset;
|
||||
let tile_pos = ws_geo.loc + tile_offset.upscale(zoom);
|
||||
|
||||
let pointer_offset_within_window = start_pos_within_output - tile_pos - window_offset;
|
||||
let window_size = tile.window_size();
|
||||
let pointer_offset_within_window =
|
||||
start_pos_within_output - tile_pos - window_offset.upscale(zoom);
|
||||
let window_size = tile.window_size().upscale(zoom);
|
||||
let pointer_ratio_within_window = (
|
||||
f64::clamp(pointer_offset_within_window.x / window_size.w, 0., 1.),
|
||||
f64::clamp(pointer_offset_within_window.y / window_size.h, 0., 1.),
|
||||
@@ -3768,6 +3984,9 @@ impl<W: LayoutElement> Layout<W> {
|
||||
return false;
|
||||
}
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
let delta = delta.downscale(zoom);
|
||||
|
||||
pointer_delta += delta;
|
||||
|
||||
let (cx, cy) = (pointer_delta.x, pointer_delta.y);
|
||||
@@ -3824,7 +4043,8 @@ impl<W: LayoutElement> Layout<W> {
|
||||
.find(|(tile, _, _)| tile.window().id() == window)
|
||||
.unwrap();
|
||||
|
||||
tile_pos = Some(ws_geo.loc + tile_offset);
|
||||
let zoom = mon.overview_zoom();
|
||||
tile_pos = Some((ws_geo.loc + tile_offset.upscale(zoom), zoom));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3903,9 +4123,10 @@ impl<W: LayoutElement> Layout<W> {
|
||||
pointer_ratio_within_window,
|
||||
};
|
||||
|
||||
if let Some(tile_pos) = tile_pos {
|
||||
let new_tile_pos = data.tile_render_location();
|
||||
data.tile.animate_move_from(tile_pos - new_tile_pos);
|
||||
if let Some((tile_pos, zoom)) = tile_pos {
|
||||
let new_tile_pos = data.tile_render_location(zoom);
|
||||
data.tile
|
||||
.animate_move_from((tile_pos - new_tile_pos).downscale(zoom));
|
||||
}
|
||||
|
||||
self.interactive_move = Some(InteractiveMoveState::Moving(data));
|
||||
@@ -3960,12 +4181,16 @@ impl<W: LayoutElement> Layout<W> {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let mut ws_id = None;
|
||||
for ws in self.workspaces_mut() {
|
||||
let id = ws.id();
|
||||
if let Some(tile) = ws.tiles_mut().find(|tile| *tile.window().id() == window_id)
|
||||
{
|
||||
let offset = tile.interactive_move_offset;
|
||||
tile.interactive_move_offset = Point::from((0., 0.));
|
||||
tile.animate_move_from(offset);
|
||||
|
||||
ws_id = Some(id);
|
||||
}
|
||||
|
||||
// Unlock the view on the workspaces, but if the moved window was active,
|
||||
@@ -3980,6 +4205,32 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
}
|
||||
|
||||
// In the overview, we want to click on a window to focus it, and also to
|
||||
// click-and-drag to move the window. The way we handle this is by always starting
|
||||
// the interactive move (to get frozen view), then, when in the overview, *not*
|
||||
// calling interactive_move_update() until the cursor moves far enough. This means
|
||||
// that if we "just click" then we end up in this branch with state == Starting.
|
||||
// Close the overview in this case.
|
||||
if self.overview_open {
|
||||
let ws_id = ws_id.unwrap();
|
||||
if let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set {
|
||||
for mon in monitors {
|
||||
if let Some(ws_idx) =
|
||||
mon.workspaces.iter().position(|ws| ws.id() == ws_id)
|
||||
{
|
||||
mon.activate_workspace_with_anim_config(
|
||||
ws_idx,
|
||||
Some(self.options.animations.overview_open_close.0),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.activate_window(&window_id);
|
||||
self.close_overview();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
InteractiveMoveState::Moving(move_) => move_,
|
||||
@@ -4007,20 +4258,28 @@ impl<W: LayoutElement> Layout<W> {
|
||||
);
|
||||
}
|
||||
|
||||
// Dragging in the overview shouldn't switch the workspace and so on.
|
||||
let activate = if self.overview_open {
|
||||
ActivateWindow::No
|
||||
} else {
|
||||
ActivateWindow::Yes
|
||||
};
|
||||
|
||||
match &mut self.monitor_set {
|
||||
MonitorSet::Normal {
|
||||
monitors,
|
||||
active_monitor_idx,
|
||||
..
|
||||
} => {
|
||||
let (mon, ws_idx, position, offset) = if let Some(mon) =
|
||||
let (mon, ws_idx, position, offset, zoom) = if let Some(mon) =
|
||||
monitors.iter_mut().find(|mon| mon.output == move_.output)
|
||||
{
|
||||
let zoom = mon.overview_zoom();
|
||||
|
||||
let (ws, ws_geo) = mon
|
||||
.workspace_under(move_.pointer_pos_within_output)
|
||||
// If the pointer is somehow outside the move output and a workspace switch
|
||||
// is in progress, this won't necessarily do the expected thing, but also
|
||||
// that is not really supposed to happen so eh?
|
||||
// is in progress, this won't necessarily do the expected thing.
|
||||
.unwrap_or_else(|| mon.workspaces_with_render_geo().next().unwrap());
|
||||
|
||||
let ws_id = ws.id();
|
||||
@@ -4033,14 +4292,16 @@ impl<W: LayoutElement> Layout<W> {
|
||||
let position = if move_.is_floating {
|
||||
InsertPosition::Floating
|
||||
} else {
|
||||
let pos_within_workspace = move_.pointer_pos_within_output - ws_geo.loc;
|
||||
let pos_within_workspace =
|
||||
(move_.pointer_pos_within_output - ws_geo.loc).downscale(zoom);
|
||||
let ws = &mut mon.workspaces[ws_idx];
|
||||
ws.scrolling_insert_position(pos_within_workspace)
|
||||
};
|
||||
|
||||
(mon, ws_idx, position, ws_geo.loc)
|
||||
(mon, ws_idx, position, ws_geo.loc, zoom)
|
||||
} else {
|
||||
let mon = &mut monitors[*active_monitor_idx];
|
||||
let zoom = mon.overview_zoom();
|
||||
// No point in trying to use the pointer position on the wrong output.
|
||||
let (ws, ws_geo) = mon.workspaces_with_render_geo().next().unwrap();
|
||||
|
||||
@@ -4056,11 +4317,12 @@ impl<W: LayoutElement> Layout<W> {
|
||||
.iter_mut()
|
||||
.position(|ws| ws.id() == ws_id)
|
||||
.unwrap();
|
||||
(mon, ws_idx, position, ws_geo.loc)
|
||||
|
||||
(mon, ws_idx, position, ws_geo.loc, zoom)
|
||||
};
|
||||
|
||||
let win_id = move_.tile.window().id().clone();
|
||||
let window_render_loc = move_.tile_render_location() + move_.tile.window_loc();
|
||||
let window_render_loc = move_.tile_render_location(zoom) + move_.tile.window_loc();
|
||||
|
||||
match position {
|
||||
InsertPosition::NewColumn(column_idx) => {
|
||||
@@ -4071,7 +4333,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
id: ws_id,
|
||||
column_idx: Some(column_idx),
|
||||
},
|
||||
ActivateWindow::Yes,
|
||||
activate,
|
||||
move_.width,
|
||||
move_.is_full_width,
|
||||
false,
|
||||
@@ -4083,13 +4345,15 @@ impl<W: LayoutElement> Layout<W> {
|
||||
column_idx,
|
||||
Some(tile_idx),
|
||||
move_.tile,
|
||||
true,
|
||||
activate == ActivateWindow::Yes,
|
||||
);
|
||||
}
|
||||
InsertPosition::Floating => {
|
||||
let pos = move_.tile_render_location() - offset;
|
||||
let tile_render_loc = move_.tile_render_location(zoom);
|
||||
|
||||
let mut tile = move_.tile;
|
||||
|
||||
let pos = (tile_render_loc - offset).downscale(zoom);
|
||||
let pos = mon.workspaces[ws_idx].floating_logical_to_size_frac(pos);
|
||||
tile.floating_pos = Some(pos);
|
||||
|
||||
@@ -4106,7 +4370,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
id: ws_id,
|
||||
column_idx: None,
|
||||
},
|
||||
ActivateWindow::Yes,
|
||||
activate,
|
||||
move_.width,
|
||||
move_.is_full_width,
|
||||
true,
|
||||
@@ -4115,15 +4379,18 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
|
||||
// needed because empty_workspace_above_first could have modified the idx
|
||||
let ws_idx = mon.active_workspace_idx();
|
||||
let ws = &mut mon.workspaces[ws_idx];
|
||||
let (tile, tile_render_loc) = ws
|
||||
.tiles_with_render_positions_mut(false)
|
||||
.find(|(tile, _)| tile.window().id() == &win_id)
|
||||
let (tile, tile_render_loc, ws_geo) = mon
|
||||
.workspaces_with_render_geo_mut()
|
||||
.find_map(|(ws, geo)| {
|
||||
ws.tiles_with_render_positions_mut(false)
|
||||
.find(|(tile, _)| tile.window().id() == &win_id)
|
||||
.map(|(tile, tile_render_loc)| (tile, tile_render_loc, geo))
|
||||
})
|
||||
.unwrap();
|
||||
let new_window_render_loc = offset + tile_render_loc + tile.window_loc();
|
||||
let new_window_render_loc =
|
||||
ws_geo.loc + (tile_render_loc + tile.window_loc()).upscale(zoom);
|
||||
|
||||
tile.animate_move_from(window_render_loc - new_window_render_loc);
|
||||
tile.animate_move_from((window_render_loc - new_window_render_loc).downscale(zoom));
|
||||
}
|
||||
MonitorSet::NoOutputs { workspaces, .. } => {
|
||||
if workspaces.is_empty() {
|
||||
@@ -4138,7 +4405,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
ws.add_tile(
|
||||
move_.tile,
|
||||
WorkspaceAddWindowTarget::Auto,
|
||||
ActivateWindow::Yes,
|
||||
activate,
|
||||
move_.width,
|
||||
move_.is_full_width,
|
||||
move_.is_floating,
|
||||
@@ -4376,6 +4643,60 @@ impl<W: LayoutElement> Layout<W> {
|
||||
self.unname_workspace_by_id(id);
|
||||
}
|
||||
|
||||
pub fn set_monitors_overview_state(&mut self) {
|
||||
let MonitorSet::Normal { monitors, .. } = &mut self.monitor_set else {
|
||||
return;
|
||||
};
|
||||
|
||||
for mon in monitors {
|
||||
mon.overview_open = self.overview_open;
|
||||
mon.set_overview_progress(self.overview_progress.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_overview(&mut self) {
|
||||
self.overview_open = !self.overview_open;
|
||||
|
||||
let from = self.overview_progress.take().map_or(0., |p| p.value());
|
||||
let to = if self.overview_open { 1. } else { 0. };
|
||||
|
||||
self.overview_progress = Some(OverviewProgress::Animation(Animation::new(
|
||||
self.clock.clone(),
|
||||
from,
|
||||
to,
|
||||
0.,
|
||||
self.options.animations.overview_open_close.0,
|
||||
)));
|
||||
|
||||
self.set_monitors_overview_state();
|
||||
}
|
||||
|
||||
pub fn open_overview(&mut self) -> bool {
|
||||
if self.overview_open {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.toggle_overview();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn close_overview(&mut self) -> bool {
|
||||
if !self.overview_open {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.toggle_overview();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn toggle_overview_to_workspace(&mut self, ws_idx: usize) {
|
||||
let config = self.options.animations.overview_open_close.0;
|
||||
if let Some(mon) = self.active_monitor() {
|
||||
mon.activate_workspace_with_anim_config(ws_idx, Some(config));
|
||||
}
|
||||
self.toggle_overview();
|
||||
}
|
||||
|
||||
pub fn start_open_animation_for_window(&mut self, window: &W::Id) {
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
|
||||
if move_.tile.window().id() == window {
|
||||
@@ -4460,12 +4781,14 @@ impl<W: LayoutElement> Layout<W> {
|
||||
) {
|
||||
let _span = tracy_client::span!("Layout::start_close_animation_for_window");
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &mut self.interactive_move {
|
||||
if move_.tile.window().id() == window {
|
||||
let Some(snapshot) = move_.tile.take_unmap_snapshot() else {
|
||||
return;
|
||||
};
|
||||
let tile_pos = move_.tile_render_location();
|
||||
let tile_pos = move_.tile_render_location(zoom);
|
||||
let tile_size = move_.tile.tile_size();
|
||||
|
||||
let output = move_.output.clone();
|
||||
@@ -4516,7 +4839,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
renderer: &mut R,
|
||||
output: &Output,
|
||||
target: RenderTarget,
|
||||
) -> impl Iterator<Item = TileRenderElement<R>> + 'a {
|
||||
) -> impl Iterator<Item = RescaleRenderElement<TileRenderElement<R>>> + 'a {
|
||||
if self.update_render_elements_time != self.clock.now() {
|
||||
error!("clock moved between updating render elements and rendering");
|
||||
}
|
||||
@@ -4525,8 +4848,20 @@ impl<W: LayoutElement> Layout<W> {
|
||||
|
||||
if let Some(InteractiveMoveState::Moving(move_)) = &self.interactive_move {
|
||||
if &move_.output == output {
|
||||
let location = move_.tile_render_location();
|
||||
rv = Some(move_.tile.render(renderer, location, true, target));
|
||||
let scale = Scale::from(move_.output.current_scale().fractional_scale());
|
||||
let zoom = self.overview_zoom();
|
||||
let location = move_.tile_render_location(zoom);
|
||||
let iter = move_
|
||||
.tile
|
||||
.render(renderer, location, true, target)
|
||||
.map(move |elem| {
|
||||
RescaleRenderElement::from_element(
|
||||
elem,
|
||||
location.to_physical_precise_round(scale),
|
||||
zoom,
|
||||
)
|
||||
});
|
||||
rv = Some(iter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4589,7 +4924,7 @@ impl<W: LayoutElement> Layout<W> {
|
||||
}
|
||||
} else {
|
||||
// Cancel the view offset gesture after workspace switches, moves, etc.
|
||||
if ws_idx != mon.active_workspace_idx {
|
||||
if !self.overview_open && ws_idx != mon.active_workspace_idx {
|
||||
ws.view_offset_gesture_end(None);
|
||||
}
|
||||
}
|
||||
@@ -4684,6 +5019,10 @@ impl<W: LayoutElement> Layout<W> {
|
||||
self.windows().any(|(_, win)| win.id() == window)
|
||||
}
|
||||
|
||||
pub fn is_overview_open(&self) -> bool {
|
||||
self.overview_open
|
||||
}
|
||||
|
||||
fn resolve_scrolling_width(&self, window: &W, width: Option<PresetSize>) -> ColumnWidth {
|
||||
let width = width.unwrap_or_else(|| PresetSize::Fixed(window.size().w));
|
||||
match width {
|
||||
@@ -4709,3 +5048,14 @@ impl<W: LayoutElement> Default for MonitorSet<W> {
|
||||
Self::NoOutputs { workspaces: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_overview_zoom(options: &Options, overview_progress: Option<f64>) -> f64 {
|
||||
// Clamp to some sane values.
|
||||
let zoom = options.overview.zoom.0.clamp(0.0001, 0.75);
|
||||
|
||||
if let Some(p) = overview_progress {
|
||||
(1. - p * (1. - zoom)).max(0.0001)
|
||||
} else {
|
||||
1.
|
||||
}
|
||||
}
|
||||
|
||||
+216
-20
@@ -5,7 +5,7 @@ use std::time::Duration;
|
||||
|
||||
use niri_config::CornerRadius;
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
CropRenderElement, Relocate, RelocateRenderElement,
|
||||
CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement,
|
||||
};
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
@@ -17,11 +17,12 @@ use super::workspace::{
|
||||
compute_working_area, OutputId, Workspace, WorkspaceAddWindowTarget, WorkspaceId,
|
||||
WorkspaceRenderElement,
|
||||
};
|
||||
use super::{ActivateWindow, HitType, LayoutElement, Options};
|
||||
use super::{compute_overview_zoom, ActivateWindow, HitType, LayoutElement, Options};
|
||||
use crate::animation::{Animation, Clock};
|
||||
use crate::input::swipe_tracker::SwipeTracker;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||
use crate::render_helpers::RenderTarget;
|
||||
use crate::rubber_band::RubberBand;
|
||||
use crate::utils::transaction::Transaction;
|
||||
@@ -68,6 +69,10 @@ pub struct Monitor<W: LayoutElement> {
|
||||
insert_hint_element: InsertHintElement,
|
||||
/// Location to render the insert hint element.
|
||||
insert_hint_render_loc: Option<InsertHintRenderLoc>,
|
||||
/// Whether the overview is open.
|
||||
pub(super) overview_open: bool,
|
||||
/// Progress of the overview zoom animation, 1 is fully in overview.
|
||||
overview_progress: Option<OverviewProgress>,
|
||||
/// Clock for driving animations.
|
||||
pub(super) clock: Clock,
|
||||
/// Configurable properties of the layout.
|
||||
@@ -94,6 +99,8 @@ pub struct WorkspaceSwitchGesture {
|
||||
tracker: SwipeTracker,
|
||||
/// Whether the gesture is controlled by the touchpad.
|
||||
is_touchpad: bool,
|
||||
/// Whether the gesture is clamped to +-1 workspace around the center.
|
||||
is_clamped: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -116,6 +123,12 @@ struct InsertHintRenderLoc {
|
||||
location: Point<f64, Logical>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) enum OverviewProgress {
|
||||
Animation(Animation),
|
||||
Value(f64),
|
||||
}
|
||||
|
||||
/// Where to put a newly added window.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MonitorAddWindowTarget<'a, W: LayoutElement> {
|
||||
@@ -137,10 +150,12 @@ niri_render_elements! {
|
||||
MonitorInnerRenderElement<R> => {
|
||||
Workspace = CropRenderElement<WorkspaceRenderElement<R>>,
|
||||
InsertHint = CropRenderElement<InsertHintRenderElement>,
|
||||
Shadow = ShadowRenderElement,
|
||||
}
|
||||
}
|
||||
|
||||
pub type MonitorRenderElement<R> = RelocateRenderElement<MonitorInnerRenderElement<R>>;
|
||||
pub type MonitorRenderElement<R> =
|
||||
RelocateRenderElement<RescaleRenderElement<MonitorInnerRenderElement<R>>>;
|
||||
|
||||
impl WorkspaceSwitch {
|
||||
pub fn current_idx(&self) -> f64 {
|
||||
@@ -183,9 +198,38 @@ impl WorkspaceSwitch {
|
||||
|
||||
impl WorkspaceSwitchGesture {
|
||||
fn min_max(&self, workspace_count: usize) -> (f64, f64) {
|
||||
let min = self.center_idx.saturating_sub(1) as f64;
|
||||
let max = (self.center_idx + 1).min(workspace_count - 1) as f64;
|
||||
(min, max)
|
||||
if self.is_clamped {
|
||||
let min = self.center_idx.saturating_sub(1) as f64;
|
||||
let max = (self.center_idx + 1).min(workspace_count - 1) as f64;
|
||||
(min, max)
|
||||
} else {
|
||||
(0., (workspace_count - 1) as f64)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OverviewProgress {
|
||||
pub fn value(&self) -> f64 {
|
||||
match self {
|
||||
OverviewProgress::Animation(anim) => anim.value(),
|
||||
OverviewProgress::Value(v) => *v,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clamped_value(&self) -> f64 {
|
||||
match self {
|
||||
OverviewProgress::Animation(anim) => anim.clamped_value(),
|
||||
OverviewProgress::Value(v) => *v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&super::OverviewProgress> for OverviewProgress {
|
||||
fn from(value: &super::OverviewProgress) -> Self {
|
||||
match value {
|
||||
super::OverviewProgress::Animation(anim) => Self::Animation(anim.clone()),
|
||||
super::OverviewProgress::Gesture(gesture) => Self::Value(gesture.value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +256,8 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
insert_hint: None,
|
||||
insert_hint_element: InsertHintElement::new(options.insert_hint),
|
||||
insert_hint_render_loc: None,
|
||||
overview_open: false,
|
||||
overview_progress: None,
|
||||
workspace_switch: None,
|
||||
clock,
|
||||
options,
|
||||
@@ -769,7 +815,9 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
|
||||
// Make sure the hint is at least partially visible.
|
||||
if matches!(hint.position, InsertPosition::NewColumn(_)) {
|
||||
let zoom = self.overview_zoom();
|
||||
let geo = insert_hint_ws_geo.unwrap();
|
||||
let geo = geo.downscale(zoom);
|
||||
|
||||
area.loc.x = area.loc.x.max(-geo.loc.x - area.size.w / 2.);
|
||||
area.loc.x = area.loc.x.min(geo.loc.x + geo.size.w - area.size.w / 2.);
|
||||
@@ -942,6 +990,10 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
///
|
||||
/// During animations, assumes the final view position.
|
||||
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
|
||||
if self.overview_open {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.active_workspace_ref().active_tile_visual_rectangle()
|
||||
}
|
||||
|
||||
@@ -962,7 +1014,100 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
self.workspace_size(zoom) + Size::from((0., gap))
|
||||
}
|
||||
|
||||
pub fn overview_zoom(&self) -> f64 {
|
||||
let progress = self.overview_progress.as_ref().map(|p| p.value());
|
||||
compute_overview_zoom(&self.options, progress)
|
||||
}
|
||||
|
||||
pub(super) fn set_overview_progress(&mut self, progress: Option<&super::OverviewProgress>) {
|
||||
let prev_render_idx = self.workspace_render_idx();
|
||||
self.overview_progress = progress.map(OverviewProgress::from);
|
||||
let new_render_idx = self.workspace_render_idx();
|
||||
|
||||
// If the view jumped (can happen when going from corrected to uncorrected render_idx, for
|
||||
// example when toggling the overview in the middle of an overview animation), then restart
|
||||
// the workspace switch to avoid jumps.
|
||||
if prev_render_idx != new_render_idx {
|
||||
if let Some(WorkspaceSwitch::Animation(anim)) = &mut self.workspace_switch {
|
||||
// FIXME: maintain velocity.
|
||||
*anim = anim.restarted(prev_render_idx, anim.to(), 0.);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn overview_progress_value(&self) -> Option<f64> {
|
||||
self.overview_progress.as_ref().map(|p| p.value())
|
||||
}
|
||||
|
||||
pub fn workspace_render_idx(&self) -> f64 {
|
||||
// If workspace switch and overview progress are matching animations, then compute a
|
||||
// correction term to make the movement appear monotonic.
|
||||
if let (
|
||||
Some(WorkspaceSwitch::Animation(switch_anim)),
|
||||
Some(OverviewProgress::Animation(progress_anim)),
|
||||
) = (&self.workspace_switch, &self.overview_progress)
|
||||
{
|
||||
if switch_anim.start_time() == progress_anim.start_time()
|
||||
&& (switch_anim.duration().as_secs_f64() - progress_anim.duration().as_secs_f64())
|
||||
.abs()
|
||||
<= 0.001
|
||||
{
|
||||
#[rustfmt::skip]
|
||||
// How this was derived:
|
||||
//
|
||||
// - Assume we're animating a zoom + switch. Consider switch "from" and "to".
|
||||
// These are render_idx values, so first workspace to second would have switch
|
||||
// from = 0. and to = 1. regardless of the zoom level.
|
||||
//
|
||||
// - At the start, the point at "from" is at Y = 0. We're moving the point at "to"
|
||||
// to Y = 0. We want this to be a monotonic motion in apparent coordinates (after
|
||||
// zoom).
|
||||
//
|
||||
// - Height at the start:
|
||||
// from_height = (size.h + gap) * from_zoom.
|
||||
//
|
||||
// - Current height:
|
||||
// current_height = (size.h + gap) * zoom.
|
||||
//
|
||||
// - We're moving the "to" point to Y = 0:
|
||||
// to_y = 0.
|
||||
//
|
||||
// - The initial position of the point we're moving:
|
||||
// from_y = (to - from) * from_height.
|
||||
//
|
||||
// - We want this point to travel monotonically in apparent coordinates:
|
||||
// current_y = from_y + (to_y - from_y) * progress,
|
||||
// where progress is from 0 to 1, equals to the animation progress (switch and
|
||||
// zoom are the same since they are synchronized).
|
||||
//
|
||||
// - Derive the Y of the first workspace from this:
|
||||
// first_y = current_y - to * current_height.
|
||||
//
|
||||
// Now, let's substitute and rearrange the terms.
|
||||
//
|
||||
// - current_y = from_y + (0 - (to - from) * from_height) * progress
|
||||
// - progress = (switch_anim.value() - from) / (to - from)
|
||||
// - current_y = from_y - (to - from) * from_height * (switch_anim.value() - from) / (to - from)
|
||||
// - current_y = from_y - from_height * (switch_anim.value() - from)
|
||||
// - first_y = from_y - from_height * (switch_anim.value() - from) - to * current_height
|
||||
// - first_y = (to - from) * from_height - from_height * (switch_anim.value() - from) - to * current_height
|
||||
// - first_y = to * from_height - switch_anim.value() * from_height - to * current_height
|
||||
// - first_y = -switch_anim.value() * from_height + to * (from_height - current_height)
|
||||
let from = progress_anim.from();
|
||||
let from_zoom = compute_overview_zoom(&self.options, Some(from));
|
||||
let from_ws_height_with_gap = self.workspace_size_with_gap(from_zoom).h;
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
let ws_height_with_gap = self.workspace_size_with_gap(zoom).h;
|
||||
|
||||
let first_ws_y = -switch_anim.value() * from_ws_height_with_gap
|
||||
+ switch_anim.to() * (from_ws_height_with_gap - ws_height_with_gap);
|
||||
|
||||
return -first_ws_y / ws_height_with_gap;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(switch) = &self.workspace_switch {
|
||||
switch.current_idx()
|
||||
} else {
|
||||
@@ -972,19 +1117,24 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
|
||||
pub fn workspaces_render_geo(&self) -> impl Iterator<Item = Rectangle<f64, Logical>> {
|
||||
let scale = self.scale.fractional_scale();
|
||||
let zoom = 1.;
|
||||
let zoom = self.overview_zoom();
|
||||
|
||||
let ws_size = self.workspace_size(zoom);
|
||||
let gap = self.workspace_gap(zoom);
|
||||
let ws_height_with_gap = ws_size.h + gap;
|
||||
|
||||
let static_offset = (self.view_size.to_point() - ws_size.to_point()).downscale(2.);
|
||||
let static_offset = static_offset
|
||||
.to_physical_precise_round(scale)
|
||||
.to_logical(scale);
|
||||
|
||||
let first_ws_y = -self.workspace_render_idx() * ws_height_with_gap;
|
||||
let first_ws_y = round_logical_in_physical(scale, first_ws_y);
|
||||
|
||||
// Return position for one-past-last workspace too.
|
||||
(0..=self.workspaces.len()).map(move |idx| {
|
||||
let y = first_ws_y + idx as f64 * ws_height_with_gap;
|
||||
let loc = Point::from((0., y));
|
||||
let loc = Point::from((0., y)) + static_offset;
|
||||
Rectangle::new(loc, ws_size)
|
||||
})
|
||||
}
|
||||
@@ -1026,20 +1176,42 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
Some((ws, geo))
|
||||
}
|
||||
|
||||
pub fn workspace_under_narrow(
|
||||
&self,
|
||||
pos_within_output: Point<f64, Logical>,
|
||||
) -> Option<&Workspace<W>> {
|
||||
self.workspaces_with_render_geo()
|
||||
.find_map(|(ws, geo)| geo.contains(pos_within_output).then_some(ws))
|
||||
}
|
||||
|
||||
pub fn window_under(&self, pos_within_output: Point<f64, Logical>) -> Option<(&W, HitType)> {
|
||||
let (ws, geo) = self.workspace_under(pos_within_output)?;
|
||||
let (win, hit) = ws.window_under(pos_within_output - geo.loc)?;
|
||||
Some((win, hit.offset_win_pos(geo.loc)))
|
||||
|
||||
if self.overview_progress.is_some() {
|
||||
let zoom = self.overview_zoom();
|
||||
let pos_within_workspace = (pos_within_output - geo.loc).downscale(zoom);
|
||||
let (win, hit) = ws.window_under(pos_within_workspace)?;
|
||||
// During the overview animation, we cannot do input hits because we cannot really
|
||||
// represent scaled windows properly.
|
||||
Some((win, hit.to_activate()))
|
||||
} else {
|
||||
let (win, hit) = ws.window_under(pos_within_output - geo.loc)?;
|
||||
Some((win, hit.offset_win_pos(geo.loc)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize_edges_under(&self, pos_within_output: Point<f64, Logical>) -> Option<ResizeEdge> {
|
||||
if self.overview_progress.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (ws, geo) = self.workspace_under(pos_within_output)?;
|
||||
ws.resize_edges_under(pos_within_output - geo.loc)
|
||||
}
|
||||
|
||||
pub fn render_above_top_layer(&self) -> bool {
|
||||
// Render above the top layer only if the view is stationary.
|
||||
if self.workspace_switch.is_some() {
|
||||
if self.workspace_switch.is_some() || self.overview_progress.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1074,7 +1246,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
// rendering for maximized GTK windows.
|
||||
//
|
||||
// FIXME: use proper bounds after fixing the Crop element.
|
||||
let crop_bounds = if self.workspace_switch.is_some() {
|
||||
let crop_bounds = if self.workspace_switch.is_some() || self.overview_progress.is_some() {
|
||||
Rectangle::new(
|
||||
Point::from((-i32::MAX / 2, 0)),
|
||||
Size::from((i32::MAX, height)),
|
||||
@@ -1086,6 +1258,9 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
)
|
||||
};
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
let overview_clamped_progress = self.overview_progress.as_ref().map(|p| p.clamped_value());
|
||||
|
||||
// Draw the insert hint.
|
||||
let mut insert_hint = None;
|
||||
if !self.options.insert_hint.off {
|
||||
@@ -1109,6 +1284,13 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
let floating = floating.filter_map(map_ws_contents);
|
||||
let scrolling = scrolling.filter_map(map_ws_contents);
|
||||
|
||||
let shadow = overview_clamped_progress.map(|value| {
|
||||
ws.render_shadow(renderer)
|
||||
.map(move |elem| elem.with_alpha(value.clamp(0., 1.) as f32))
|
||||
.map(MonitorInnerRenderElement::Shadow)
|
||||
});
|
||||
let shadow = shadow.into_iter().flatten();
|
||||
|
||||
let hint = if matches!(insert_hint, Some((hint_ws_id, _)) if hint_ws_id == ws.id()) {
|
||||
let iter = insert_hint.take().unwrap().1;
|
||||
let iter = iter.filter_map(move |elem| {
|
||||
@@ -1122,9 +1304,10 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
};
|
||||
let hint = hint.into_iter().flatten();
|
||||
|
||||
let iter = floating.chain(hint).chain(scrolling);
|
||||
let iter = floating.chain(hint).chain(scrolling).chain(shadow);
|
||||
|
||||
let iter = iter.map(move |elem| {
|
||||
let elem = RescaleRenderElement::from_element(elem, Point::from((0, 0)), zoom);
|
||||
RelocateRenderElement::from_element(
|
||||
elem,
|
||||
// The offset we get from workspaces_with_render_positions() is already
|
||||
@@ -1149,6 +1332,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
current_idx,
|
||||
tracker: SwipeTracker::new(),
|
||||
is_touchpad,
|
||||
is_clamped: !self.overview_open,
|
||||
};
|
||||
self.workspace_switch = Some(WorkspaceSwitch::Gesture(gesture));
|
||||
}
|
||||
@@ -1167,6 +1351,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
let total_height = if gesture.is_touchpad {
|
||||
WORKSPACE_GESTURE_MOVEMENT
|
||||
} else {
|
||||
@@ -1177,13 +1362,24 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Reduce the effect of zoom on the touchpad somewhat.
|
||||
let delta_scale = if gesture.is_touchpad {
|
||||
(zoom - 1.) / 2.5 + 1.
|
||||
} else {
|
||||
zoom
|
||||
};
|
||||
|
||||
let delta_y = delta_y / delta_scale;
|
||||
let mut rubber_band = WORKSPACE_GESTURE_RUBBER_BAND;
|
||||
rubber_band.limit /= zoom;
|
||||
|
||||
gesture.tracker.push(delta_y, timestamp);
|
||||
|
||||
let pos = gesture.tracker.pos() / total_height;
|
||||
|
||||
let (min, max) = gesture.min_max(self.workspaces.len());
|
||||
let new_idx = gesture.start_idx + pos;
|
||||
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
|
||||
let new_idx = rubber_band.clamp(min, max, new_idx);
|
||||
|
||||
if gesture.current_idx == new_idx {
|
||||
return Some(false);
|
||||
@@ -1202,6 +1398,7 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
return false;
|
||||
}
|
||||
|
||||
let zoom = self.overview_zoom();
|
||||
let total_height = if gesture.is_touchpad {
|
||||
WORKSPACE_GESTURE_MOVEMENT
|
||||
} else {
|
||||
@@ -1216,6 +1413,9 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
let now = self.clock.now_unadjusted();
|
||||
gesture.tracker.push(0., now);
|
||||
|
||||
let mut rubber_band = WORKSPACE_GESTURE_RUBBER_BAND;
|
||||
rubber_band.limit /= zoom;
|
||||
|
||||
let mut velocity = gesture.tracker.velocity() / total_height;
|
||||
let current_pos = gesture.tracker.pos() / total_height;
|
||||
let pos = gesture.tracker.projected_end_pos() / total_height;
|
||||
@@ -1223,14 +1423,10 @@ impl<W: LayoutElement> Monitor<W> {
|
||||
let (min, max) = gesture.min_max(self.workspaces.len());
|
||||
let new_idx = gesture.start_idx + pos;
|
||||
|
||||
let new_idx = WORKSPACE_GESTURE_RUBBER_BAND.clamp(min, max, new_idx);
|
||||
let new_idx = new_idx.clamp(min, max);
|
||||
let new_idx = new_idx.round() as usize;
|
||||
|
||||
velocity *= WORKSPACE_GESTURE_RUBBER_BAND.clamp_derivative(
|
||||
min,
|
||||
max,
|
||||
gesture.start_idx + current_pos,
|
||||
);
|
||||
velocity *= rubber_band.clamp_derivative(min, max, gesture.start_idx + current_pos);
|
||||
|
||||
self.previous_workspace_id = Some(self.workspaces[self.active_workspace_idx].id());
|
||||
|
||||
|
||||
@@ -603,6 +603,13 @@ enum Op {
|
||||
WorkspaceSwitchGestureEnd {
|
||||
is_touchpad: Option<bool>,
|
||||
},
|
||||
OverviewGestureBegin,
|
||||
OverviewGestureUpdate {
|
||||
#[proptest(strategy = "-400f64..400f64")]
|
||||
delta: f64,
|
||||
timestamp: Duration,
|
||||
},
|
||||
OverviewGestureEnd,
|
||||
InteractiveMoveBegin {
|
||||
#[proptest(strategy = "1..=5usize")]
|
||||
window: usize,
|
||||
@@ -658,6 +665,7 @@ enum Op {
|
||||
#[proptest(strategy = "1..=5usize")]
|
||||
window: usize,
|
||||
},
|
||||
ToggleOverview,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
@@ -1387,6 +1395,15 @@ impl Op {
|
||||
Op::WorkspaceSwitchGestureEnd { is_touchpad } => {
|
||||
layout.workspace_switch_gesture_end(is_touchpad);
|
||||
}
|
||||
Op::OverviewGestureBegin => {
|
||||
layout.overview_gesture_begin();
|
||||
}
|
||||
Op::OverviewGestureUpdate { delta, timestamp } => {
|
||||
layout.overview_gesture_update(delta, timestamp);
|
||||
}
|
||||
Op::OverviewGestureEnd => {
|
||||
layout.overview_gesture_end();
|
||||
}
|
||||
Op::InteractiveMoveBegin {
|
||||
window,
|
||||
output_idx,
|
||||
@@ -1440,6 +1457,9 @@ impl Op {
|
||||
Op::InteractiveResizeEnd { window } => {
|
||||
layout.interactive_resize_end(&window);
|
||||
}
|
||||
Op::ToggleOverview => {
|
||||
layout.toggle_overview();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-2
@@ -2,7 +2,10 @@ use std::cmp::max;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use niri_config::{CenterFocusedColumn, OutputName, PresetSize, Workspace as WorkspaceConfig};
|
||||
use niri_config::{
|
||||
CenterFocusedColumn, CornerRadius, FloatOrInt, OutputName, PresetSize,
|
||||
Workspace as WorkspaceConfig,
|
||||
};
|
||||
use niri_ipc::{ColumnDisplay, PositionChange, SizeChange};
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::desktop::{layer_map_for_output, Window};
|
||||
@@ -17,6 +20,7 @@ use super::floating::{FloatingSpace, FloatingSpaceRenderElement};
|
||||
use super::scrolling::{
|
||||
Column, ColumnWidth, ScrollDirection, ScrollingSpace, ScrollingSpaceRenderElement,
|
||||
};
|
||||
use super::shadow::Shadow;
|
||||
use super::tile::{Tile, TileRenderSnapshot};
|
||||
use super::{
|
||||
ActivateWindow, HitType, InsertPosition, InteractiveResizeData, LayoutElement, Options,
|
||||
@@ -25,6 +29,7 @@ use super::{
|
||||
use crate::animation::Clock;
|
||||
use crate::niri_render_elements;
|
||||
use crate::render_helpers::renderer::NiriRenderer;
|
||||
use crate::render_helpers::shadow::ShadowRenderElement;
|
||||
use crate::render_helpers::RenderTarget;
|
||||
use crate::utils::id::IdCounter;
|
||||
use crate::utils::transaction::{Transaction, TransactionBlocker};
|
||||
@@ -80,6 +85,9 @@ pub struct Workspace<W: LayoutElement> {
|
||||
/// zones.
|
||||
working_area: Rectangle<f64, Logical>,
|
||||
|
||||
/// This workspace's shadow in the overview.
|
||||
shadow: Shadow,
|
||||
|
||||
/// Clock for driving animations.
|
||||
pub(super) clock: Clock,
|
||||
|
||||
@@ -228,6 +236,17 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
options.clone(),
|
||||
);
|
||||
|
||||
let shadow_config = niri_config::Shadow {
|
||||
on: true,
|
||||
offset: niri_config::ShadowOffset {
|
||||
x: FloatOrInt(0.),
|
||||
y: FloatOrInt(20.),
|
||||
},
|
||||
softness: FloatOrInt(120.),
|
||||
spread: FloatOrInt(20.),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
scrolling,
|
||||
floating,
|
||||
@@ -237,6 +256,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
transform: output.current_transform(),
|
||||
view_size,
|
||||
working_area,
|
||||
shadow: Shadow::new(shadow_config),
|
||||
output: Some(output),
|
||||
clock,
|
||||
base_options,
|
||||
@@ -281,6 +301,17 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
options.clone(),
|
||||
);
|
||||
|
||||
let shadow_config = niri_config::Shadow {
|
||||
on: true,
|
||||
offset: niri_config::ShadowOffset {
|
||||
x: FloatOrInt(0.),
|
||||
y: FloatOrInt(20.),
|
||||
},
|
||||
softness: FloatOrInt(120.),
|
||||
spread: FloatOrInt(20.),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
scrolling,
|
||||
floating,
|
||||
@@ -291,6 +322,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
original_output,
|
||||
view_size,
|
||||
working_area,
|
||||
shadow: Shadow::new(shadow_config),
|
||||
clock,
|
||||
base_options,
|
||||
options,
|
||||
@@ -343,6 +375,14 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
let view_rect = Rectangle::from_size(self.view_size);
|
||||
self.floating
|
||||
.update_render_elements(is_active && self.floating_is_active.get(), view_rect);
|
||||
|
||||
self.shadow.update_render_elements(
|
||||
self.view_size,
|
||||
true,
|
||||
CornerRadius::default(),
|
||||
self.scale.fractional_scale(),
|
||||
1.,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn update_config(&mut self, base_options: Rc<Options>) {
|
||||
@@ -370,6 +410,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
pub fn update_shaders(&mut self) {
|
||||
self.scrolling.update_shaders();
|
||||
self.floating.update_shaders();
|
||||
self.shadow.update_shaders();
|
||||
}
|
||||
|
||||
pub fn windows(&self) -> impl Iterator<Item = &W> + '_ {
|
||||
@@ -1432,6 +1473,13 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
(floating, scrolling)
|
||||
}
|
||||
|
||||
pub fn render_shadow<R: NiriRenderer>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
) -> impl Iterator<Item = ShadowRenderElement> + '_ {
|
||||
self.shadow.render(renderer, Point::from((0., 0.)))
|
||||
}
|
||||
|
||||
pub fn render_above_top_layer(&self) -> bool {
|
||||
self.scrolling.render_above_top_layer()
|
||||
}
|
||||
@@ -1630,7 +1678,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
self.scrolling.dnd_scroll_gesture_begin();
|
||||
}
|
||||
|
||||
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>) {
|
||||
pub fn dnd_scroll_gesture_scroll(&mut self, pos: Point<f64, Logical>, speed: f64) {
|
||||
let config = &self.options.gestures.dnd_edge_view_scroll;
|
||||
let trigger_width = config.trigger_width.0;
|
||||
|
||||
@@ -1656,6 +1704,7 @@ impl<W: LayoutElement> Workspace<W> {
|
||||
// Normalize to [0, 1].
|
||||
delta / trigger_width
|
||||
};
|
||||
let delta = delta * speed;
|
||||
|
||||
self.scrolling.dnd_scroll_gesture_scroll(delta);
|
||||
}
|
||||
|
||||
+88
-14
@@ -133,7 +133,7 @@ use crate::ipc::server::IpcServer;
|
||||
use crate::layer::mapped::LayerSurfaceRenderElement;
|
||||
use crate::layer::MappedLayer;
|
||||
use crate::layout::tile::TileRenderElement;
|
||||
use crate::layout::workspace::WorkspaceId;
|
||||
use crate::layout::workspace::{Workspace, WorkspaceId};
|
||||
use crate::layout::{HitType, Layout, LayoutElement as _, MonitorRenderElement};
|
||||
use crate::niri_render_elements;
|
||||
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
|
||||
@@ -470,6 +470,7 @@ pub enum KeyboardFocus {
|
||||
LayerShell { surface: WlSurface },
|
||||
LockScreen { surface: Option<WlSurface> },
|
||||
ScreenshotUi,
|
||||
Overview,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PartialEq)]
|
||||
@@ -566,6 +567,7 @@ impl KeyboardFocus {
|
||||
KeyboardFocus::LayerShell { surface } => Some(surface),
|
||||
KeyboardFocus::LockScreen { surface } => surface.as_ref(),
|
||||
KeyboardFocus::ScreenshotUi => None,
|
||||
KeyboardFocus::Overview => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -575,12 +577,17 @@ impl KeyboardFocus {
|
||||
KeyboardFocus::LayerShell { surface } => Some(surface),
|
||||
KeyboardFocus::LockScreen { surface } => surface,
|
||||
KeyboardFocus::ScreenshotUi => None,
|
||||
KeyboardFocus::Overview => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_layout(&self) -> bool {
|
||||
matches!(self, KeyboardFocus::Layout { .. })
|
||||
}
|
||||
|
||||
pub fn is_overview(&self) -> bool {
|
||||
matches!(self, KeyboardFocus::Overview)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
@@ -1026,13 +1033,18 @@ impl State {
|
||||
let focus_on_layer =
|
||||
|layer| excl_focus_on_layer(layer).or_else(|| on_d_focus_on_layer(layer));
|
||||
|
||||
let is_overview_open = self.niri.layout.is_overview_open();
|
||||
|
||||
let mut surface = grab_on_layer(Layer::Overlay);
|
||||
// FIXME: we shouldn't prioritize the top layer grabs over regular overlay input or a
|
||||
// fullscreen layout window. This will need tracking in grab() to avoid handing it out
|
||||
// in the first place. Or a better way to structure this code.
|
||||
surface = surface.or_else(|| grab_on_layer(Layer::Top));
|
||||
surface = surface.or_else(|| grab_on_layer(Layer::Bottom));
|
||||
surface = surface.or_else(|| grab_on_layer(Layer::Background));
|
||||
|
||||
if !is_overview_open {
|
||||
surface = surface.or_else(|| grab_on_layer(Layer::Bottom));
|
||||
surface = surface.or_else(|| grab_on_layer(Layer::Background));
|
||||
}
|
||||
|
||||
surface = surface.or_else(|| focus_on_layer(Layer::Overlay));
|
||||
|
||||
@@ -1043,6 +1055,11 @@ impl State {
|
||||
surface = surface.or_else(|| focus_on_layer(Layer::Background));
|
||||
} else {
|
||||
surface = surface.or_else(|| focus_on_layer(Layer::Top));
|
||||
|
||||
if is_overview_open {
|
||||
surface = Some(surface.unwrap_or(KeyboardFocus::Overview));
|
||||
}
|
||||
|
||||
surface = surface.or_else(|| on_d_focus_on_layer(Layer::Bottom));
|
||||
surface = surface.or_else(|| on_d_focus_on_layer(Layer::Background));
|
||||
surface = surface.or_else(layout_focus);
|
||||
@@ -1102,7 +1119,9 @@ impl State {
|
||||
// focused window.
|
||||
//
|
||||
// FIXME: Ideally this should happen inside Layout itself, then there wouldn't be any
|
||||
// problems with layer-shell, etc.
|
||||
// problems with layer-shell, etc. Or a similar problem now with the Overview where we
|
||||
// don't update the previously focused window because the keyboard focus is on the
|
||||
// Overview rather than on the Layout.
|
||||
if matches!(self.niri.keyboard_focus, KeyboardFocus::Layout { .. })
|
||||
&& matches!(focus, KeyboardFocus::Layout { .. })
|
||||
{
|
||||
@@ -2910,6 +2929,10 @@ impl Niri {
|
||||
output: &Output,
|
||||
pos_within_output: Point<f64, Logical>,
|
||||
) -> bool {
|
||||
if self.layout.is_overview_open() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if some layer-shell surface is on top.
|
||||
let layers = layer_map_for_output(output);
|
||||
let layer_popup_under = |layer| {
|
||||
@@ -2939,6 +2962,42 @@ impl Niri {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns the workspace under the position to be activated.
|
||||
///
|
||||
/// The return value is an output and a workspace index on it.
|
||||
pub fn workspace_under(
|
||||
&self,
|
||||
extended_bounds: bool,
|
||||
pos: Point<f64, Logical>,
|
||||
) -> Option<(Output, &Workspace<Mapped>)> {
|
||||
if self.is_locked() || self.screenshot_ui.is_open() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (output, pos_within_output) = self.output_under(pos)?;
|
||||
|
||||
if self.is_sticky_obscured_under(output, pos_within_output) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.is_layout_obscured_under(output, pos_within_output) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ws = self
|
||||
.layout
|
||||
.workspace_under(extended_bounds, output, pos_within_output)?;
|
||||
Some((output.clone(), ws))
|
||||
}
|
||||
|
||||
pub fn workspace_under_cursor(
|
||||
&self,
|
||||
extended_bounds: bool,
|
||||
) -> Option<(Output, &Workspace<Mapped>)> {
|
||||
let pos = self.seat.get_pointer().unwrap().current_location();
|
||||
self.workspace_under(extended_bounds, pos)
|
||||
}
|
||||
|
||||
/// Returns the window under the position to be activated.
|
||||
///
|
||||
/// The cursor may be inside the window's activation region, but not within the window's input
|
||||
@@ -3039,6 +3098,8 @@ impl Niri {
|
||||
let mon = self.layout.monitor_for_output(output)?;
|
||||
let (_, geo) = mon.workspace_under(pos_within_output)?;
|
||||
layer_pos_within_output += geo.loc;
|
||||
// Don't need to deal with zoom here because in the overview background and
|
||||
// bottom layers don't receive input.
|
||||
}
|
||||
|
||||
let surface_type = if popup {
|
||||
@@ -3096,6 +3157,8 @@ impl Niri {
|
||||
let mut under =
|
||||
layer_popup_under(Layer::Overlay).or_else(|| layer_toplevel_under(Layer::Overlay));
|
||||
|
||||
let is_overview_open = self.layout.is_overview_open();
|
||||
|
||||
// When rendering above the top layer, we put the regular monitor elements first.
|
||||
// Otherwise, we will render all layer-shell pop-ups and the top layer on top.
|
||||
if mon.render_above_top_layer() {
|
||||
@@ -3111,13 +3174,23 @@ impl Niri {
|
||||
} else {
|
||||
under = under
|
||||
.or_else(|| layer_popup_under(Layer::Top))
|
||||
.or_else(|| layer_toplevel_under(Layer::Top))
|
||||
.or_else(interactive_moved_window_under)
|
||||
.or_else(|| layer_popup_under(Layer::Bottom))
|
||||
.or_else(|| layer_popup_under(Layer::Background))
|
||||
.or_else(window_under)
|
||||
.or_else(|| layer_toplevel_under(Layer::Bottom))
|
||||
.or_else(|| layer_toplevel_under(Layer::Background));
|
||||
.or_else(|| layer_toplevel_under(Layer::Top));
|
||||
|
||||
under = under.or_else(interactive_moved_window_under);
|
||||
|
||||
if !is_overview_open {
|
||||
under = under
|
||||
.or_else(|| layer_popup_under(Layer::Bottom))
|
||||
.or_else(|| layer_popup_under(Layer::Background));
|
||||
}
|
||||
|
||||
under = under.or_else(window_under);
|
||||
|
||||
if !is_overview_open {
|
||||
under = under
|
||||
.or_else(|| layer_toplevel_under(Layer::Bottom))
|
||||
.or_else(|| layer_toplevel_under(Layer::Background));
|
||||
}
|
||||
}
|
||||
|
||||
let Some((mut surface_and_pos, (window, layer))) = under else {
|
||||
@@ -3576,6 +3649,7 @@ impl Niri {
|
||||
// layer-shell, the layout will briefly draw as active, despite never having focus.
|
||||
KeyboardFocus::LockScreen { .. } => true,
|
||||
KeyboardFocus::ScreenshotUi => true,
|
||||
KeyboardFocus::Overview => true,
|
||||
};
|
||||
|
||||
self.layout.refresh(layout_is_active);
|
||||
@@ -3877,7 +3951,7 @@ impl Niri {
|
||||
|
||||
// Get monitor elements.
|
||||
let mon = self.layout.monitor_for_output(output).unwrap();
|
||||
let zoom = 1.;
|
||||
let zoom = mon.overview_zoom();
|
||||
let monitor_elements = Vec::from_iter(
|
||||
mon.render_elements(renderer, target, focus_ring)
|
||||
.map(|(geo, iter)| (geo, Vec::from_iter(iter))),
|
||||
@@ -5560,7 +5634,7 @@ impl Niri {
|
||||
}
|
||||
|
||||
if let Some(window) = &new_focus.window {
|
||||
if current_focus.window.as_ref() != Some(window) {
|
||||
if !self.layout.is_overview_open() && current_focus.window.as_ref() != Some(window) {
|
||||
let (window, hit) = window;
|
||||
|
||||
// Don't trigger focus-follows-mouse over the tab indicator.
|
||||
@@ -5809,7 +5883,7 @@ fn scale_relocate_crop<E: Element>(
|
||||
niri_render_elements! {
|
||||
OutputRenderElements<R> => {
|
||||
Monitor = MonitorRenderElement<R>,
|
||||
Tile = TileRenderElement<R>,
|
||||
RescaledTile = RescaleRenderElement<TileRenderElement<R>>,
|
||||
LayerSurface = LayerSurfaceRenderElement<R>,
|
||||
RelocatedLayerSurface = CropRenderElement<RelocateRenderElement<RescaleRenderElement<
|
||||
LayerSurfaceRenderElement<R>
|
||||
|
||||
Reference in New Issue
Block a user