Implement interactive window move

This commit is contained in:
Rasmus Eneman
2024-07-15 15:51:48 +02:00
committed by Ivan Molodetskikh
parent d640e85158
commit e887ee93a3
11 changed files with 1651 additions and 61 deletions

View File

@@ -421,6 +421,8 @@ pub struct Layout {
pub focus_ring: FocusRing,
#[knuffel(child, default)]
pub border: Border,
#[knuffel(child, default)]
pub insert_hint: InsertHint,
#[knuffel(child, unwrap(children), default)]
pub preset_column_widths: Vec<PresetSize>,
#[knuffel(child)]
@@ -442,6 +444,7 @@ impl Default for Layout {
Self {
focus_ring: Default::default(),
border: Default::default(),
insert_hint: Default::default(),
preset_column_widths: Default::default(),
default_column_width: Default::default(),
center_focused_column: Default::default(),
@@ -588,6 +591,23 @@ impl From<FocusRing> for Border {
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub struct InsertHint {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, default = Self::default().color)]
pub color: Color,
}
impl Default for InsertHint {
fn default() -> Self {
Self {
off: false,
color: Color::from_rgba8_unpremul(127, 200, 255, 128),
}
}
}
/// RGB color in [0, 1] with unpremultiplied alpha.
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub struct Color {
@@ -3028,6 +3048,10 @@ mod tests {
}
center-focused-column "on-overflow"
insert-hint {
color "rgb(255, 200, 127)"
}
}
spawn-at-startup "alacritty" "-e" "fish"
@@ -3226,6 +3250,10 @@ mod tests {
active_gradient: None,
inactive_gradient: None,
},
insert_hint: InsertHint {
off: false,
color: Color::from_rgba8_unpremul(255, 200, 127, 255),
},
preset_column_widths: vec![
PresetSize::Proportion(0.25),
PresetSize::Proportion(0.5),

View File

@@ -198,11 +198,7 @@ impl TestCase for Layout {
}
fn are_animations_ongoing(&self) -> bool {
self.layout
.monitor_for_output(&self.output)
.unwrap()
.are_animations_ongoing()
|| !self.steps.is_empty()
self.layout.are_animations_ongoing(Some(&self.output)) || !self.steps.is_empty()
}
fn advance_animations(&mut self, mut current_time: Duration) {

View File

@@ -6,7 +6,7 @@ use smithay::desktop::{
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, PopupUngrabStrategy, Window,
WindowSurfaceType,
};
use smithay::input::pointer::Focus;
use smithay::input::pointer::{Focus, PointerGrab};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment;
@@ -36,6 +36,7 @@ use smithay::{
};
use tracing::field::Empty;
use crate::input::move_grab::MoveGrab;
use crate::input::resize_grab::ResizeGrab;
use crate::input::DOUBLE_CLICK_TIME;
use crate::layout::workspace::ColumnWidth;
@@ -65,8 +66,54 @@ impl XdgShellHandler for State {
}
}
fn move_request(&mut self, _surface: ToplevelSurface, _seat: WlSeat, _serial: Serial) {
// FIXME
fn move_request(&mut self, surface: ToplevelSurface, _seat: WlSeat, serial: Serial) {
let pointer = self.niri.seat.get_pointer().unwrap();
if !pointer.has_grab(serial) {
return;
}
let Some(start_data) = pointer.grab_start_data() else {
return;
};
let Some((focus, _)) = &start_data.focus else {
return;
};
let wl_surface = surface.wl_surface();
if !focus.id().same_client_as(&wl_surface.id()) {
return;
}
let Some((mapped, output)) = self.niri.layout.find_window_and_output(wl_surface) else {
return;
};
let window = mapped.window.clone();
let output = output.clone();
let output_pos = self
.niri
.global_space
.output_geometry(&output)
.unwrap()
.loc
.to_f64();
let pos_within_output = start_data.location - output_pos;
if !self
.niri
.layout
.interactive_move_begin(window.clone(), &output, pos_within_output)
{
return;
}
let grab = MoveGrab::new(start_data, window.clone());
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
}
fn resize_request(

View File

@@ -29,6 +29,7 @@ use smithay::utils::{Logical, Point, Rectangle, Transform, SERIAL_COUNTER};
use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint};
use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait};
use self::move_grab::MoveGrab;
use self::resize_grab::ResizeGrab;
use self::spatial_movement_grab::SpatialMovementGrab;
use crate::niri::State;
@@ -36,6 +37,7 @@ use crate::ui::screenshot_ui::ScreenshotUi;
use crate::utils::spawning::spawn;
use crate::utils::{center, get_monotonic_time, ResizeEdge};
pub mod move_grab;
pub mod resize_grab;
pub mod scroll_tracker;
pub mod spatial_movement_grab;
@@ -1535,8 +1537,41 @@ impl State {
if let Some(mapped) = self.niri.window_under_cursor() {
let window = mapped.window.clone();
// Check if we need to start an interactive move.
if event.button() == Some(MouseButton::Left) && !pointer.is_grabbed() {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,
CompositorMod::Alt => mods.alt,
};
if 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 self.niri.layout.interactive_move_begin(
window.clone(),
&output,
pos_within_output,
) {
let start_data = PointerGrabStartData {
focus: None,
button: event.button_code(),
location,
};
let grab = MoveGrab::new(start_data, window.clone());
pointer.set_grab(self, grab, serial, Focus::Clear);
self.niri.pointer_grab_ongoing = true;
self.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::Named(CursorIcon::Move));
}
}
}
// Check if we need to start an interactive resize.
if event.button() == Some(MouseButton::Right) && !pointer.is_grabbed() {
else if event.button() == Some(MouseButton::Right) && !pointer.is_grabbed() {
let mods = self.niri.seat.get_keyboard().unwrap().modifier_state();
let mod_down = match self.backend.mod_key() {
CompositorMod::Super => mods.logo,

223
src/input/move_grab.rs Normal file
View File

@@ -0,0 +1,223 @@
use std::time::Duration;
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,
};
use smithay::input::SeatHandler;
use smithay::utils::{IsAlive, Logical, Point};
use crate::niri::State;
pub struct MoveGrab {
start_data: PointerGrabStartData<State>,
last_location: Point<f64, Logical>,
window: Window,
is_moving: bool,
}
impl MoveGrab {
pub fn new(start_data: PointerGrabStartData<State>, window: Window) -> Self {
Self {
last_location: start_data.location,
start_data,
window,
is_moving: false,
}
}
fn on_ungrab(&mut self, state: &mut State) {
state.niri.layout.interactive_move_end(&self.window);
// FIXME: only redraw the window output.
state.niri.queue_redraw_all();
state.niri.pointer_grab_ongoing = false;
state
.niri
.cursor_manager
.set_cursor_image(CursorImageStatus::default_named());
}
}
impl PointerGrab<State> for MoveGrab {
fn motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &MotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.motion(data, None, event);
if self.window.alive() {
if let Some((output, pos_within_output)) = data.niri.output_under(event.location) {
let output = output.clone();
let event_delta = event.location - self.last_location;
self.last_location = event.location;
let ongoing = data.niri.layout.interactive_move_update(
&self.window,
event_delta,
output,
pos_within_output,
);
if ongoing {
let timestamp = Duration::from_millis(u64::from(event.time));
if self.is_moving {
data.niri.layout.view_offset_gesture_update(
-event_delta.x,
timestamp,
false,
);
}
return;
}
} else {
return;
}
}
// The move is no longer ongoing.
handle.unset_grab(self, data, event.serial, event.time, true);
}
fn relative_motion(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
_focus: Option<(<State as SeatHandler>::PointerFocus, Point<f64, Logical>)>,
event: &RelativeMotionEvent,
) {
// While the grab is active, no client has pointer focus.
handle.relative_motion(data, None, event);
}
fn button(
&mut self,
data: &mut State,
handle: &mut PointerInnerHandle<'_, State>,
event: &ButtonEvent,
) {
handle.button(data, event);
// MouseButton::Middle
if event.button == 0x112 {
if event.state == ButtonState::Pressed {
let output = data
.niri
.output_under(handle.current_location())
.map(|(output, _)| output)
.cloned();
// TODO: workspace switch gesture.
if let Some(output) = output {
self.is_moving = true;
data.niri.layout.view_offset_gesture_begin(&output, false);
}
} else if event.state == ButtonState::Released {
self.is_moving = false;
data.niri.layout.view_offset_gesture_end(false, None);
}
}
if handle.current_pressed().is_empty() {
// No more buttons are pressed, release the grab.
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);
}
}

View File

@@ -551,12 +551,12 @@ impl State {
}
let Some(ipc_win) = state.windows.get(&id) else {
let window = make_ipc_window(mapped, Some(ws_id));
let window = make_ipc_window(mapped, ws_id);
events.push(Event::WindowOpenedOrChanged { window });
return;
};
let workspace_id = Some(ws_id.get());
let workspace_id = ws_id.map(|id| id.get());
let mut changed = ipc_win.workspace_id != workspace_id;
let wl_surface = mapped.toplevel().wl_surface();
@@ -572,7 +572,7 @@ impl State {
});
if changed {
let window = make_ipc_window(mapped, Some(ws_id));
let window = make_ipc_window(mapped, ws_id);
events.push(Event::WindowOpenedOrChanged { window });
return;
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ use smithay::backend::renderer::element::utils::{
use smithay::output::Output;
use smithay::utils::{Logical, Point, Rectangle};
use super::tile::Tile;
use super::workspace::{
compute_working_area, Column, ColumnWidth, OutputId, Workspace, WorkspaceId,
WorkspaceRenderElement,
@@ -241,6 +242,56 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn add_tile(
&mut self,
workspace_idx: usize,
column_idx: Option<usize>,
tile: Tile<W>,
activate: bool,
width: ColumnWidth,
is_full_width: bool,
) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_tile(column_idx, tile, activate, width, is_full_width, None);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
if workspace_idx == self.workspaces.len() - 1 {
// Insert a new empty workspace.
let ws = Workspace::new(self.output.clone(), self.options.clone());
self.workspaces.push(ws);
}
if activate {
self.activate_workspace(workspace_idx);
}
}
pub fn add_tile_to_column(
&mut self,
workspace_idx: usize,
column_idx: usize,
tile_idx: Option<usize>,
tile: Tile<W>,
activate: bool,
) {
let workspace = &mut self.workspaces[workspace_idx];
workspace.add_tile_to_column(column_idx, tile_idx, tile, activate);
// After adding a new window, workspace becomes this output's own.
workspace.original_output = OutputId::new(&self.output);
// Since we're adding window to an existing column, the workspace isn't empty, and
// therefore cannot be the last one, so we never need to insert a new empty workspace.
if activate {
self.activate_workspace(workspace_idx);
}
}
pub fn clean_up_workspaces(&mut self) {
assert!(self.workspace_switch.is_none());
@@ -682,7 +733,7 @@ impl<W: LayoutElement> Monitor<W> {
}
}
pub fn are_animations_ongoing(&self) -> bool {
pub(super) fn are_animations_ongoing(&self) -> bool {
self.workspace_switch
.as_ref()
.is_some_and(|s| s.is_animation())

View File

@@ -64,6 +64,9 @@ pub struct Tile<W: LayoutElement> {
/// The animation of a tile visually moving vertically.
move_y_animation: Option<MoveAnimation>,
/// Offset during the initial interactive move rubberband.
pub(super) interactive_move_offset: Point<f64, Logical>,
/// Snapshot of the last render for use in the close animation.
unmap_snapshot: Option<TileRenderSnapshot>,
@@ -90,7 +93,7 @@ niri_render_elements! {
}
}
type TileRenderSnapshot =
pub type TileRenderSnapshot =
RenderSnapshot<TileRenderElement<GlesRenderer>, TileRenderElement<GlesRenderer>>;
#[derive(Debug)]
@@ -123,6 +126,7 @@ impl<W: LayoutElement> Tile<W> {
resize_animation: None,
move_x_animation: None,
move_y_animation: None,
interactive_move_offset: Point::from((0., 0.)),
unmap_snapshot: None,
rounded_corner_damage: Default::default(),
scale,
@@ -305,6 +309,8 @@ impl<W: LayoutElement> Tile<W> {
offset.y += move_.from * move_.anim.value();
}
offset += self.interactive_move_offset;
offset
}
@@ -364,6 +370,11 @@ impl<W: LayoutElement> Tile<W> {
});
}
pub fn stop_move_animations(&mut self) {
self.move_x_animation = None;
self.move_y_animation = None;
}
pub fn window(&self) -> &W {
&self.window
}

View File

@@ -8,6 +8,7 @@ use niri_config::{
};
use niri_ipc::SizeChange;
use ordered_float::NotNan;
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::{layer_map_for_output, Window};
use smithay::output::Output;
@@ -16,12 +17,13 @@ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform};
use super::closing_window::{ClosingWindow, ClosingWindowRenderElement};
use super::tile::{Tile, TileRenderElement};
use super::tile::{Tile, TileRenderElement, TileRenderSnapshot};
use super::{ConfigureIntent, InteractiveResizeData, LayoutElement, Options, RemovedTile};
use crate::animation::Animation;
use crate::input::swipe_tracker::SwipeTracker;
use crate::niri_render_elements;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::solid_color::{SolidColorBuffer, SolidColorRenderElement};
use crate::render_helpers::RenderTarget;
use crate::utils::id::IdCounter;
use crate::utils::transaction::{Transaction, TransactionBlocker};
@@ -106,6 +108,12 @@ pub struct Workspace<W: LayoutElement> {
/// Windows in the closing animation.
closing_windows: Vec<ClosingWindow>,
/// Indication where an interactively-moved window is about to be placed.
insert_hint: Option<InsertHint>,
/// Buffer for the insert hint.
insert_hint_buffer: SolidColorBuffer,
/// Configurable properties of the layout as received from the parent monitor.
pub(super) base_options: Rc<Options>,
@@ -119,6 +127,19 @@ pub struct Workspace<W: LayoutElement> {
id: WorkspaceId,
}
#[derive(Debug, PartialEq)]
pub enum InsertPosition {
NewColumn(usize),
InColumn(usize, usize),
}
#[derive(Debug)]
pub struct InsertHint {
pub position: InsertPosition,
pub width: ColumnWidth,
pub is_full_width: bool,
}
#[derive(Debug, Clone)]
pub struct OutputId(String);
@@ -418,6 +439,8 @@ impl<W: LayoutElement> Workspace<W> {
activate_prev_column_on_removal: None,
view_offset_before_fullscreen: None,
closing_windows: vec![],
insert_hint: None,
insert_hint_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
base_options,
options,
name: config.map(|c| c.name.0),
@@ -456,6 +479,8 @@ impl<W: LayoutElement> Workspace<W> {
activate_prev_column_on_removal: None,
view_offset_before_fullscreen: None,
closing_windows: vec![],
insert_hint: None,
insert_hint_buffer: SolidColorBuffer::new((0., 0.), [0., 0., 0., 1.]),
base_options,
options,
name: config.map(|c| c.name.0),
@@ -534,6 +559,13 @@ impl<W: LayoutElement> Workspace<W> {
let view_rect = Rectangle::from_loc_and_size(col_pos, view_size);
col.update_render_elements(is_active, view_rect);
}
if let Some(insert_hint) = &self.insert_hint {
if let Some(area) = self.insert_hint_area(insert_hint) {
self.insert_hint_buffer
.update(area.size, self.options.insert_hint.color.to_array_premul());
}
}
}
pub fn update_config(&mut self, base_options: Rc<Options>) {
@@ -565,10 +597,11 @@ impl<W: LayoutElement> Workspace<W> {
}
pub fn windows_mut(&mut self) -> impl Iterator<Item = &mut W> + '_ {
self.columns
.iter_mut()
.flat_map(|col| col.tiles.iter_mut())
.map(Tile::window_mut)
self.tiles_mut().map(Tile::window_mut)
}
pub fn tiles_mut(&mut self) -> impl Iterator<Item = &mut Tile<W>> + '_ {
self.columns.iter_mut().flat_map(|col| col.tiles.iter_mut())
}
pub fn current_output(&self) -> Option<&Output> {
@@ -731,14 +764,17 @@ impl<W: LayoutElement> Workspace<W> {
});
}
fn compute_new_view_offset_for_column_fit(&self, current_x: f64, idx: usize) -> f64 {
let col = &self.columns[idx];
if col.is_fullscreen {
fn compute_new_view_offset_fit(
&self,
current_x: f64,
col_x: f64,
width: f64,
is_fullscreen: bool,
) -> f64 {
if is_fullscreen {
return 0.;
}
let new_col_x = self.column_x(idx);
let final_x = if let Some(ViewOffsetAdjustment::Animation(anim)) = &self.view_offset_adj {
current_x - self.view_offset + anim.to()
} else {
@@ -748,8 +784,8 @@ impl<W: LayoutElement> Workspace<W> {
let new_offset = compute_new_view_offset(
final_x + self.working_area.loc.x,
self.working_area.size.w,
new_col_x,
col.width(),
col_x,
width,
self.options.gaps,
);
@@ -757,22 +793,45 @@ impl<W: LayoutElement> Workspace<W> {
new_offset - self.working_area.loc.x
}
fn compute_new_view_offset_for_column_centered(&self, current_x: f64, idx: usize) -> f64 {
let col = &self.columns[idx];
if col.is_fullscreen {
return self.compute_new_view_offset_for_column_fit(current_x, idx);
fn compute_new_view_offset_centered(
&self,
current_x: f64,
col_x: f64,
width: f64,
is_fullscreen: bool,
) -> f64 {
if is_fullscreen {
return self.compute_new_view_offset_fit(current_x, col_x, width, is_fullscreen);
}
let width = col.width();
// Columns wider than the view are left-aligned (the fit code can deal with that).
if self.working_area.size.w <= width {
return self.compute_new_view_offset_for_column_fit(current_x, idx);
return self.compute_new_view_offset_fit(current_x, col_x, width, is_fullscreen);
}
-(self.working_area.size.w - width) / 2. - self.working_area.loc.x
}
fn compute_new_view_offset_for_column_fit(&self, current_x: f64, idx: usize) -> f64 {
let col = &self.columns[idx];
self.compute_new_view_offset_fit(
current_x,
self.column_x(idx),
col.width(),
col.is_fullscreen,
)
}
fn compute_new_view_offset_for_column_centered(&self, current_x: f64, idx: usize) -> f64 {
let col = &self.columns[idx];
self.compute_new_view_offset_centered(
current_x,
self.column_x(idx),
col.width(),
col.is_fullscreen,
)
}
fn compute_new_view_offset_for_column(
&self,
current_x: f64,
@@ -958,6 +1017,71 @@ impl<W: LayoutElement> Workspace<W> {
self.windows_mut().find(|win| win.is_wl_surface(wl_surface))
}
pub fn set_insert_hint(&mut self, insert_hint: InsertHint) {
if self.options.insert_hint.off {
return;
}
self.insert_hint = Some(insert_hint);
}
pub fn clear_insert_hint(&mut self) {
self.insert_hint = None;
}
pub fn get_insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
if self.columns.is_empty() {
return InsertPosition::NewColumn(0);
}
let x = pos.x + self.view_pos();
// Aim for the center of the gap.
let x = x + self.options.gaps / 2.;
let y = pos.y + self.options.gaps / 2.;
// Insert position is before the first column.
if x < 0. {
return InsertPosition::NewColumn(0);
}
// Find the closest gap between columns.
let (closest_col_idx, col_x) = self
.column_xs(self.data.iter().copied())
.enumerate()
.min_by_key(|(_, col_x)| NotNan::new((col_x - x).abs()).unwrap())
.unwrap();
// Find the column containing the position.
let (col_idx, _) = self
.column_xs(self.data.iter().copied())
.enumerate()
.take_while(|(_, col_x)| *col_x <= x)
.last()
.unwrap_or((0, 0.));
// Insert position is past the last column.
if col_idx == self.columns.len() {
return InsertPosition::NewColumn(closest_col_idx);
}
// Find the closest gap between tiles.
let col = &self.columns[col_idx];
let (closest_tile_idx, tile_off) = col
.tile_offsets()
.enumerate()
.min_by_key(|(_, tile_off)| NotNan::new((tile_off.y - y).abs()).unwrap())
.unwrap();
// Return the closest among the vertical and the horizontal gap.
let vert_dist = (col_x - x).abs();
let hor_dist = (tile_off.y - y).abs();
if vert_dist <= hor_dist {
InsertPosition::NewColumn(closest_col_idx)
} else {
InsertPosition::InColumn(col_idx, closest_tile_idx)
}
}
pub fn add_window(
&mut self,
col_idx: Option<usize>,
@@ -970,7 +1094,7 @@ impl<W: LayoutElement> Workspace<W> {
self.add_tile(col_idx, tile, activate, width, is_full_width, None);
}
fn add_tile(
pub fn add_tile(
&mut self,
col_idx: Option<usize>,
tile: Tile<W>,
@@ -1488,8 +1612,6 @@ impl<W: LayoutElement> Workspace<W> {
window: &W::Id,
blocker: TransactionBlocker,
) {
let output_scale = Scale::from(self.scale.fractional_scale());
let (tile, mut tile_pos) = self
.tiles_with_render_positions_mut(false)
.find(|(tile, _)| tile.window().id() == window)
@@ -1537,6 +1659,19 @@ impl<W: LayoutElement> Workspace<W> {
tile_pos.x -= offset;
}
self.start_close_animation_for_tile(renderer, snapshot, tile_size, tile_pos, blocker);
}
pub fn start_close_animation_for_tile(
&mut self,
renderer: &mut GlesRenderer,
snapshot: TileRenderSnapshot,
tile_size: Size<f64, Logical>,
tile_pos: Point<f64, Logical>,
blocker: TransactionBlocker,
) {
let output_scale = Scale::from(self.scale.fractional_scale());
let anim = Animation::new(0., 1., 0., self.options.animations.window_close.anim);
let blocker = if self.options.disable_transactions {
@@ -1565,7 +1700,7 @@ impl<W: LayoutElement> Workspace<W> {
}
#[cfg(test)]
pub fn verify_invariants(&self) {
pub fn verify_invariants(&self, move_win_id: Option<&W::Id>) {
use approx::assert_abs_diff_eq;
let scale = self.scale.fractional_scale();
@@ -1601,7 +1736,11 @@ impl<W: LayoutElement> Workspace<W> {
);
}
for (_, tile_pos) in self.tiles_with_render_positions() {
for (tile, tile_pos) in self.tiles_with_render_positions() {
if Some(tile.window().id()) != move_win_id {
assert_eq!(tile.interactive_move_offset, Point::from((0., 0.)));
}
let rounded_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale);
// Tile positions must be rounded to physical pixels.
@@ -2046,7 +2185,7 @@ impl<W: LayoutElement> Workspace<W> {
cancel_resize_for_column(&mut self.interactive_resize, col);
}
fn view_pos(&self) -> f64 {
pub fn view_pos(&self) -> f64 {
self.column_x(self.active_column_idx) + self.view_offset
}
@@ -2123,7 +2262,9 @@ impl<W: LayoutElement> Workspace<W> {
zip(tiles, offsets)
}
fn tiles_with_render_positions(&self) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> {
pub fn tiles_with_render_positions(
&self,
) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> {
let scale = self.scale.fractional_scale();
let view_off = Point::from((-self.view_pos(), 0.));
self.columns_in_render_order()
@@ -2139,7 +2280,7 @@ impl<W: LayoutElement> Workspace<W> {
})
}
fn tiles_with_render_positions_mut(
pub fn tiles_with_render_positions_mut(
&mut self,
round: bool,
) -> impl Iterator<Item = (&mut Tile<W>, Point<f64, Logical>)> {
@@ -2162,6 +2303,96 @@ impl<W: LayoutElement> Workspace<W> {
})
}
fn insert_hint_area(&self, insert_hint: &InsertHint) -> Option<Rectangle<f64, Logical>> {
let mut hint_area = match insert_hint.position {
InsertPosition::NewColumn(column_index) => {
if column_index == 0 || column_index == self.columns.len() {
let size =
Size::from((300., self.working_area.size.h - self.options.gaps * 2.));
let mut loc = Point::from((
self.column_x(column_index),
self.working_area.loc.y + self.options.gaps,
));
if column_index == 0 && !self.columns.is_empty() {
loc.x -= size.w + self.options.gaps;
}
Rectangle::from_loc_and_size(loc, size)
} else if column_index > self.columns.len() {
error!("insert hint column index is out of range");
return None;
} else {
let size =
Size::from((300., self.working_area.size.h - self.options.gaps * 2.));
let loc = Point::from((
self.column_x(column_index) - size.w / 2. - self.options.gaps / 2.,
self.working_area.loc.y + self.options.gaps,
));
Rectangle::from_loc_and_size(loc, size)
}
}
InsertPosition::InColumn(column_index, tile_index) => {
if column_index > self.columns.len() {
error!("insert hint column index is out of range");
return None;
}
if tile_index > self.columns[column_index].tiles.len() {
error!("insert hint tile index is out of range");
return None;
}
let (height, y) = if tile_index == 0 {
(150., self.columns[column_index].tile_offset(tile_index).y)
} else if tile_index == self.columns[column_index].tiles.len() {
(
150.,
self.columns[column_index].tile_offset(tile_index).y
- self.options.gaps
- 150.,
)
} else {
(
300.,
self.columns[column_index].tile_offset(tile_index).y
- self.options.gaps / 2.
- 150.,
)
};
let size = Size::from((self.data[column_index].width, height));
let loc = Point::from((self.column_x(column_index), y));
Rectangle::from_loc_and_size(loc, size)
}
};
// First window on an empty workspace will cancel out any view offset. Replicate this
// effect here.
if self.columns.is_empty() {
let view_offset = if self.is_centering_focused_column() {
self.compute_new_view_offset_centered(0., 0., hint_area.size.w, false)
} else {
self.compute_new_view_offset_fit(0., 0., hint_area.size.w, false)
};
hint_area.loc.x -= view_offset;
} else {
hint_area.loc.x -= self.view_pos();
}
let view_size = self.view_size();
// Make sure the hint is at least partially visible.
if matches!(insert_hint.position, InsertPosition::NewColumn(_)) {
hint_area.loc.x = hint_area.loc.x.max(-hint_area.size.w / 2.);
hint_area.loc.x = hint_area.loc.x.min(view_size.w - hint_area.size.w / 2.);
}
// Round to physical pixels.
hint_area = hint_area
.to_physical_precise_round(self.scale.fractional_scale())
.to_logical(self.scale.fractional_scale());
Some(hint_area)
}
/// Returns the geometry of the active tile relative to and clamped to the view.
///
/// During animations, assumes the final view position.
@@ -2438,7 +2669,22 @@ impl<W: LayoutElement> Workspace<W> {
let mut rv = vec![];
// Draw the closing windows on top.
// Draw the insert hint.
if let Some(insert_hint) = &self.insert_hint {
if let Some(area) = self.insert_hint_area(insert_hint) {
rv.push(
TileRenderElement::SolidColor(SolidColorRenderElement::from_buffer(
&self.insert_hint_buffer,
area.loc,
1.,
Kind::Unspecified,
))
.into(),
);
}
}
// Draw the closing windows on top of the other windows.
let view_rect = Rectangle::from_loc_and_size((self.view_pos(), 0.), self.view_size);
for closing in self.closing_windows.iter().rev() {
let elem = closing.render(renderer.as_gles_renderer(), view_rect, output_scale, target);

View File

@@ -116,6 +116,7 @@ use crate::input::{
apply_libinput_settings, mods_with_finger_scroll_binds, mods_with_wheel_binds, TabletData,
};
use crate::ipc::server::IpcServer;
use crate::layout::tile::TileRenderElement;
use crate::layout::workspace::WorkspaceId;
use crate::layout::{Layout, LayoutElement as _, MonitorRenderElement};
use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
@@ -3076,6 +3077,10 @@ impl Niri {
// Get monitor elements.
let mon = self.layout.monitor_for_output(output).unwrap();
let monitor_elements: Vec<_> = mon.render_elements(renderer, target).collect();
let float_elements: Vec<_> = self
.layout
.render_floating_for_output(renderer, output, target)
.collect();
// Get layer-shell elements.
let layer_map = layer_map_for_output(output);
@@ -3106,10 +3111,12 @@ impl Niri {
// Then the regular monitor elements and the top layer in varying order.
if mon.render_above_top_layer() {
elements.extend(float_elements.into_iter().map(OutputRenderElements::from));
elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from));
extend_from_layer(&mut elements, Layer::Top);
} else {
extend_from_layer(&mut elements, Layer::Top);
elements.extend(float_elements.into_iter().map(OutputRenderElements::from));
elements.extend(monitor_elements.into_iter().map(OutputRenderElements::from));
}
@@ -3151,11 +3158,7 @@ impl Niri {
}
}
state.unfinished_animations_remain = self
.layout
.monitor_for_output(output)
.unwrap()
.are_animations_ongoing();
state.unfinished_animations_remain = self.layout.are_animations_ongoing(Some(output));
self.config_error_notification
.advance_animations(target_presentation_time);
@@ -4797,6 +4800,7 @@ impl ClientData for ClientState {
niri_render_elements! {
OutputRenderElements<R> => {
Monitor = MonitorRenderElement<R>,
Tile = TileRenderElement<R>,
Wayland = WaylandSurfaceRenderElement<R>,
NamedPointer = MemoryRenderBufferRenderElement<R>,
SolidColor = SolidColorRenderElement,