2024-11-30 09:18:33 +03:00
|
|
|
use std::cmp::{max, min};
|
|
|
|
|
use std::iter::{self, zip};
|
|
|
|
|
use std::rc::Rc;
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
2025-04-17 11:00:17 +03:00
|
|
|
use niri_config::{CenterFocusedColumn, PresetSize, Struts};
|
2025-02-06 08:42:09 +03:00
|
|
|
use niri_ipc::{ColumnDisplay, SizeChange};
|
2024-11-30 09:18:33 +03:00
|
|
|
use ordered_float::NotNan;
|
|
|
|
|
use smithay::backend::renderer::gles::GlesRenderer;
|
|
|
|
|
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size};
|
|
|
|
|
|
|
|
|
|
use super::closing_window::{ClosingWindow, ClosingWindowRenderElement};
|
2025-04-17 11:00:17 +03:00
|
|
|
use super::monitor::InsertPosition;
|
2025-02-02 08:41:42 +03:00
|
|
|
use super::tab_indicator::{TabIndicator, TabIndicatorRenderElement, TabInfo};
|
2024-11-30 09:18:33 +03:00
|
|
|
use super::tile::{Tile, TileRenderElement, TileRenderSnapshot};
|
|
|
|
|
use super::workspace::{InteractiveResize, ResolvedSize};
|
2025-02-10 13:12:53 +03:00
|
|
|
use super::{ConfigureIntent, HitType, InteractiveResizeData, LayoutElement, Options, RemovedTile};
|
2024-11-30 09:18:33 +03:00
|
|
|
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::RenderTarget;
|
|
|
|
|
use crate::utils::transaction::{Transaction, TransactionBlocker};
|
|
|
|
|
use crate::utils::ResizeEdge;
|
|
|
|
|
use crate::window::ResolvedWindowRules;
|
|
|
|
|
|
|
|
|
|
/// Amount of touchpad movement to scroll the view for the width of one working area.
|
|
|
|
|
const VIEW_GESTURE_WORKING_AREA_MOVEMENT: f64 = 1200.;
|
|
|
|
|
|
|
|
|
|
/// A scrollable-tiling space for windows.
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct ScrollingSpace<W: LayoutElement> {
|
|
|
|
|
/// Columns of windows on this space.
|
|
|
|
|
columns: Vec<Column<W>>,
|
|
|
|
|
|
|
|
|
|
/// Extra per-column data.
|
|
|
|
|
data: Vec<ColumnData>,
|
|
|
|
|
|
|
|
|
|
/// Index of the currently active column, if any.
|
|
|
|
|
active_column_idx: usize,
|
|
|
|
|
|
|
|
|
|
/// Ongoing interactive resize.
|
|
|
|
|
interactive_resize: Option<InteractiveResize<W>>,
|
|
|
|
|
|
|
|
|
|
/// Offset of the view computed from the active column.
|
|
|
|
|
///
|
|
|
|
|
/// Any gaps, including left padding from work area left exclusive zone, is handled
|
|
|
|
|
/// with this view offset (rather than added as a constant elsewhere in the code). This allows
|
|
|
|
|
/// for natural handling of fullscreen windows, which must ignore work area padding.
|
|
|
|
|
view_offset: ViewOffset,
|
|
|
|
|
|
|
|
|
|
/// Whether to activate the previous, rather than the next, column upon column removal.
|
|
|
|
|
///
|
|
|
|
|
/// When a new column is created and removed with no focus changes in-between, it is more
|
|
|
|
|
/// natural to activate the previously-focused column. This variable tracks that.
|
|
|
|
|
///
|
|
|
|
|
/// Since we only create-and-activate columns immediately to the right of the active column (in
|
|
|
|
|
/// contrast to tabs in Firefox, for example), we can track this as a bool, rather than an
|
|
|
|
|
/// index of the previous column to activate.
|
|
|
|
|
///
|
|
|
|
|
/// The value is the view offset that the previous column had before, to restore it.
|
|
|
|
|
activate_prev_column_on_removal: Option<f64>,
|
|
|
|
|
|
|
|
|
|
/// View offset to restore after unfullscreening.
|
|
|
|
|
view_offset_before_fullscreen: Option<f64>,
|
|
|
|
|
|
|
|
|
|
/// Windows in the closing animation.
|
|
|
|
|
closing_windows: Vec<ClosingWindow>,
|
|
|
|
|
|
|
|
|
|
/// View size for this space.
|
|
|
|
|
view_size: Size<f64, Logical>,
|
|
|
|
|
|
|
|
|
|
/// Working area for this space.
|
|
|
|
|
///
|
|
|
|
|
/// Takes into account layer-shell exclusive zones and niri struts.
|
|
|
|
|
working_area: Rectangle<f64, Logical>,
|
|
|
|
|
|
2025-06-09 13:39:08 +03:00
|
|
|
/// Working area for this space excluding struts.
|
|
|
|
|
///
|
|
|
|
|
/// Used for popup unconstraining. Popups can go over struts, but they shouldn't go over
|
|
|
|
|
/// the layer-shell top layer (which renders on top of popups).
|
|
|
|
|
parent_area: Rectangle<f64, Logical>,
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
/// Scale of the output the space is on (and rounds its sizes to).
|
|
|
|
|
scale: f64,
|
|
|
|
|
|
|
|
|
|
/// Clock for driving animations.
|
|
|
|
|
clock: Clock,
|
|
|
|
|
|
|
|
|
|
/// Configurable properties of the layout.
|
|
|
|
|
options: Rc<Options>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
niri_render_elements! {
|
|
|
|
|
ScrollingSpaceRenderElement<R> => {
|
|
|
|
|
Tile = TileRenderElement<R>,
|
|
|
|
|
ClosingWindow = ClosingWindowRenderElement,
|
2025-02-02 08:41:42 +03:00
|
|
|
TabIndicator = TabIndicatorRenderElement,
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Extra per-column data.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
|
|
|
struct ColumnData {
|
|
|
|
|
/// Cached actual column width.
|
|
|
|
|
width: f64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2025-02-15 11:15:35 +03:00
|
|
|
pub(super) enum ViewOffset {
|
2024-11-30 09:18:33 +03:00
|
|
|
/// The view offset is static.
|
|
|
|
|
Static(f64),
|
|
|
|
|
/// The view offset is animating.
|
|
|
|
|
Animation(Animation),
|
|
|
|
|
/// The view offset is controlled by the ongoing gesture.
|
|
|
|
|
Gesture(ViewGesture),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2025-02-15 11:15:35 +03:00
|
|
|
pub(super) struct ViewGesture {
|
2024-11-30 09:18:33 +03:00
|
|
|
current_view_offset: f64,
|
2025-04-24 21:06:24 +03:00
|
|
|
/// Animation for the extra offset to the current position.
|
|
|
|
|
///
|
|
|
|
|
/// For example, when we need to activate a specific window during a DnD scroll.
|
|
|
|
|
animation: Option<Animation>,
|
2024-11-30 09:18:33 +03:00
|
|
|
tracker: SwipeTracker,
|
|
|
|
|
delta_from_tracker: f64,
|
|
|
|
|
// The view offset we'll use if needed for activate_prev_column_on_removal.
|
|
|
|
|
stationary_view_offset: f64,
|
|
|
|
|
/// Whether the gesture is controlled by the touchpad.
|
|
|
|
|
is_touchpad: bool,
|
2025-02-15 11:15:35 +03:00
|
|
|
|
|
|
|
|
// If this gesture is for drag-and-drop scrolling, this is the last event's unadjusted
|
|
|
|
|
// timestamp.
|
|
|
|
|
dnd_last_event_time: Option<Duration>,
|
2025-02-16 08:16:14 +03:00
|
|
|
// Time when the drag-and-drop scroll delta became non-zero, used for debouncing.
|
|
|
|
|
//
|
|
|
|
|
// If `None` then the scroll delta is currently zero.
|
|
|
|
|
dnd_nonzero_start_time: Option<Duration>,
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct Column<W: LayoutElement> {
|
|
|
|
|
/// Tiles in this column.
|
|
|
|
|
///
|
|
|
|
|
/// Must be non-empty.
|
|
|
|
|
tiles: Vec<Tile<W>>,
|
|
|
|
|
|
|
|
|
|
/// Extra per-tile data.
|
|
|
|
|
///
|
|
|
|
|
/// Must have the same number of elements as `tiles`.
|
|
|
|
|
data: Vec<TileData>,
|
|
|
|
|
|
|
|
|
|
/// Index of the currently active tile.
|
|
|
|
|
active_tile_idx: usize,
|
|
|
|
|
|
|
|
|
|
/// Desired width of this column.
|
|
|
|
|
///
|
|
|
|
|
/// If the column is full-width or full-screened, this is the width that should be restored
|
|
|
|
|
/// upon unfullscreening and untoggling full-width.
|
|
|
|
|
width: ColumnWidth,
|
|
|
|
|
|
2025-01-31 19:24:26 +03:00
|
|
|
/// Currently selected preset width index.
|
|
|
|
|
preset_width_idx: Option<usize>,
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
/// Whether this column is full-width.
|
|
|
|
|
is_full_width: bool,
|
|
|
|
|
|
|
|
|
|
/// Whether this column contains a single full-screened window.
|
|
|
|
|
is_fullscreen: bool,
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
/// How this column displays and arranges windows.
|
|
|
|
|
display_mode: ColumnDisplay,
|
|
|
|
|
|
2025-02-02 08:41:42 +03:00
|
|
|
/// Tab indicator for the tabbed display mode.
|
|
|
|
|
tab_indicator: TabIndicator,
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
/// Animation of the render offset during window swapping.
|
|
|
|
|
move_animation: Option<Animation>,
|
|
|
|
|
|
|
|
|
|
/// Latest known view size for this column's workspace.
|
|
|
|
|
view_size: Size<f64, Logical>,
|
|
|
|
|
|
|
|
|
|
/// Latest known working area for this column's workspace.
|
|
|
|
|
working_area: Rectangle<f64, Logical>,
|
|
|
|
|
|
|
|
|
|
/// Scale of the output the column is on (and rounds its sizes to).
|
|
|
|
|
scale: f64,
|
|
|
|
|
|
|
|
|
|
/// Clock for driving animations.
|
|
|
|
|
clock: Clock,
|
|
|
|
|
|
|
|
|
|
/// Configurable properties of the layout.
|
|
|
|
|
options: Rc<Options>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Extra per-tile data.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
|
|
|
struct TileData {
|
|
|
|
|
/// Requested height of the window.
|
|
|
|
|
///
|
|
|
|
|
/// This is window height, not tile height, so it excludes tile decorations.
|
|
|
|
|
height: WindowHeight,
|
|
|
|
|
|
|
|
|
|
/// Cached actual size of the tile.
|
|
|
|
|
size: Size<f64, Logical>,
|
|
|
|
|
|
|
|
|
|
/// Cached whether the tile is being interactively resized by its left edge.
|
|
|
|
|
interactively_resizing_by_left_edge: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Width of a column.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
|
|
|
pub enum ColumnWidth {
|
|
|
|
|
/// Proportion of the current view width.
|
|
|
|
|
Proportion(f64),
|
|
|
|
|
/// Fixed width in logical pixels.
|
|
|
|
|
Fixed(f64),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Height of a window in a column.
|
|
|
|
|
///
|
|
|
|
|
/// Every window but one in a column must be `Auto`-sized so that the total height can add up to
|
|
|
|
|
/// the workspace height. Resizing a window converts all other windows to `Auto`, weighted to
|
|
|
|
|
/// preserve their visual heights at the moment of the conversion.
|
|
|
|
|
///
|
|
|
|
|
/// In contrast to column widths, proportional height changes are converted to, and stored as,
|
|
|
|
|
/// fixed height right away. With column widths you frequently want e.g. two columns side-by-side
|
|
|
|
|
/// with 50% width each, and you want them to remain this way when moving to a differently sized
|
|
|
|
|
/// monitor. Windows in a column, however, already auto-size to fill the available height, giving
|
|
|
|
|
/// you this behavior. The main reason to set a different window height, then, is when you want
|
|
|
|
|
/// something in the window to fit exactly, e.g. to fit 30 lines in a terminal, which corresponds
|
|
|
|
|
/// to the `Fixed` variant.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
|
|
|
pub enum WindowHeight {
|
|
|
|
|
/// Automatically computed *tile* height, distributed across the column according to weights.
|
|
|
|
|
///
|
|
|
|
|
/// This controls the tile height rather than the window height because it's easier in the auto
|
|
|
|
|
/// height distribution algorithm.
|
|
|
|
|
Auto { weight: f64 },
|
|
|
|
|
/// Fixed *window* height in logical pixels.
|
|
|
|
|
Fixed(f64),
|
|
|
|
|
/// One of the preset heights (tile or window).
|
|
|
|
|
Preset(usize),
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-13 10:29:39 +03:00
|
|
|
/// Horizontal direction for an operation.
|
2025-01-09 08:29:36 +00:00
|
|
|
///
|
|
|
|
|
/// As operations often have a symmetrical counterpart, e.g. focus-right/focus-left, methods
|
|
|
|
|
/// on `Scrolling` can sometimes be factored using the direction of the operation as a parameter.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
|
|
|
pub enum ScrollDirection {
|
|
|
|
|
Left,
|
|
|
|
|
Right,
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
impl<W: LayoutElement> ScrollingSpace<W> {
|
|
|
|
|
pub fn new(
|
|
|
|
|
view_size: Size<f64, Logical>,
|
2025-06-09 13:39:08 +03:00
|
|
|
parent_area: Rectangle<f64, Logical>,
|
2024-11-30 09:18:33 +03:00
|
|
|
scale: f64,
|
|
|
|
|
clock: Clock,
|
|
|
|
|
options: Rc<Options>,
|
|
|
|
|
) -> Self {
|
2025-06-09 13:39:08 +03:00
|
|
|
let working_area = compute_working_area(parent_area, scale, options.struts);
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
columns: Vec::new(),
|
|
|
|
|
data: Vec::new(),
|
|
|
|
|
active_column_idx: 0,
|
|
|
|
|
interactive_resize: None,
|
|
|
|
|
view_offset: ViewOffset::Static(0.),
|
|
|
|
|
activate_prev_column_on_removal: None,
|
|
|
|
|
view_offset_before_fullscreen: None,
|
|
|
|
|
closing_windows: Vec::new(),
|
|
|
|
|
view_size,
|
|
|
|
|
working_area,
|
2025-06-09 13:39:08 +03:00
|
|
|
parent_area,
|
2024-11-30 09:18:33 +03:00
|
|
|
scale,
|
|
|
|
|
clock,
|
|
|
|
|
options,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update_config(
|
|
|
|
|
&mut self,
|
|
|
|
|
view_size: Size<f64, Logical>,
|
2025-06-09 13:39:08 +03:00
|
|
|
parent_area: Rectangle<f64, Logical>,
|
2024-11-30 09:18:33 +03:00
|
|
|
scale: f64,
|
|
|
|
|
options: Rc<Options>,
|
|
|
|
|
) {
|
2025-06-09 13:39:08 +03:00
|
|
|
let working_area = compute_working_area(parent_area, scale, options.struts);
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
for (column, data) in zip(&mut self.columns, &mut self.data) {
|
|
|
|
|
column.update_config(view_size, working_area, scale, options.clone());
|
|
|
|
|
data.update(column);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.view_size = view_size;
|
|
|
|
|
self.working_area = working_area;
|
2025-06-09 13:39:08 +03:00
|
|
|
self.parent_area = parent_area;
|
2024-11-30 09:18:33 +03:00
|
|
|
self.scale = scale;
|
|
|
|
|
self.options = options;
|
2025-05-01 21:34:46 +03:00
|
|
|
|
|
|
|
|
// Apply always-center and such right away.
|
|
|
|
|
if !self.columns.is_empty() && !self.view_offset.is_gesture() {
|
|
|
|
|
self.animate_view_offset_to_column(None, self.active_column_idx, None);
|
|
|
|
|
}
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update_shaders(&mut self) {
|
|
|
|
|
for tile in self.tiles_mut() {
|
|
|
|
|
tile.update_shaders();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn advance_animations(&mut self) {
|
|
|
|
|
if let ViewOffset::Animation(anim) = &self.view_offset {
|
|
|
|
|
if anim.is_done() {
|
|
|
|
|
self.view_offset = ViewOffset::Static(anim.to());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-15 11:15:35 +03:00
|
|
|
if let ViewOffset::Gesture(gesture) = &mut self.view_offset {
|
|
|
|
|
// Make sure the last event time doesn't go too much out of date (for
|
|
|
|
|
// workspaces not under cursor), causing sudden jumps.
|
|
|
|
|
//
|
|
|
|
|
// This happens after any dnd_scroll_gesture_scroll() calls (in
|
|
|
|
|
// Layout::advance_animations()), so it doesn't mess up the time delta there.
|
|
|
|
|
if let Some(last_time) = &mut gesture.dnd_last_event_time {
|
2025-02-16 08:16:14 +03:00
|
|
|
let now = self.clock.now_unadjusted();
|
|
|
|
|
if *last_time != now {
|
|
|
|
|
*last_time = now;
|
|
|
|
|
|
|
|
|
|
// If last_time was already == now, then dnd_scroll_gesture_scroll() must've
|
|
|
|
|
// updated the gesture already. Therefore, when this code runs, the pointer
|
|
|
|
|
// must be outside the DnD scrolling zone.
|
|
|
|
|
gesture.dnd_nonzero_start_time = None;
|
|
|
|
|
}
|
2025-02-15 11:15:35 +03:00
|
|
|
}
|
2025-04-24 21:06:24 +03:00
|
|
|
|
|
|
|
|
if let Some(anim) = &mut gesture.animation {
|
|
|
|
|
if anim.is_done() {
|
|
|
|
|
gesture.animation = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-02-15 11:15:35 +03:00
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
for col in &mut self.columns {
|
|
|
|
|
col.advance_animations();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.closing_windows.retain_mut(|closing| {
|
|
|
|
|
closing.advance_animations();
|
|
|
|
|
closing.are_animations_ongoing()
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn are_animations_ongoing(&self) -> bool {
|
2025-04-24 21:06:24 +03:00
|
|
|
self.view_offset.is_animation_ongoing()
|
2024-11-30 09:18:33 +03:00
|
|
|
|| self.columns.iter().any(Column::are_animations_ongoing)
|
|
|
|
|
|| !self.closing_windows.is_empty()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn are_transitions_ongoing(&self) -> bool {
|
|
|
|
|
!self.view_offset.is_static()
|
2025-02-11 15:35:06 +03:00
|
|
|
|| self.columns.iter().any(Column::are_transitions_ongoing)
|
2024-11-30 09:18:33 +03:00
|
|
|
|| !self.closing_windows.is_empty()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update_render_elements(&mut self, is_active: bool) {
|
|
|
|
|
let view_pos = Point::from((self.view_pos(), 0.));
|
2024-12-04 22:08:47 +03:00
|
|
|
let view_size = self.view_size;
|
2024-11-30 09:18:33 +03:00
|
|
|
let active_idx = self.active_column_idx;
|
|
|
|
|
for (col_idx, (col, col_x)) in self.columns_mut().enumerate() {
|
|
|
|
|
let is_active = is_active && col_idx == active_idx;
|
|
|
|
|
let col_off = Point::from((col_x, 0.));
|
|
|
|
|
let col_pos = view_pos - col_off - col.render_offset();
|
2025-01-04 10:14:51 +03:00
|
|
|
let view_rect = Rectangle::new(col_pos, view_size);
|
2024-11-30 09:18:33 +03:00
|
|
|
col.update_render_elements(is_active, view_rect);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn tiles(&self) -> impl Iterator<Item = &Tile<W>> + '_ {
|
|
|
|
|
self.columns.iter().flat_map(|col| col.tiles.iter())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn tiles_mut(&mut self) -> impl Iterator<Item = &mut Tile<W>> + '_ {
|
|
|
|
|
self.columns.iter_mut().flat_map(|col| col.tiles.iter_mut())
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 21:11:02 +03:00
|
|
|
pub fn is_empty(&self) -> bool {
|
|
|
|
|
self.columns.is_empty()
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn active_window(&self) -> Option<&W> {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col = &self.columns[self.active_column_idx];
|
|
|
|
|
Some(col.tiles[col.active_tile_idx].window())
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-23 10:40:52 +03:00
|
|
|
pub fn active_window_mut(&mut self) -> Option<&mut W> {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col = &mut self.columns[self.active_column_idx];
|
|
|
|
|
Some(col.tiles[col.active_tile_idx].window_mut())
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-28 11:40:16 +03:00
|
|
|
pub fn active_tile_mut(&mut self) -> Option<&mut Tile<W>> {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col = &mut self.columns[self.active_column_idx];
|
|
|
|
|
Some(&mut col.tiles[col.active_tile_idx])
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn is_active_fullscreen(&self) -> bool {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col = &self.columns[self.active_column_idx];
|
|
|
|
|
col.is_fullscreen
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-06 10:54:48 +03:00
|
|
|
pub fn new_window_toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size<i32, Logical> {
|
2024-11-30 09:18:33 +03:00
|
|
|
let border_config = rules.border.resolve_against(self.options.border);
|
2025-02-07 09:36:08 +03:00
|
|
|
|
|
|
|
|
let display_mode = rules
|
|
|
|
|
.default_column_display
|
|
|
|
|
.unwrap_or(self.options.default_column_display);
|
|
|
|
|
let will_tab = display_mode == ColumnDisplay::Tabbed;
|
|
|
|
|
let extra_size = if will_tab {
|
|
|
|
|
TabIndicator::new(self.options.tab_indicator).extra_size(1, self.scale)
|
|
|
|
|
} else {
|
|
|
|
|
Size::from((0., 0.))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
compute_toplevel_bounds(
|
|
|
|
|
border_config,
|
|
|
|
|
self.working_area.size,
|
|
|
|
|
extra_size,
|
|
|
|
|
self.options.gaps,
|
|
|
|
|
)
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn new_window_size(
|
|
|
|
|
&self,
|
2025-01-31 19:24:26 +03:00
|
|
|
width: Option<PresetSize>,
|
2024-12-30 08:48:05 +03:00
|
|
|
height: Option<PresetSize>,
|
2024-11-30 09:18:33 +03:00
|
|
|
rules: &ResolvedWindowRules,
|
|
|
|
|
) -> Size<i32, Logical> {
|
|
|
|
|
let border = rules.border.resolve_against(self.options.border);
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
let display_mode = rules
|
|
|
|
|
.default_column_display
|
|
|
|
|
.unwrap_or(self.options.default_column_display);
|
|
|
|
|
let will_tab = display_mode == ColumnDisplay::Tabbed;
|
|
|
|
|
let extra = if will_tab {
|
|
|
|
|
TabIndicator::new(self.options.tab_indicator).extra_size(1, self.scale)
|
|
|
|
|
} else {
|
|
|
|
|
Size::from((0., 0.))
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
let working_size = self.working_area.size;
|
|
|
|
|
|
2025-01-31 19:24:26 +03:00
|
|
|
let width = if let Some(size) = width {
|
2025-02-07 09:36:08 +03:00
|
|
|
let size = match resolve_preset_size(size, &self.options, working_size.w, extra.w) {
|
2025-01-31 19:24:26 +03:00
|
|
|
ResolvedSize::Tile(mut size) => {
|
|
|
|
|
if !border.off {
|
|
|
|
|
size -= border.width.0 * 2.;
|
|
|
|
|
}
|
|
|
|
|
size
|
|
|
|
|
}
|
|
|
|
|
ResolvedSize::Window(size) => size,
|
|
|
|
|
};
|
2024-11-30 09:18:33 +03:00
|
|
|
|
2025-01-31 19:24:26 +03:00
|
|
|
max(1, size.floor() as i32)
|
2024-11-30 09:18:33 +03:00
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
};
|
|
|
|
|
|
2024-12-30 08:48:05 +03:00
|
|
|
let mut full_height = self.working_area.size.h - self.options.gaps * 2.;
|
2024-11-30 09:18:33 +03:00
|
|
|
if !border.off {
|
2024-12-30 08:48:05 +03:00
|
|
|
full_height -= border.width.0 * 2.;
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
2024-12-30 08:48:05 +03:00
|
|
|
let height = if let Some(height) = height {
|
2025-02-07 09:36:08 +03:00
|
|
|
let height = match resolve_preset_size(height, &self.options, working_size.h, extra.h) {
|
2024-12-30 08:48:05 +03:00
|
|
|
ResolvedSize::Tile(mut size) => {
|
|
|
|
|
if !border.off {
|
|
|
|
|
size -= border.width.0 * 2.;
|
|
|
|
|
}
|
|
|
|
|
size
|
|
|
|
|
}
|
|
|
|
|
ResolvedSize::Window(size) => size,
|
|
|
|
|
};
|
|
|
|
|
f64::min(height, full_height)
|
|
|
|
|
} else {
|
|
|
|
|
full_height
|
|
|
|
|
};
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
Size::from((width, max(height.floor() as i32, 1)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_centering_focused_column(&self) -> bool {
|
|
|
|
|
self.options.center_focused_column == CenterFocusedColumn::Always
|
|
|
|
|
|| (self.options.always_center_single_column && self.columns.len() <= 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compute_new_view_offset_fit(
|
|
|
|
|
&self,
|
|
|
|
|
target_x: Option<f64>,
|
|
|
|
|
col_x: f64,
|
|
|
|
|
width: f64,
|
|
|
|
|
is_fullscreen: bool,
|
|
|
|
|
) -> f64 {
|
|
|
|
|
if is_fullscreen {
|
|
|
|
|
return 0.;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let target_x = target_x.unwrap_or_else(|| self.target_view_pos());
|
|
|
|
|
|
|
|
|
|
let new_offset = compute_new_view_offset(
|
|
|
|
|
target_x + self.working_area.loc.x,
|
|
|
|
|
self.working_area.size.w,
|
|
|
|
|
col_x,
|
|
|
|
|
width,
|
|
|
|
|
self.options.gaps,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Non-fullscreen windows are always offset at least by the working area position.
|
|
|
|
|
new_offset - self.working_area.loc.x
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compute_new_view_offset_centered(
|
|
|
|
|
&self,
|
|
|
|
|
target_x: Option<f64>,
|
|
|
|
|
col_x: f64,
|
|
|
|
|
width: f64,
|
|
|
|
|
is_fullscreen: bool,
|
|
|
|
|
) -> f64 {
|
|
|
|
|
if is_fullscreen {
|
|
|
|
|
return self.compute_new_view_offset_fit(target_x, col_x, width, is_fullscreen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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_fit(target_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, target_x: Option<f64>, idx: usize) -> f64 {
|
|
|
|
|
let col = &self.columns[idx];
|
|
|
|
|
self.compute_new_view_offset_fit(
|
|
|
|
|
target_x,
|
|
|
|
|
self.column_x(idx),
|
|
|
|
|
col.width(),
|
|
|
|
|
col.is_fullscreen,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compute_new_view_offset_for_column_centered(
|
|
|
|
|
&self,
|
|
|
|
|
target_x: Option<f64>,
|
|
|
|
|
idx: usize,
|
|
|
|
|
) -> f64 {
|
|
|
|
|
let col = &self.columns[idx];
|
|
|
|
|
self.compute_new_view_offset_centered(
|
|
|
|
|
target_x,
|
|
|
|
|
self.column_x(idx),
|
|
|
|
|
col.width(),
|
|
|
|
|
col.is_fullscreen,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compute_new_view_offset_for_column(
|
|
|
|
|
&self,
|
|
|
|
|
target_x: Option<f64>,
|
|
|
|
|
idx: usize,
|
|
|
|
|
prev_idx: Option<usize>,
|
|
|
|
|
) -> f64 {
|
|
|
|
|
if self.is_centering_focused_column() {
|
|
|
|
|
return self.compute_new_view_offset_for_column_centered(target_x, idx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match self.options.center_focused_column {
|
|
|
|
|
CenterFocusedColumn::Always => {
|
|
|
|
|
self.compute_new_view_offset_for_column_centered(target_x, idx)
|
|
|
|
|
}
|
|
|
|
|
CenterFocusedColumn::OnOverflow => {
|
|
|
|
|
let Some(prev_idx) = prev_idx else {
|
|
|
|
|
return self.compute_new_view_offset_for_column_fit(target_x, idx);
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-25 10:06:46 +03:00
|
|
|
// Activating the same column.
|
|
|
|
|
if prev_idx == idx {
|
|
|
|
|
return self.compute_new_view_offset_for_column_fit(target_x, idx);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
// Always take the left or right neighbor of the target as the source.
|
|
|
|
|
let source_idx = if prev_idx > idx {
|
|
|
|
|
min(idx + 1, self.columns.len() - 1)
|
|
|
|
|
} else {
|
|
|
|
|
idx.saturating_sub(1)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let source_col_x = self.column_x(source_idx);
|
|
|
|
|
let source_col_width = self.columns[source_idx].width();
|
|
|
|
|
|
|
|
|
|
let target_col_x = self.column_x(idx);
|
|
|
|
|
let target_col_width = self.columns[idx].width();
|
|
|
|
|
|
|
|
|
|
let total_width = if source_col_x < target_col_x {
|
|
|
|
|
// Source is left from target.
|
|
|
|
|
target_col_x - source_col_x + target_col_width
|
|
|
|
|
} else {
|
|
|
|
|
// Source is right from target.
|
|
|
|
|
source_col_x - target_col_x + source_col_width
|
|
|
|
|
} + self.options.gaps * 2.;
|
|
|
|
|
|
|
|
|
|
// If it fits together, do a normal animation, otherwise center the new column.
|
|
|
|
|
if total_width <= self.working_area.size.w {
|
|
|
|
|
self.compute_new_view_offset_for_column_fit(target_x, idx)
|
|
|
|
|
} else {
|
|
|
|
|
self.compute_new_view_offset_for_column_centered(target_x, idx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
CenterFocusedColumn::Never => {
|
|
|
|
|
self.compute_new_view_offset_for_column_fit(target_x, idx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn animate_view_offset(&mut self, idx: usize, new_view_offset: f64) {
|
|
|
|
|
self.animate_view_offset_with_config(
|
|
|
|
|
idx,
|
|
|
|
|
new_view_offset,
|
|
|
|
|
self.options.animations.horizontal_view_movement.0,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn animate_view_offset_with_config(
|
|
|
|
|
&mut self,
|
|
|
|
|
idx: usize,
|
|
|
|
|
new_view_offset: f64,
|
|
|
|
|
config: niri_config::Animation,
|
|
|
|
|
) {
|
|
|
|
|
let new_col_x = self.column_x(idx);
|
|
|
|
|
let old_col_x = self.column_x(self.active_column_idx);
|
|
|
|
|
let offset_delta = old_col_x - new_col_x;
|
|
|
|
|
self.view_offset.offset(offset_delta);
|
|
|
|
|
|
|
|
|
|
let pixel = 1. / self.scale;
|
|
|
|
|
|
|
|
|
|
// If our view offset is already this or animating towards this, we don't need to do
|
|
|
|
|
// anything.
|
|
|
|
|
let to_diff = new_view_offset - self.view_offset.target();
|
|
|
|
|
if to_diff.abs() < pixel {
|
|
|
|
|
// Correct for any inaccuracy.
|
|
|
|
|
self.view_offset.offset(to_diff);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-24 21:06:24 +03:00
|
|
|
match &mut self.view_offset {
|
|
|
|
|
ViewOffset::Gesture(gesture) if gesture.dnd_last_event_time.is_some() => {
|
|
|
|
|
gesture.stationary_view_offset = new_view_offset;
|
|
|
|
|
|
|
|
|
|
let current_pos = gesture.current_view_offset - gesture.delta_from_tracker;
|
|
|
|
|
gesture.delta_from_tracker = new_view_offset - current_pos;
|
|
|
|
|
let offset_delta = new_view_offset - gesture.current_view_offset;
|
|
|
|
|
gesture.current_view_offset = new_view_offset;
|
|
|
|
|
|
|
|
|
|
gesture.animate_from(-offset_delta, self.clock.clone(), config);
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
// FIXME: also compute and use current velocity.
|
|
|
|
|
self.view_offset = ViewOffset::Animation(Animation::new(
|
|
|
|
|
self.clock.clone(),
|
|
|
|
|
self.view_offset.current(),
|
|
|
|
|
new_view_offset,
|
|
|
|
|
0.,
|
|
|
|
|
config,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn animate_view_offset_to_column_centered(
|
|
|
|
|
&mut self,
|
|
|
|
|
target_x: Option<f64>,
|
|
|
|
|
idx: usize,
|
|
|
|
|
config: niri_config::Animation,
|
|
|
|
|
) {
|
|
|
|
|
let new_view_offset = self.compute_new_view_offset_for_column_centered(target_x, idx);
|
|
|
|
|
self.animate_view_offset_with_config(idx, new_view_offset, config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn animate_view_offset_to_column_with_config(
|
|
|
|
|
&mut self,
|
|
|
|
|
target_x: Option<f64>,
|
|
|
|
|
idx: usize,
|
|
|
|
|
prev_idx: Option<usize>,
|
|
|
|
|
config: niri_config::Animation,
|
|
|
|
|
) {
|
|
|
|
|
let new_view_offset = self.compute_new_view_offset_for_column(target_x, idx, prev_idx);
|
|
|
|
|
self.animate_view_offset_with_config(idx, new_view_offset, config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn animate_view_offset_to_column(
|
|
|
|
|
&mut self,
|
|
|
|
|
target_x: Option<f64>,
|
|
|
|
|
idx: usize,
|
|
|
|
|
prev_idx: Option<usize>,
|
|
|
|
|
) {
|
|
|
|
|
self.animate_view_offset_to_column_with_config(
|
|
|
|
|
target_x,
|
|
|
|
|
idx,
|
|
|
|
|
prev_idx,
|
|
|
|
|
self.options.animations.horizontal_view_movement.0,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn activate_column(&mut self, idx: usize) {
|
|
|
|
|
self.activate_column_with_anim_config(
|
|
|
|
|
idx,
|
|
|
|
|
self.options.animations.horizontal_view_movement.0,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn activate_column_with_anim_config(&mut self, idx: usize, config: niri_config::Animation) {
|
2025-04-25 10:06:46 +03:00
|
|
|
if self.active_column_idx == idx
|
|
|
|
|
// During a DnD scroll, animate even when activating the same window, for DnD hold.
|
|
|
|
|
&& (self.columns.is_empty() || !self.view_offset.is_dnd_scroll())
|
|
|
|
|
{
|
2024-11-30 09:18:33 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.animate_view_offset_to_column_with_config(
|
|
|
|
|
None,
|
|
|
|
|
idx,
|
|
|
|
|
Some(self.active_column_idx),
|
|
|
|
|
config,
|
|
|
|
|
);
|
|
|
|
|
|
2025-04-25 10:06:46 +03:00
|
|
|
if self.active_column_idx != idx {
|
|
|
|
|
self.active_column_idx = idx;
|
2024-11-30 09:18:33 +03:00
|
|
|
|
2025-04-25 10:06:46 +03:00
|
|
|
// A different column was activated; reset the flag.
|
|
|
|
|
self.activate_prev_column_on_removal = None;
|
|
|
|
|
self.view_offset_before_fullscreen = None;
|
|
|
|
|
self.interactive_resize = None;
|
|
|
|
|
}
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
2025-04-17 11:00:17 +03:00
|
|
|
pub(super) fn insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
|
2024-11-30 09:18:33 +03:00
|
|
|
if self.columns.is_empty() {
|
2024-12-12 09:06:02 +03:00
|
|
|
return InsertPosition::NewColumn(0);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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. {
|
2024-12-12 09:06:02 +03:00
|
|
|
return InsertPosition::NewColumn(0);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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() {
|
2024-12-12 09:06:02 +03:00
|
|
|
return InsertPosition::NewColumn(closest_col_idx);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the closest gap between tiles.
|
|
|
|
|
let col = &self.columns[col_idx];
|
2025-02-01 10:46:52 +03:00
|
|
|
|
|
|
|
|
let (closest_tile_idx, tile_y) = if col.display_mode == ColumnDisplay::Tabbed {
|
|
|
|
|
// In tabbed mode, there's only one tile visible, and we want to check its top and
|
|
|
|
|
// bottom.
|
|
|
|
|
let top = col.tile_offsets().nth(col.active_tile_idx).unwrap().y;
|
|
|
|
|
let bottom = top + col.data[col.active_tile_idx].size.h;
|
|
|
|
|
if (top - y).abs() <= (bottom - y).abs() {
|
|
|
|
|
(col.active_tile_idx, top)
|
|
|
|
|
} else {
|
|
|
|
|
(col.active_tile_idx + 1, bottom)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
col.tile_offsets()
|
|
|
|
|
.map(|tile_off| tile_off.y)
|
|
|
|
|
.enumerate()
|
|
|
|
|
.min_by_key(|(_, tile_y)| NotNan::new((tile_y - y).abs()).unwrap())
|
|
|
|
|
.unwrap()
|
|
|
|
|
};
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
// Return the closest among the vertical and the horizontal gap.
|
|
|
|
|
let vert_dist = (col_x - x).abs();
|
2025-02-01 10:46:52 +03:00
|
|
|
let hor_dist = (tile_y - y).abs();
|
2024-11-30 09:18:33 +03:00
|
|
|
if vert_dist <= hor_dist {
|
2024-12-12 09:06:02 +03:00
|
|
|
InsertPosition::NewColumn(closest_col_idx)
|
2024-11-30 09:18:33 +03:00
|
|
|
} else {
|
2024-12-12 09:06:02 +03:00
|
|
|
InsertPosition::InColumn(col_idx, closest_tile_idx)
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn add_tile(
|
|
|
|
|
&mut self,
|
|
|
|
|
col_idx: Option<usize>,
|
|
|
|
|
tile: Tile<W>,
|
|
|
|
|
activate: bool,
|
|
|
|
|
width: ColumnWidth,
|
|
|
|
|
is_full_width: bool,
|
|
|
|
|
anim_config: Option<niri_config::Animation>,
|
|
|
|
|
) {
|
|
|
|
|
let column = Column::new_with_tile(
|
|
|
|
|
tile,
|
|
|
|
|
self.view_size,
|
|
|
|
|
self.working_area,
|
|
|
|
|
self.scale,
|
|
|
|
|
width,
|
|
|
|
|
is_full_width,
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
self.add_column(col_idx, column, activate, anim_config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn add_tile_to_column(
|
|
|
|
|
&mut self,
|
|
|
|
|
col_idx: usize,
|
|
|
|
|
tile_idx: Option<usize>,
|
|
|
|
|
tile: Tile<W>,
|
|
|
|
|
activate: bool,
|
|
|
|
|
) {
|
|
|
|
|
let prev_next_x = self.column_x(col_idx + 1);
|
|
|
|
|
|
|
|
|
|
let target_column = &mut self.columns[col_idx];
|
|
|
|
|
let tile_idx = tile_idx.unwrap_or(target_column.tiles.len());
|
2025-02-08 15:22:11 +03:00
|
|
|
let mut prev_active_tile_idx = target_column.active_tile_idx;
|
|
|
|
|
let was_fullscreen = target_column.tiles[prev_active_tile_idx].is_fullscreen();
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
target_column.add_tile_at(tile_idx, tile, true);
|
|
|
|
|
self.data[col_idx].update(target_column);
|
|
|
|
|
|
|
|
|
|
// If the target column is the active column and its window was requested to, but hasn't
|
2025-02-04 09:51:23 +03:00
|
|
|
// gone into fullscreen yet, then clear the stored view offset, if we just asked it to
|
2024-11-30 09:18:33 +03:00
|
|
|
// stop going into fullscreen.
|
2025-02-04 09:51:23 +03:00
|
|
|
if col_idx == self.active_column_idx && !was_fullscreen && !target_column.is_fullscreen {
|
2024-11-30 09:18:33 +03:00
|
|
|
self.view_offset_before_fullscreen = None;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-08 15:22:11 +03:00
|
|
|
if tile_idx <= prev_active_tile_idx {
|
|
|
|
|
target_column.active_tile_idx += 1;
|
|
|
|
|
prev_active_tile_idx += 1;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
if activate {
|
2025-02-08 15:22:11 +03:00
|
|
|
target_column.activate_idx(tile_idx);
|
2024-11-30 09:18:33 +03:00
|
|
|
if self.active_column_idx != col_idx {
|
|
|
|
|
self.activate_column(col_idx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-08 15:24:02 +03:00
|
|
|
let target_column = &mut self.columns[col_idx];
|
|
|
|
|
if target_column.display_mode == ColumnDisplay::Tabbed {
|
|
|
|
|
if target_column.active_tile_idx == tile_idx {
|
|
|
|
|
// Fade out the previously active tile.
|
|
|
|
|
let tile = &mut target_column.tiles[prev_active_tile_idx];
|
|
|
|
|
tile.animate_alpha(1., 0., self.options.animations.window_movement.0);
|
|
|
|
|
} else {
|
|
|
|
|
// Fade out when adding into a tabbed column into the background.
|
|
|
|
|
let tile = &mut target_column.tiles[tile_idx];
|
|
|
|
|
tile.animate_alpha(1., 0., self.options.animations.window_movement.0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
// Adding a wider window into a column increases its width now (even if the window will
|
|
|
|
|
// shrink later). Move the columns to account for this.
|
|
|
|
|
let offset = self.column_x(col_idx + 1) - prev_next_x;
|
|
|
|
|
if self.active_column_idx <= col_idx {
|
|
|
|
|
for col in &mut self.columns[col_idx + 1..] {
|
|
|
|
|
col.animate_move_from(-offset);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for col in &mut self.columns[..=col_idx] {
|
|
|
|
|
col.animate_move_from(offset);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn add_tile_right_of(
|
|
|
|
|
&mut self,
|
|
|
|
|
right_of: &W::Id,
|
|
|
|
|
tile: Tile<W>,
|
2024-12-26 09:37:38 +03:00
|
|
|
activate: bool,
|
2024-11-30 09:18:33 +03:00
|
|
|
width: ColumnWidth,
|
|
|
|
|
is_full_width: bool,
|
|
|
|
|
) {
|
|
|
|
|
let right_of_idx = self
|
|
|
|
|
.columns
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|col| col.contains(right_of))
|
|
|
|
|
.unwrap();
|
|
|
|
|
let col_idx = right_of_idx + 1;
|
|
|
|
|
|
|
|
|
|
self.add_tile(Some(col_idx), tile, activate, width, is_full_width, None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn add_column(
|
|
|
|
|
&mut self,
|
|
|
|
|
idx: Option<usize>,
|
|
|
|
|
mut column: Column<W>,
|
|
|
|
|
activate: bool,
|
|
|
|
|
anim_config: Option<niri_config::Animation>,
|
|
|
|
|
) {
|
|
|
|
|
let was_empty = self.columns.is_empty();
|
|
|
|
|
|
|
|
|
|
let idx = idx.unwrap_or_else(|| {
|
|
|
|
|
if was_empty {
|
|
|
|
|
0
|
|
|
|
|
} else {
|
|
|
|
|
self.active_column_idx + 1
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
column.update_config(
|
|
|
|
|
self.view_size,
|
|
|
|
|
self.working_area,
|
|
|
|
|
self.scale,
|
|
|
|
|
self.options.clone(),
|
|
|
|
|
);
|
|
|
|
|
self.data.insert(idx, ColumnData::new(&column));
|
|
|
|
|
self.columns.insert(idx, column);
|
|
|
|
|
|
|
|
|
|
if activate {
|
|
|
|
|
// If this is the first window on an empty workspace, remove the effect of whatever
|
|
|
|
|
// view_offset was left over and skip the animation.
|
|
|
|
|
if was_empty {
|
|
|
|
|
self.view_offset = ViewOffset::Static(0.);
|
|
|
|
|
self.view_offset =
|
|
|
|
|
ViewOffset::Static(self.compute_new_view_offset_for_column(None, idx, None));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let prev_offset = (!was_empty && idx == self.active_column_idx + 1)
|
|
|
|
|
.then(|| self.view_offset.stationary());
|
|
|
|
|
|
|
|
|
|
let anim_config =
|
|
|
|
|
anim_config.unwrap_or(self.options.animations.horizontal_view_movement.0);
|
|
|
|
|
self.activate_column_with_anim_config(idx, anim_config);
|
|
|
|
|
self.activate_prev_column_on_removal = prev_offset;
|
|
|
|
|
} else if !was_empty && idx <= self.active_column_idx {
|
|
|
|
|
self.active_column_idx += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Animate movement of other columns.
|
|
|
|
|
let offset = self.column_x(idx + 1) - self.column_x(idx);
|
|
|
|
|
let config = anim_config.unwrap_or(self.options.animations.window_movement.0);
|
|
|
|
|
if self.active_column_idx <= idx {
|
|
|
|
|
for col in &mut self.columns[idx + 1..] {
|
|
|
|
|
col.animate_move_from_with_config(-offset, config);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for col in &mut self.columns[..idx] {
|
|
|
|
|
col.animate_move_from_with_config(offset, config);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn remove_active_tile(&mut self, transaction: Transaction) -> Option<RemovedTile<W>> {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let column = &self.columns[self.active_column_idx];
|
|
|
|
|
Some(self.remove_tile_by_idx(
|
|
|
|
|
self.active_column_idx,
|
|
|
|
|
column.active_tile_idx,
|
|
|
|
|
transaction,
|
|
|
|
|
None,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn remove_tile(&mut self, window: &W::Id, transaction: Transaction) -> RemovedTile<W> {
|
|
|
|
|
let column_idx = self
|
|
|
|
|
.columns
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|col| col.contains(window))
|
|
|
|
|
.unwrap();
|
|
|
|
|
let column = &self.columns[column_idx];
|
|
|
|
|
|
|
|
|
|
let tile_idx = column.position(window).unwrap();
|
|
|
|
|
self.remove_tile_by_idx(column_idx, tile_idx, transaction, None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn remove_tile_by_idx(
|
|
|
|
|
&mut self,
|
|
|
|
|
column_idx: usize,
|
|
|
|
|
tile_idx: usize,
|
|
|
|
|
transaction: Transaction,
|
|
|
|
|
anim_config: Option<niri_config::Animation>,
|
|
|
|
|
) -> RemovedTile<W> {
|
|
|
|
|
// If this is the only tile in the column, remove the whole column.
|
|
|
|
|
if self.columns[column_idx].tiles.len() == 1 {
|
|
|
|
|
let mut column = self.remove_column_by_idx(column_idx, anim_config);
|
|
|
|
|
return RemovedTile {
|
|
|
|
|
tile: column.tiles.remove(tile_idx),
|
|
|
|
|
width: column.width,
|
|
|
|
|
is_full_width: column.is_full_width,
|
2024-11-29 21:11:02 +03:00
|
|
|
is_floating: false,
|
2024-11-30 09:18:33 +03:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let column = &mut self.columns[column_idx];
|
|
|
|
|
let prev_width = self.data[column_idx].width;
|
|
|
|
|
|
2025-02-08 15:24:02 +03:00
|
|
|
let movement_config = anim_config.unwrap_or(self.options.animations.window_movement.0);
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
// Animate movement of other tiles.
|
|
|
|
|
// FIXME: tiles can move by X too, in a centered or resizing layout with one window smaller
|
|
|
|
|
// than the others.
|
|
|
|
|
let offset_y = column.tile_offset(tile_idx + 1).y - column.tile_offset(tile_idx).y;
|
|
|
|
|
for tile in &mut column.tiles[tile_idx + 1..] {
|
|
|
|
|
tile.animate_move_y_from(offset_y);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-08 15:24:02 +03:00
|
|
|
if column.display_mode == ColumnDisplay::Tabbed && tile_idx != column.active_tile_idx {
|
|
|
|
|
// Fade in when removing background tab from a tabbed column.
|
|
|
|
|
let tile = &mut column.tiles[tile_idx];
|
|
|
|
|
tile.animate_alpha(0., 1., movement_config);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
let tile = column.tiles.remove(tile_idx);
|
|
|
|
|
column.data.remove(tile_idx);
|
|
|
|
|
|
2025-03-22 13:52:08 +03:00
|
|
|
// If we're removing a pending-unfullscreen window, we need to clear the stored view
|
|
|
|
|
// offset. There might be other pending-unfullscreen windows in this column but that's kind
|
|
|
|
|
// of an edge case, don't think we need to handle that.
|
|
|
|
|
if column_idx == self.active_column_idx && tile.is_fullscreen() && !column.is_fullscreen {
|
|
|
|
|
self.view_offset_before_fullscreen = None;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
// If one window is left, reset its weight to 1.
|
|
|
|
|
if column.data.len() == 1 {
|
|
|
|
|
if let WindowHeight::Auto { weight } = &mut column.data[0].height {
|
|
|
|
|
*weight = 1.;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Stop interactive resize.
|
|
|
|
|
if let Some(resize) = &self.interactive_resize {
|
|
|
|
|
if tile.window().id() == &resize.window {
|
|
|
|
|
self.interactive_resize = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let tile = RemovedTile {
|
|
|
|
|
tile,
|
|
|
|
|
width: column.width,
|
|
|
|
|
is_full_width: column.is_full_width,
|
2024-11-29 21:11:02 +03:00
|
|
|
is_floating: false,
|
2024-11-30 09:18:33 +03:00
|
|
|
};
|
|
|
|
|
|
2025-02-08 15:24:02 +03:00
|
|
|
#[allow(clippy::comparison_chain)] // What do you even want here?
|
2025-02-06 09:38:35 +03:00
|
|
|
if tile_idx < column.active_tile_idx {
|
|
|
|
|
// A tile above was removed; preserve the current position.
|
|
|
|
|
column.active_tile_idx -= 1;
|
2025-02-08 15:22:11 +03:00
|
|
|
} else if tile_idx == column.active_tile_idx {
|
|
|
|
|
// The active tile was removed, so the active tile index shifted to the next tile.
|
|
|
|
|
if tile_idx == column.tiles.len() {
|
|
|
|
|
// The bottom tile was removed and it was active, update active idx to remain valid.
|
|
|
|
|
column.activate_idx(tile_idx - 1);
|
2025-02-08 15:24:02 +03:00
|
|
|
} else {
|
|
|
|
|
// Ensure the newly active tile animates to opaque.
|
|
|
|
|
column.tiles[tile_idx].ensure_alpha_animates_to_1();
|
2025-02-08 15:22:11 +03:00
|
|
|
}
|
2025-02-06 09:38:35 +03:00
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
column.update_tile_sizes_with_transaction(true, transaction);
|
|
|
|
|
self.data[column_idx].update(column);
|
|
|
|
|
let offset = prev_width - column.width();
|
|
|
|
|
|
|
|
|
|
// Animate movement of the other columns.
|
|
|
|
|
if self.active_column_idx <= column_idx {
|
|
|
|
|
for col in &mut self.columns[column_idx + 1..] {
|
|
|
|
|
col.animate_move_from_with_config(offset, movement_config);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for col in &mut self.columns[..=column_idx] {
|
|
|
|
|
col.animate_move_from_with_config(-offset, movement_config);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tile
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn remove_active_column(&mut self) -> Option<Column<W>> {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Some(self.remove_column_by_idx(self.active_column_idx, None))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn remove_column_by_idx(
|
|
|
|
|
&mut self,
|
|
|
|
|
column_idx: usize,
|
|
|
|
|
anim_config: Option<niri_config::Animation>,
|
|
|
|
|
) -> Column<W> {
|
|
|
|
|
// Animate movement of the other columns.
|
|
|
|
|
let movement_config = anim_config.unwrap_or(self.options.animations.window_movement.0);
|
|
|
|
|
let offset = self.column_x(column_idx + 1) - self.column_x(column_idx);
|
|
|
|
|
if self.active_column_idx <= column_idx {
|
|
|
|
|
for col in &mut self.columns[column_idx + 1..] {
|
|
|
|
|
col.animate_move_from_with_config(offset, movement_config);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for col in &mut self.columns[..column_idx] {
|
|
|
|
|
col.animate_move_from_with_config(-offset, movement_config);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let column = self.columns.remove(column_idx);
|
|
|
|
|
self.data.remove(column_idx);
|
|
|
|
|
|
|
|
|
|
// Stop interactive resize.
|
|
|
|
|
if let Some(resize) = &self.interactive_resize {
|
|
|
|
|
if column
|
|
|
|
|
.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|tile| tile.window().id() == &resize.window)
|
|
|
|
|
{
|
|
|
|
|
self.interactive_resize = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if column_idx + 1 == self.active_column_idx {
|
|
|
|
|
// The previous column, that we were going to activate upon removal of the active
|
|
|
|
|
// column, has just been itself removed.
|
|
|
|
|
self.activate_prev_column_on_removal = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if column_idx == self.active_column_idx {
|
|
|
|
|
self.view_offset_before_fullscreen = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let view_config = anim_config.unwrap_or(self.options.animations.horizontal_view_movement.0);
|
|
|
|
|
|
|
|
|
|
if column_idx < self.active_column_idx {
|
|
|
|
|
// A column to the left was removed; preserve the current position.
|
|
|
|
|
// FIXME: preserve activate_prev_column_on_removal.
|
|
|
|
|
self.active_column_idx -= 1;
|
|
|
|
|
self.activate_prev_column_on_removal = None;
|
|
|
|
|
} else if column_idx == self.active_column_idx
|
|
|
|
|
&& self.activate_prev_column_on_removal.is_some()
|
|
|
|
|
{
|
|
|
|
|
// The active column was removed, and we needed to activate the previous column.
|
|
|
|
|
if 0 < column_idx {
|
|
|
|
|
let prev_offset = self.activate_prev_column_on_removal.unwrap();
|
|
|
|
|
|
|
|
|
|
self.activate_column_with_anim_config(self.active_column_idx - 1, view_config);
|
|
|
|
|
|
|
|
|
|
// Restore the view offset but make sure to scroll the view in case the
|
|
|
|
|
// previous window had resized.
|
|
|
|
|
self.animate_view_offset_with_config(
|
|
|
|
|
self.active_column_idx,
|
|
|
|
|
prev_offset,
|
|
|
|
|
view_config,
|
|
|
|
|
);
|
|
|
|
|
self.animate_view_offset_to_column_with_config(
|
|
|
|
|
None,
|
|
|
|
|
self.active_column_idx,
|
|
|
|
|
None,
|
|
|
|
|
view_config,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.activate_column_with_anim_config(
|
|
|
|
|
min(self.active_column_idx, self.columns.len() - 1),
|
|
|
|
|
view_config,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
column
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update_window(&mut self, window: &W::Id, serial: Option<Serial>) {
|
|
|
|
|
let (col_idx, column) = self
|
|
|
|
|
.columns
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.find(|(_, col)| col.contains(window))
|
|
|
|
|
.unwrap();
|
|
|
|
|
let (tile_idx, tile) = column
|
|
|
|
|
.tiles
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.find(|(_, tile)| tile.window().id() == window)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let was_fullscreen = tile.is_fullscreen();
|
|
|
|
|
let resize = tile.window_mut().interactive_resize_data();
|
|
|
|
|
|
|
|
|
|
// Do this before calling update_window() so it can get up-to-date info.
|
|
|
|
|
if let Some(serial) = serial {
|
2024-12-18 10:38:02 +03:00
|
|
|
tile.window_mut().on_commit(serial);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let prev_width = self.data[col_idx].width;
|
|
|
|
|
|
|
|
|
|
column.update_window(window);
|
|
|
|
|
self.data[col_idx].update(column);
|
|
|
|
|
column.update_tile_sizes(false);
|
|
|
|
|
|
|
|
|
|
let offset = prev_width - self.data[col_idx].width;
|
|
|
|
|
|
|
|
|
|
// Move other columns in tandem with resizing.
|
|
|
|
|
let started_resize_anim =
|
|
|
|
|
column.tiles[tile_idx].resize_animation().is_some() && offset != 0.;
|
|
|
|
|
if started_resize_anim {
|
|
|
|
|
if self.active_column_idx <= col_idx {
|
|
|
|
|
for col in &mut self.columns[col_idx + 1..] {
|
|
|
|
|
col.animate_move_from_with_config(
|
|
|
|
|
offset,
|
|
|
|
|
self.options.animations.window_resize.anim,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for col in &mut self.columns[..=col_idx] {
|
|
|
|
|
col.animate_move_from_with_config(
|
|
|
|
|
-offset,
|
|
|
|
|
self.options.animations.window_resize.anim,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if col_idx == self.active_column_idx {
|
|
|
|
|
// If offset == 0, then don't mess with the view or the gesture. Some clients (Firefox,
|
|
|
|
|
// Chromium, Electron) currently don't commit after the ack of a configure that drops
|
|
|
|
|
// the Resizing state, which can trigger this code path for a while.
|
|
|
|
|
let resize = if offset != 0. { resize } else { None };
|
|
|
|
|
if let Some(resize) = resize {
|
|
|
|
|
// Don't bother with the gesture.
|
|
|
|
|
self.view_offset.cancel_gesture();
|
|
|
|
|
|
|
|
|
|
// If this is an interactive resize commit of an active window, then we need to
|
|
|
|
|
// either preserve the view offset or adjust it accordingly.
|
|
|
|
|
let centered = self.is_centering_focused_column();
|
|
|
|
|
|
|
|
|
|
let width = self.data[col_idx].width;
|
|
|
|
|
let offset = if centered {
|
|
|
|
|
// FIXME: when view_offset becomes fractional, this can be made additive too.
|
|
|
|
|
let new_offset =
|
|
|
|
|
-(self.working_area.size.w - width) / 2. - self.working_area.loc.x;
|
|
|
|
|
new_offset - self.view_offset.target()
|
|
|
|
|
} else if resize.edges.contains(ResizeEdge::LEFT) {
|
|
|
|
|
-offset
|
|
|
|
|
} else {
|
|
|
|
|
0.
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.view_offset.offset(offset);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.interactive_resize.is_none() && !self.view_offset.is_gesture() {
|
|
|
|
|
// We might need to move the view to ensure the resized window is still visible.
|
|
|
|
|
|
|
|
|
|
// Upon unfullscreening, restore the view offset.
|
2025-02-04 09:51:23 +03:00
|
|
|
//
|
|
|
|
|
// In tabbed display mode, there can be multiple tiles in a fullscreen column. They
|
|
|
|
|
// will unfullscreen one by one, and the column width will shrink only when the
|
|
|
|
|
// last tile unfullscreens. This is when we want to restore the view offset,
|
|
|
|
|
// otherwise it will immediately reset back by the animate_view_offset below.
|
|
|
|
|
let is_fullscreen = self.columns[col_idx].tiles.iter().any(Tile::is_fullscreen);
|
2024-11-30 09:18:33 +03:00
|
|
|
if was_fullscreen && !is_fullscreen {
|
|
|
|
|
if let Some(prev_offset) = self.view_offset_before_fullscreen.take() {
|
|
|
|
|
self.animate_view_offset(col_idx, prev_offset);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Synchronize the horizontal view movement with the resize so that it looks nice.
|
|
|
|
|
// This is especially important for always-centered view.
|
|
|
|
|
let config = if started_resize_anim {
|
|
|
|
|
self.options.animations.window_resize.anim
|
|
|
|
|
} else {
|
|
|
|
|
self.options.animations.horizontal_view_movement.0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// FIXME: we will want to skip the animation in some cases here to make continuously
|
|
|
|
|
// resizing windows not look janky.
|
|
|
|
|
self.animate_view_offset_to_column_with_config(None, col_idx, None, config);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn scroll_amount_to_activate(&self, window: &W::Id) -> f64 {
|
|
|
|
|
let column_idx = self
|
|
|
|
|
.columns
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|col| col.contains(window))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
if self.active_column_idx == column_idx {
|
|
|
|
|
return 0.;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Consider the end of an ongoing animation because that's what compute to fit does too.
|
|
|
|
|
let target_x = self.target_view_pos();
|
|
|
|
|
let new_view_offset = self.compute_new_view_offset_for_column(
|
|
|
|
|
Some(target_x),
|
|
|
|
|
column_idx,
|
|
|
|
|
Some(self.active_column_idx),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let new_col_x = self.column_x(column_idx);
|
|
|
|
|
let from_view_offset = target_x - new_col_x;
|
|
|
|
|
|
|
|
|
|
(from_view_offset - new_view_offset).abs() / self.working_area.size.w
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-08 09:15:10 +03:00
|
|
|
pub fn activate_window(&mut self, window: &W::Id) -> bool {
|
|
|
|
|
let column_idx = self.columns.iter().position(|col| col.contains(window));
|
|
|
|
|
let Some(column_idx) = column_idx else {
|
|
|
|
|
return false;
|
|
|
|
|
};
|
2024-11-30 09:18:33 +03:00
|
|
|
let column = &mut self.columns[column_idx];
|
|
|
|
|
|
|
|
|
|
column.activate_window(window);
|
|
|
|
|
self.activate_column(column_idx);
|
2024-12-08 09:15:10 +03:00
|
|
|
|
|
|
|
|
true
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn start_close_animation_for_window(
|
|
|
|
|
&mut self,
|
|
|
|
|
renderer: &mut GlesRenderer,
|
|
|
|
|
window: &W::Id,
|
|
|
|
|
blocker: TransactionBlocker,
|
|
|
|
|
) {
|
|
|
|
|
let (tile, mut tile_pos) = self
|
|
|
|
|
.tiles_with_render_positions_mut(false)
|
|
|
|
|
.find(|(tile, _)| tile.window().id() == window)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let Some(snapshot) = tile.take_unmap_snapshot() else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let tile_size = tile.tile_size();
|
|
|
|
|
|
|
|
|
|
let (col_idx, tile_idx) = self
|
|
|
|
|
.columns
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.find_map(|(col_idx, col)| {
|
|
|
|
|
col.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|tile| tile.window().id() == window)
|
|
|
|
|
.map(move |tile_idx| (col_idx, tile_idx))
|
|
|
|
|
})
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let col = &self.columns[col_idx];
|
|
|
|
|
let removing_last = col.tiles.len() == 1;
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
// Skip closing animation for invisible tiles in a tabbed column.
|
|
|
|
|
if col.display_mode == ColumnDisplay::Tabbed && tile_idx != col.active_tile_idx {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
tile_pos.x += self.view_pos();
|
|
|
|
|
|
|
|
|
|
if col_idx < self.active_column_idx {
|
|
|
|
|
let offset = if removing_last {
|
|
|
|
|
self.column_x(col_idx + 1) - self.column_x(col_idx)
|
|
|
|
|
} else {
|
|
|
|
|
self.data[col_idx].width
|
|
|
|
|
- col
|
|
|
|
|
.data
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.filter_map(|(idx, data)| {
|
|
|
|
|
(idx != tile_idx).then_some(NotNan::new(data.size.w).unwrap())
|
|
|
|
|
})
|
|
|
|
|
.max()
|
|
|
|
|
.map(NotNan::into_inner)
|
|
|
|
|
.unwrap()
|
|
|
|
|
};
|
|
|
|
|
tile_pos.x -= offset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.start_close_animation_for_tile(renderer, snapshot, tile_size, tile_pos, blocker);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 21:11:02 +03:00
|
|
|
fn start_close_animation_for_tile(
|
2024-11-30 09:18:33 +03:00
|
|
|
&mut self,
|
|
|
|
|
renderer: &mut GlesRenderer,
|
|
|
|
|
snapshot: TileRenderSnapshot,
|
|
|
|
|
tile_size: Size<f64, Logical>,
|
|
|
|
|
tile_pos: Point<f64, Logical>,
|
|
|
|
|
blocker: TransactionBlocker,
|
|
|
|
|
) {
|
|
|
|
|
let anim = Animation::new(
|
|
|
|
|
self.clock.clone(),
|
|
|
|
|
0.,
|
|
|
|
|
1.,
|
|
|
|
|
0.,
|
|
|
|
|
self.options.animations.window_close.anim,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let blocker = if self.options.disable_transactions {
|
|
|
|
|
TransactionBlocker::completed()
|
|
|
|
|
} else {
|
|
|
|
|
blocker
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let scale = Scale::from(self.scale);
|
|
|
|
|
let res = ClosingWindow::new(
|
|
|
|
|
renderer, snapshot, scale, tile_size, tile_pos, blocker, anim,
|
|
|
|
|
);
|
|
|
|
|
match res {
|
|
|
|
|
Ok(closing) => {
|
|
|
|
|
self.closing_windows.push(closing);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
warn!("error creating a closing window animation: {err:?}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-10 14:12:54 +03:00
|
|
|
pub fn start_open_animation(&mut self, id: &W::Id) -> bool {
|
|
|
|
|
self.columns
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.any(|col| col.start_open_animation(id))
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn focus_left(&mut self) -> bool {
|
|
|
|
|
if self.active_column_idx == 0 {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
self.activate_column(self.active_column_idx - 1);
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn focus_right(&mut self) -> bool {
|
|
|
|
|
if self.active_column_idx + 1 >= self.columns.len() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.activate_column(self.active_column_idx + 1);
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn focus_column_first(&mut self) {
|
|
|
|
|
self.activate_column(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn focus_column_last(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.activate_column(self.columns.len() - 1);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 03:05:55 +01:00
|
|
|
pub fn focus_column(&mut self, index: usize) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.activate_column(index.saturating_sub(1).min(self.columns.len() - 1));
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-05 17:18:21 +03:00
|
|
|
pub fn focus_window_in_column(&mut self, index: u8) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.columns[self.active_column_idx].focus_index(index);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn focus_down(&mut self) -> bool {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.columns[self.active_column_idx].focus_down()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn focus_up(&mut self) -> bool {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.columns[self.active_column_idx].focus_up()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn focus_down_or_left(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let column = &mut self.columns[self.active_column_idx];
|
|
|
|
|
if !column.focus_down() {
|
|
|
|
|
self.focus_left();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn focus_down_or_right(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let column = &mut self.columns[self.active_column_idx];
|
|
|
|
|
if !column.focus_down() {
|
|
|
|
|
self.focus_right();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn focus_up_or_left(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let column = &mut self.columns[self.active_column_idx];
|
|
|
|
|
if !column.focus_up() {
|
|
|
|
|
self.focus_left();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn focus_up_or_right(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let column = &mut self.columns[self.active_column_idx];
|
|
|
|
|
if !column.focus_up() {
|
|
|
|
|
self.focus_right();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-05 17:18:21 +03:00
|
|
|
pub fn focus_top(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.columns[self.active_column_idx].focus_top()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn focus_bottom(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.columns[self.active_column_idx].focus_bottom()
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-13 03:09:06 +01:00
|
|
|
pub fn move_column_to_index(&mut self, index: usize) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.move_column_to(index.saturating_sub(1).min(self.columns.len() - 1));
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
fn move_column_to(&mut self, new_idx: usize) {
|
|
|
|
|
if self.active_column_idx == new_idx {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let current_col_x = self.column_x(self.active_column_idx);
|
|
|
|
|
let next_col_x = self.column_x(self.active_column_idx + 1);
|
|
|
|
|
|
|
|
|
|
let mut column = self.columns.remove(self.active_column_idx);
|
|
|
|
|
let data = self.data.remove(self.active_column_idx);
|
|
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, &mut column);
|
|
|
|
|
self.columns.insert(new_idx, column);
|
|
|
|
|
self.data.insert(new_idx, data);
|
|
|
|
|
|
|
|
|
|
// Preserve the camera position when moving to the left.
|
|
|
|
|
let view_offset_delta = -self.column_x(self.active_column_idx) + current_col_x;
|
|
|
|
|
self.view_offset.offset(view_offset_delta);
|
|
|
|
|
|
|
|
|
|
// The column we just moved is offset by the difference between its new and old position.
|
|
|
|
|
let new_col_x = self.column_x(new_idx);
|
|
|
|
|
self.columns[new_idx].animate_move_from(current_col_x - new_col_x);
|
|
|
|
|
|
|
|
|
|
// All columns in between moved by the width of the column that we just moved.
|
|
|
|
|
let others_x_offset = next_col_x - current_col_x;
|
|
|
|
|
if self.active_column_idx < new_idx {
|
|
|
|
|
for col in &mut self.columns[self.active_column_idx..new_idx] {
|
|
|
|
|
col.animate_move_from(others_x_offset);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for col in &mut self.columns[new_idx + 1..=self.active_column_idx] {
|
|
|
|
|
col.animate_move_from(-others_x_offset);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.activate_column_with_anim_config(new_idx, self.options.animations.window_movement.0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn move_left(&mut self) -> bool {
|
|
|
|
|
if self.active_column_idx == 0 {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.move_column_to(self.active_column_idx - 1);
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn move_right(&mut self) -> bool {
|
|
|
|
|
let new_idx = self.active_column_idx + 1;
|
|
|
|
|
if new_idx >= self.columns.len() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.move_column_to(new_idx);
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn move_column_to_first(&mut self) {
|
|
|
|
|
self.move_column_to(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn move_column_to_last(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let new_idx = self.columns.len() - 1;
|
|
|
|
|
self.move_column_to(new_idx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn move_down(&mut self) -> bool {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.columns[self.active_column_idx].move_down()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn move_up(&mut self) -> bool {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.columns[self.active_column_idx].move_up()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn consume_or_expel_window_left(&mut self, window: Option<&W::Id>) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (source_col_idx, source_tile_idx) = if let Some(window) = window {
|
|
|
|
|
self.columns
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.find_map(|(col_idx, col)| {
|
|
|
|
|
col.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|tile| tile.window().id() == window)
|
|
|
|
|
.map(|tile_idx| (col_idx, tile_idx))
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
} else {
|
|
|
|
|
let source_col_idx = self.active_column_idx;
|
|
|
|
|
let source_tile_idx = self.columns[self.active_column_idx].active_tile_idx;
|
|
|
|
|
(source_col_idx, source_tile_idx)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let source_column = &self.columns[source_col_idx];
|
|
|
|
|
let prev_off = source_column.tile_offset(source_tile_idx);
|
|
|
|
|
|
|
|
|
|
let source_tile_was_active = self.active_column_idx == source_col_idx
|
|
|
|
|
&& source_column.active_tile_idx == source_tile_idx;
|
|
|
|
|
|
|
|
|
|
if source_column.tiles.len() == 1 {
|
|
|
|
|
if source_col_idx == 0 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Move into adjacent column.
|
|
|
|
|
let target_column_idx = source_col_idx - 1;
|
|
|
|
|
|
|
|
|
|
let offset = if self.active_column_idx <= source_col_idx {
|
|
|
|
|
// Tiles to the right animate from the following column.
|
|
|
|
|
self.column_x(source_col_idx) - self.column_x(target_column_idx)
|
|
|
|
|
} else {
|
|
|
|
|
// Tiles to the left animate to preserve their right edge position.
|
|
|
|
|
f64::max(
|
|
|
|
|
0.,
|
|
|
|
|
self.data[target_column_idx].width - self.data[source_col_idx].width,
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
let mut offset = Point::from((offset, 0.));
|
|
|
|
|
|
|
|
|
|
if source_tile_was_active {
|
|
|
|
|
// Make sure the previous (target) column is activated so the animation looks right.
|
2025-03-14 08:47:32 +03:00
|
|
|
//
|
|
|
|
|
// However, if it was already going to be activated, leave the offset as is. This
|
|
|
|
|
// improves the workflow that has become common with tabbed columns: open a new
|
|
|
|
|
// window, then immediately consume it left as a new tab.
|
|
|
|
|
self.activate_prev_column_on_removal
|
|
|
|
|
.get_or_insert(self.view_offset.stationary() + offset.x);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
offset.x += self.columns[source_col_idx].render_offset().x;
|
|
|
|
|
let RemovedTile { tile, .. } = self.remove_tile_by_idx(
|
|
|
|
|
source_col_idx,
|
|
|
|
|
0,
|
|
|
|
|
Transaction::new(),
|
|
|
|
|
Some(self.options.animations.window_movement.0),
|
|
|
|
|
);
|
|
|
|
|
self.add_tile_to_column(target_column_idx, None, tile, source_tile_was_active);
|
|
|
|
|
|
|
|
|
|
let target_column = &mut self.columns[target_column_idx];
|
|
|
|
|
offset.x -= target_column.render_offset().x;
|
|
|
|
|
offset += prev_off - target_column.tile_offset(target_column.tiles.len() - 1);
|
|
|
|
|
|
|
|
|
|
let new_tile = target_column.tiles.last_mut().unwrap();
|
|
|
|
|
new_tile.animate_move_from(offset);
|
|
|
|
|
} else {
|
|
|
|
|
// Move out of column.
|
|
|
|
|
let mut offset = Point::from((source_column.render_offset().x, 0.));
|
|
|
|
|
|
|
|
|
|
let removed =
|
|
|
|
|
self.remove_tile_by_idx(source_col_idx, source_tile_idx, Transaction::new(), None);
|
|
|
|
|
|
|
|
|
|
// We're inserting into the source column position.
|
|
|
|
|
let target_column_idx = source_col_idx;
|
|
|
|
|
|
|
|
|
|
self.add_tile(
|
|
|
|
|
Some(target_column_idx),
|
|
|
|
|
removed.tile,
|
|
|
|
|
source_tile_was_active,
|
|
|
|
|
removed.width,
|
|
|
|
|
removed.is_full_width,
|
|
|
|
|
Some(self.options.animations.window_movement.0),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if source_tile_was_active {
|
|
|
|
|
// We added to the left, don't activate even further left on removal.
|
|
|
|
|
self.activate_prev_column_on_removal = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if target_column_idx < self.active_column_idx {
|
|
|
|
|
// Tiles to the left animate from the following column.
|
|
|
|
|
offset.x += self.column_x(target_column_idx + 1) - self.column_x(target_column_idx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let new_col = &mut self.columns[target_column_idx];
|
|
|
|
|
offset += prev_off - new_col.tile_offset(0);
|
|
|
|
|
new_col.tiles[0].animate_move_from(offset);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn consume_or_expel_window_right(&mut self, window: Option<&W::Id>) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (source_col_idx, source_tile_idx) = if let Some(window) = window {
|
|
|
|
|
self.columns
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.find_map(|(col_idx, col)| {
|
|
|
|
|
col.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|tile| tile.window().id() == window)
|
|
|
|
|
.map(|tile_idx| (col_idx, tile_idx))
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
} else {
|
|
|
|
|
let source_col_idx = self.active_column_idx;
|
|
|
|
|
let source_tile_idx = self.columns[self.active_column_idx].active_tile_idx;
|
|
|
|
|
(source_col_idx, source_tile_idx)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let cur_x = self.column_x(source_col_idx);
|
|
|
|
|
|
|
|
|
|
let source_column = &self.columns[source_col_idx];
|
|
|
|
|
let mut offset = Point::from((source_column.render_offset().x, 0.));
|
|
|
|
|
let prev_off = source_column.tile_offset(source_tile_idx);
|
|
|
|
|
|
|
|
|
|
let source_tile_was_active = self.active_column_idx == source_col_idx
|
|
|
|
|
&& source_column.active_tile_idx == source_tile_idx;
|
|
|
|
|
|
|
|
|
|
if source_column.tiles.len() == 1 {
|
|
|
|
|
if source_col_idx + 1 == self.columns.len() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Move into adjacent column.
|
|
|
|
|
let target_column_idx = source_col_idx;
|
|
|
|
|
|
|
|
|
|
offset.x += cur_x - self.column_x(source_col_idx + 1);
|
|
|
|
|
offset.x -= self.columns[source_col_idx + 1].render_offset().x;
|
|
|
|
|
|
|
|
|
|
if source_tile_was_active {
|
|
|
|
|
// Make sure the target column gets activated.
|
|
|
|
|
self.activate_prev_column_on_removal = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let RemovedTile { tile, .. } = self.remove_tile_by_idx(
|
|
|
|
|
source_col_idx,
|
|
|
|
|
0,
|
|
|
|
|
Transaction::new(),
|
|
|
|
|
Some(self.options.animations.window_movement.0),
|
|
|
|
|
);
|
|
|
|
|
self.add_tile_to_column(target_column_idx, None, tile, source_tile_was_active);
|
|
|
|
|
|
|
|
|
|
let target_column = &mut self.columns[target_column_idx];
|
|
|
|
|
offset += prev_off - target_column.tile_offset(target_column.tiles.len() - 1);
|
|
|
|
|
|
|
|
|
|
let new_tile = target_column.tiles.last_mut().unwrap();
|
|
|
|
|
new_tile.animate_move_from(offset);
|
|
|
|
|
} else {
|
|
|
|
|
// Move out of column.
|
|
|
|
|
let prev_width = self.data[source_col_idx].width;
|
|
|
|
|
|
|
|
|
|
let removed =
|
|
|
|
|
self.remove_tile_by_idx(source_col_idx, source_tile_idx, Transaction::new(), None);
|
|
|
|
|
|
|
|
|
|
let target_column_idx = source_col_idx + 1;
|
|
|
|
|
|
|
|
|
|
self.add_tile(
|
|
|
|
|
Some(target_column_idx),
|
|
|
|
|
removed.tile,
|
|
|
|
|
source_tile_was_active,
|
|
|
|
|
removed.width,
|
|
|
|
|
removed.is_full_width,
|
|
|
|
|
Some(self.options.animations.window_movement.0),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
offset.x += if self.active_column_idx <= target_column_idx {
|
|
|
|
|
// Tiles to the right animate to the following column.
|
|
|
|
|
cur_x - self.column_x(target_column_idx)
|
|
|
|
|
} else {
|
|
|
|
|
// Tiles to the left animate for a change in width.
|
|
|
|
|
-f64::max(0., prev_width - self.data[target_column_idx].width)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let new_col = &mut self.columns[target_column_idx];
|
|
|
|
|
offset += prev_off - new_col.tile_offset(0);
|
|
|
|
|
new_col.tiles[0].animate_move_from(offset);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn consume_into_column(&mut self) {
|
|
|
|
|
if self.columns.len() < 2 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.active_column_idx == self.columns.len() - 1 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let target_column_idx = self.active_column_idx;
|
|
|
|
|
let source_column_idx = self.active_column_idx + 1;
|
|
|
|
|
|
|
|
|
|
let offset = self.column_x(source_column_idx)
|
|
|
|
|
+ self.columns[source_column_idx].render_offset().x
|
|
|
|
|
- self.column_x(target_column_idx);
|
|
|
|
|
let mut offset = Point::from((offset, 0.));
|
|
|
|
|
let prev_off = self.columns[source_column_idx].tile_offset(0);
|
|
|
|
|
|
|
|
|
|
let removed = self.remove_tile_by_idx(source_column_idx, 0, Transaction::new(), None);
|
|
|
|
|
self.add_tile_to_column(target_column_idx, None, removed.tile, false);
|
|
|
|
|
|
|
|
|
|
let target_column = &mut self.columns[target_column_idx];
|
|
|
|
|
offset += prev_off - target_column.tile_offset(target_column.tiles.len() - 1);
|
|
|
|
|
offset.x -= target_column.render_offset().x;
|
|
|
|
|
|
|
|
|
|
let new_tile = target_column.tiles.last_mut().unwrap();
|
|
|
|
|
new_tile.animate_move_from(offset);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn expel_from_column(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let source_col_idx = self.active_column_idx;
|
|
|
|
|
let target_col_idx = self.active_column_idx + 1;
|
|
|
|
|
let cur_x = self.column_x(source_col_idx);
|
|
|
|
|
|
|
|
|
|
let source_column = &self.columns[self.active_column_idx];
|
|
|
|
|
if source_column.tiles.len() == 1 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let source_tile_idx = source_column.tiles.len() - 1;
|
|
|
|
|
|
|
|
|
|
let mut offset = Point::from((source_column.render_offset().x, 0.));
|
|
|
|
|
let prev_off = source_column.tile_offset(source_tile_idx);
|
|
|
|
|
|
|
|
|
|
let removed =
|
|
|
|
|
self.remove_tile_by_idx(source_col_idx, source_tile_idx, Transaction::new(), None);
|
|
|
|
|
|
|
|
|
|
self.add_tile(
|
|
|
|
|
Some(target_col_idx),
|
|
|
|
|
removed.tile,
|
|
|
|
|
false,
|
|
|
|
|
removed.width,
|
|
|
|
|
removed.is_full_width,
|
|
|
|
|
Some(self.options.animations.window_movement.0),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
offset.x += cur_x - self.column_x(target_col_idx);
|
|
|
|
|
|
|
|
|
|
let new_col = &mut self.columns[target_col_idx];
|
|
|
|
|
offset += prev_off - new_col.tile_offset(0);
|
|
|
|
|
new_col.tiles[0].animate_move_from(offset);
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-09 08:29:36 +00:00
|
|
|
pub fn swap_window_in_direction(&mut self, direction: ScrollDirection) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if this is the first (resp. last column), then this operation is equivalent
|
|
|
|
|
// to an `consume_or_expel_window_left` (resp. `consume_or_expel_window_right`)
|
|
|
|
|
match direction {
|
|
|
|
|
ScrollDirection::Left => {
|
|
|
|
|
if self.active_column_idx == 0 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ScrollDirection::Right => {
|
|
|
|
|
if self.active_column_idx == self.columns.len() - 1 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let source_column_idx = self.active_column_idx;
|
|
|
|
|
let target_column_idx = self.active_column_idx.wrapping_add_signed(match direction {
|
|
|
|
|
ScrollDirection::Left => -1,
|
|
|
|
|
ScrollDirection::Right => 1,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// if both source and target columns contain a single tile, then the operation is equivalent
|
|
|
|
|
// to a simple column move
|
|
|
|
|
if self.columns[source_column_idx].tiles.len() == 1
|
|
|
|
|
&& self.columns[target_column_idx].tiles.len() == 1
|
|
|
|
|
{
|
|
|
|
|
return self.move_column_to(target_column_idx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let source_tile_idx = self.columns[source_column_idx].active_tile_idx;
|
|
|
|
|
let target_tile_idx = self.columns[target_column_idx].active_tile_idx;
|
|
|
|
|
let source_column_drained = self.columns[source_column_idx].tiles.len() == 1;
|
|
|
|
|
|
|
|
|
|
// capture the original positions of the tiles
|
|
|
|
|
let (mut source_pt, mut target_pt) = (
|
|
|
|
|
self.columns[source_column_idx].render_offset()
|
|
|
|
|
+ self.columns[source_column_idx].tile_offset(source_tile_idx),
|
|
|
|
|
self.columns[target_column_idx].render_offset()
|
|
|
|
|
+ self.columns[target_column_idx].tile_offset(target_tile_idx),
|
|
|
|
|
);
|
|
|
|
|
source_pt.x += self.column_x(source_column_idx);
|
|
|
|
|
target_pt.x += self.column_x(target_column_idx);
|
|
|
|
|
|
|
|
|
|
let transaction = Transaction::new();
|
|
|
|
|
|
|
|
|
|
// If the source column contains a single tile, this will also remove the column.
|
|
|
|
|
// When this happens `source_column_drained` will be set and the column will need to be
|
|
|
|
|
// recreated with `add_tile`
|
|
|
|
|
let source_removed = self.remove_tile_by_idx(
|
|
|
|
|
source_column_idx,
|
|
|
|
|
source_tile_idx,
|
|
|
|
|
transaction.clone(),
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
// special case when the source column disappears after removing its last tile
|
|
|
|
|
let adjusted_target_column_idx =
|
|
|
|
|
if direction == ScrollDirection::Right && source_column_drained {
|
|
|
|
|
target_column_idx - 1
|
|
|
|
|
} else {
|
|
|
|
|
target_column_idx
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.add_tile_to_column(
|
|
|
|
|
adjusted_target_column_idx,
|
|
|
|
|
Some(target_tile_idx),
|
|
|
|
|
source_removed.tile,
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let RemovedTile {
|
|
|
|
|
tile: target_tile, ..
|
|
|
|
|
} = self.remove_tile_by_idx(
|
|
|
|
|
adjusted_target_column_idx,
|
|
|
|
|
target_tile_idx + 1,
|
|
|
|
|
transaction.clone(),
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if source_column_drained {
|
|
|
|
|
// recreate the drained column with only the target tile
|
|
|
|
|
self.add_tile(
|
|
|
|
|
Some(source_column_idx),
|
|
|
|
|
target_tile,
|
|
|
|
|
true,
|
|
|
|
|
source_removed.width,
|
|
|
|
|
source_removed.is_full_width,
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
// simply add the removed target tile to the source column
|
|
|
|
|
self.add_tile_to_column(
|
|
|
|
|
source_column_idx,
|
|
|
|
|
Some(source_tile_idx),
|
|
|
|
|
target_tile,
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// update the active tile in the modified columns
|
|
|
|
|
self.columns[source_column_idx].active_tile_idx = source_tile_idx;
|
|
|
|
|
self.columns[target_column_idx].active_tile_idx = target_tile_idx;
|
|
|
|
|
|
|
|
|
|
// Animations
|
|
|
|
|
self.columns[target_column_idx].tiles[target_tile_idx]
|
|
|
|
|
.animate_move_from(source_pt - target_pt);
|
2025-02-08 15:24:02 +03:00
|
|
|
self.columns[target_column_idx].tiles[target_tile_idx].ensure_alpha_animates_to_1();
|
2025-01-09 08:29:36 +00:00
|
|
|
|
|
|
|
|
// FIXME: this stop_move_animations() causes the target tile animation to "reset" when
|
|
|
|
|
// swapping. It's here as a workaround to stop the unwanted animation of moving the source
|
|
|
|
|
// tile down when adding the target tile above it. This code needs to be written in some
|
|
|
|
|
// other way not to trigger that animation, or to cancel it properly, so that swap doesn't
|
|
|
|
|
// cancel all ongoing target tile animations.
|
|
|
|
|
self.columns[source_column_idx].tiles[source_tile_idx].stop_move_animations();
|
|
|
|
|
self.columns[source_column_idx].tiles[source_tile_idx]
|
|
|
|
|
.animate_move_from(target_pt - source_pt);
|
2025-02-08 15:24:02 +03:00
|
|
|
self.columns[source_column_idx].tiles[source_tile_idx].ensure_alpha_animates_to_1();
|
2025-01-09 08:29:36 +00:00
|
|
|
|
|
|
|
|
self.activate_column(target_column_idx);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
pub fn toggle_column_tabbed_display(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col = &mut self.columns[self.active_column_idx];
|
2025-02-06 08:42:09 +03:00
|
|
|
let display = match col.display_mode {
|
|
|
|
|
ColumnDisplay::Normal => ColumnDisplay::Tabbed,
|
|
|
|
|
ColumnDisplay::Tabbed => ColumnDisplay::Normal,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.set_column_display(display);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_column_display(&mut self, display: ColumnDisplay) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col = &mut self.columns[self.active_column_idx];
|
|
|
|
|
if col.display_mode == display {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
2025-02-06 08:42:09 +03:00
|
|
|
col.set_column_display(display);
|
2025-02-04 09:51:23 +03:00
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
// With place_within_column, the tab indicator changes the column size immediately.
|
|
|
|
|
self.data[self.active_column_idx].update(col);
|
|
|
|
|
col.update_tile_sizes(true);
|
|
|
|
|
|
2025-02-04 09:51:23 +03:00
|
|
|
// Disable fullscreen if needed.
|
|
|
|
|
if col.display_mode != ColumnDisplay::Tabbed && col.tiles.len() > 1 {
|
|
|
|
|
let window = col.tiles[col.active_tile_idx].window().id().clone();
|
|
|
|
|
self.set_fullscreen(&window, false);
|
|
|
|
|
}
|
2025-02-01 10:46:52 +03:00
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn center_column(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.animate_view_offset_to_column_centered(
|
|
|
|
|
None,
|
|
|
|
|
self.active_column_idx,
|
|
|
|
|
self.options.animations.horizontal_view_movement.0,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let col = &mut self.columns[self.active_column_idx];
|
|
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-29 22:44:19 +03:00
|
|
|
pub fn center_window(&mut self, window: Option<&W::Id>) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col_idx = if let Some(window) = window {
|
|
|
|
|
self.columns
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|col| col.contains(window))
|
|
|
|
|
.unwrap()
|
|
|
|
|
} else {
|
|
|
|
|
self.active_column_idx
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// We can reasonably center only the active column.
|
|
|
|
|
if col_idx != self.active_column_idx {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.center_column();
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-12 14:11:38 +03:00
|
|
|
pub fn center_visible_columns(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.is_centering_focused_column() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Consider the end of an ongoing animation because that's what compute to fit does too.
|
|
|
|
|
let view_x = self.target_view_pos();
|
|
|
|
|
let working_x = self.working_area.loc.x;
|
|
|
|
|
let working_w = self.working_area.size.w;
|
|
|
|
|
|
|
|
|
|
// Count all columns that are fully visible inside the working area.
|
|
|
|
|
let mut width_taken = 0.;
|
|
|
|
|
let mut leftmost_col_x = None;
|
|
|
|
|
let mut active_col_x = None;
|
|
|
|
|
|
|
|
|
|
let gap = self.options.gaps;
|
|
|
|
|
let col_xs = self.column_xs(self.data.iter().copied());
|
|
|
|
|
for (idx, col_x) in col_xs.take(self.columns.len()).enumerate() {
|
|
|
|
|
if col_x < view_x + working_x + gap {
|
|
|
|
|
// Column goes off-screen to the left.
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
leftmost_col_x.get_or_insert(col_x);
|
|
|
|
|
|
|
|
|
|
let width = self.data[idx].width;
|
|
|
|
|
if view_x + working_x + working_w < col_x + width + gap {
|
|
|
|
|
// Column goes off-screen to the right. We can stop here.
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if idx == self.active_column_idx {
|
|
|
|
|
active_col_x = Some(col_x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
width_taken += width + gap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if active_col_x.is_none() {
|
|
|
|
|
// The active column wasn't fully on screen, so we can't meaningfully do anything.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col = &mut self.columns[self.active_column_idx];
|
|
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
|
|
|
|
|
let free_space = working_w - width_taken + gap;
|
|
|
|
|
let new_view_x = leftmost_col_x.unwrap() - free_space / 2. - working_x;
|
|
|
|
|
|
|
|
|
|
self.animate_view_offset(self.active_column_idx, new_view_x - active_col_x.unwrap());
|
|
|
|
|
// Just in case.
|
|
|
|
|
self.animate_view_offset_to_column(None, self.active_column_idx, None);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn view_pos(&self) -> f64 {
|
|
|
|
|
self.column_x(self.active_column_idx) + self.view_offset.current()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn target_view_pos(&self) -> f64 {
|
|
|
|
|
self.column_x(self.active_column_idx) + self.view_offset.target()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HACK: pass a self.data iterator in manually as a workaround for the lack of method partial
|
|
|
|
|
// borrowing. Note that this method's return value does not borrow the entire &Self!
|
|
|
|
|
fn column_xs(&self, data: impl Iterator<Item = ColumnData>) -> impl Iterator<Item = f64> {
|
|
|
|
|
let gaps = self.options.gaps;
|
|
|
|
|
let mut x = 0.;
|
|
|
|
|
|
|
|
|
|
// Chain with a dummy value to be able to get one past all columns' X.
|
|
|
|
|
let dummy = ColumnData { width: 0. };
|
|
|
|
|
let data = data.chain(iter::once(dummy));
|
|
|
|
|
|
|
|
|
|
data.map(move |data| {
|
|
|
|
|
let rv = x;
|
|
|
|
|
x += data.width + gaps;
|
|
|
|
|
rv
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn column_x(&self, column_idx: usize) -> f64 {
|
|
|
|
|
self.column_xs(self.data.iter().copied())
|
|
|
|
|
.nth(column_idx)
|
|
|
|
|
.unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn column_xs_in_render_order(
|
|
|
|
|
&self,
|
|
|
|
|
data: impl Iterator<Item = ColumnData>,
|
|
|
|
|
) -> impl Iterator<Item = f64> {
|
|
|
|
|
let active_idx = self.active_column_idx;
|
|
|
|
|
let active_pos = self.column_x(active_idx);
|
|
|
|
|
let offsets = self
|
|
|
|
|
.column_xs(data)
|
|
|
|
|
.enumerate()
|
|
|
|
|
.filter_map(move |(idx, pos)| (idx != active_idx).then_some(pos));
|
|
|
|
|
iter::once(active_pos).chain(offsets)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn columns_mut(&mut self) -> impl Iterator<Item = (&mut Column<W>, f64)> + '_ {
|
|
|
|
|
let offsets = self.column_xs(self.data.iter().copied());
|
|
|
|
|
zip(&mut self.columns, offsets)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn columns_in_render_order(&self) -> impl Iterator<Item = (&Column<W>, f64)> + '_ {
|
|
|
|
|
let offsets = self.column_xs_in_render_order(self.data.iter().copied());
|
|
|
|
|
|
|
|
|
|
let (first, active, rest) = if self.columns.is_empty() {
|
|
|
|
|
(&[][..], &[][..], &[][..])
|
|
|
|
|
} else {
|
|
|
|
|
let (first, rest) = self.columns.split_at(self.active_column_idx);
|
|
|
|
|
let (active, rest) = rest.split_at(1);
|
|
|
|
|
(first, active, rest)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let columns = active.iter().chain(first).chain(rest);
|
|
|
|
|
zip(columns, offsets)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn columns_in_render_order_mut(&mut self) -> impl Iterator<Item = (&mut Column<W>, f64)> + '_ {
|
|
|
|
|
let offsets = self.column_xs_in_render_order(self.data.iter().copied());
|
|
|
|
|
|
|
|
|
|
let (first, active, rest) = if self.columns.is_empty() {
|
|
|
|
|
(&mut [][..], &mut [][..], &mut [][..])
|
|
|
|
|
} else {
|
|
|
|
|
let (first, rest) = self.columns.split_at_mut(self.active_column_idx);
|
|
|
|
|
let (active, rest) = rest.split_at_mut(1);
|
|
|
|
|
(first, active, rest)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let columns = active.iter_mut().chain(first).chain(rest);
|
|
|
|
|
zip(columns, offsets)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn tiles_with_render_positions(
|
|
|
|
|
&self,
|
2025-02-01 10:46:52 +03:00
|
|
|
) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> {
|
2024-11-30 09:18:33 +03:00
|
|
|
let scale = self.scale;
|
|
|
|
|
let view_off = Point::from((-self.view_pos(), 0.));
|
|
|
|
|
self.columns_in_render_order()
|
|
|
|
|
.flat_map(move |(col, col_x)| {
|
|
|
|
|
let col_off = Point::from((col_x, 0.));
|
|
|
|
|
let col_render_off = col.render_offset();
|
2025-02-01 10:46:52 +03:00
|
|
|
col.tiles_in_render_order()
|
|
|
|
|
.map(move |(tile, tile_off, visible)| {
|
|
|
|
|
let pos =
|
|
|
|
|
view_off + col_off + col_render_off + tile_off + tile.render_offset();
|
|
|
|
|
// Round to physical pixels.
|
|
|
|
|
let pos = pos.to_physical_precise_round(scale).to_logical(scale);
|
|
|
|
|
(tile, pos, visible)
|
|
|
|
|
})
|
2024-11-30 09:18:33 +03:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn tiles_with_render_positions_mut(
|
|
|
|
|
&mut self,
|
|
|
|
|
round: bool,
|
|
|
|
|
) -> impl Iterator<Item = (&mut Tile<W>, Point<f64, Logical>)> {
|
|
|
|
|
let scale = self.scale;
|
|
|
|
|
let view_off = Point::from((-self.view_pos(), 0.));
|
|
|
|
|
self.columns_in_render_order_mut()
|
|
|
|
|
.flat_map(move |(col, col_x)| {
|
|
|
|
|
let col_off = Point::from((col_x, 0.));
|
|
|
|
|
let col_render_off = col.render_offset();
|
|
|
|
|
col.tiles_in_render_order_mut()
|
|
|
|
|
.map(move |(tile, tile_off)| {
|
|
|
|
|
let mut pos =
|
|
|
|
|
view_off + col_off + col_render_off + tile_off + tile.render_offset();
|
|
|
|
|
// Round to physical pixels.
|
|
|
|
|
if round {
|
|
|
|
|
pos = pos.to_physical_precise_round(scale).to_logical(scale);
|
|
|
|
|
}
|
|
|
|
|
(tile, pos)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-17 11:00:17 +03:00
|
|
|
pub(super) fn insert_hint_area(
|
|
|
|
|
&self,
|
|
|
|
|
position: InsertPosition,
|
|
|
|
|
) -> Option<Rectangle<f64, Logical>> {
|
|
|
|
|
let mut hint_area = match position {
|
2024-11-30 09:18:33 +03:00
|
|
|
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;
|
|
|
|
|
}
|
2025-01-04 10:14:51 +03:00
|
|
|
Rectangle::new(loc, size)
|
2024-11-30 09:18:33 +03:00
|
|
|
} 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,
|
|
|
|
|
));
|
2025-01-04 10:14:51 +03:00
|
|
|
Rectangle::new(loc, size)
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
InsertPosition::InColumn(column_index, tile_index) => {
|
|
|
|
|
if column_index > self.columns.len() {
|
|
|
|
|
error!("insert hint column index is out of range");
|
|
|
|
|
return None;
|
|
|
|
|
}
|
2025-02-06 10:15:43 +03:00
|
|
|
|
|
|
|
|
let col = &self.columns[column_index];
|
|
|
|
|
if tile_index > col.tiles.len() {
|
2024-11-30 09:18:33 +03:00
|
|
|
error!("insert hint tile index is out of range");
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
let is_tabbed = col.display_mode == ColumnDisplay::Tabbed;
|
|
|
|
|
|
|
|
|
|
let (height, y) = if is_tabbed {
|
|
|
|
|
// In tabbed mode, there's only one tile visible, and we want to draw the hint
|
|
|
|
|
// at its top or bottom.
|
|
|
|
|
let top = col.tile_offset(col.active_tile_idx).y;
|
|
|
|
|
let bottom = top + col.data[col.active_tile_idx].size.h;
|
|
|
|
|
|
|
|
|
|
if tile_index <= col.active_tile_idx {
|
|
|
|
|
(150., top)
|
|
|
|
|
} else {
|
|
|
|
|
(150., bottom - 150.)
|
|
|
|
|
}
|
2024-11-30 09:18:33 +03:00
|
|
|
} else {
|
2025-02-01 10:46:52 +03:00
|
|
|
let top = col.tile_offset(tile_index).y;
|
|
|
|
|
|
|
|
|
|
if tile_index == 0 {
|
|
|
|
|
(150., top)
|
|
|
|
|
} else if tile_index == col.tiles.len() {
|
|
|
|
|
(150., top - self.options.gaps - 150.)
|
|
|
|
|
} else {
|
|
|
|
|
(300., top - self.options.gaps / 2. - 150.)
|
|
|
|
|
}
|
2024-11-30 09:18:33 +03:00
|
|
|
};
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
// Adjust for place-within-column tab indicator.
|
|
|
|
|
let origin_x = col.tiles_origin().x;
|
|
|
|
|
let extra_w = if is_tabbed && !col.is_fullscreen {
|
|
|
|
|
col.tab_indicator.extra_size(col.tiles.len(), col.scale).w
|
|
|
|
|
} else {
|
|
|
|
|
0.
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let size = Size::from((self.data[column_index].width - extra_w, height));
|
|
|
|
|
let loc = Point::from((self.column_x(column_index) + origin_x, y));
|
2025-01-04 10:14:51 +03:00
|
|
|
Rectangle::new(loc, size)
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
2024-11-29 21:11:02 +03:00
|
|
|
InsertPosition::Floating => return None,
|
2024-11-30 09:18:33 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 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(Some(0.), 0., hint_area.size.w, false)
|
|
|
|
|
} else {
|
|
|
|
|
self.compute_new_view_offset_fit(Some(0.), 0., hint_area.size.w, false)
|
|
|
|
|
};
|
|
|
|
|
hint_area.loc.x -= view_offset;
|
|
|
|
|
} else {
|
|
|
|
|
hint_area.loc.x -= self.view_pos();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Some(hint_area)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the geometry of the active tile relative to and clamped to the view.
|
|
|
|
|
///
|
|
|
|
|
/// During animations, assumes the final view position.
|
|
|
|
|
pub fn active_tile_visual_rectangle(&self) -> Option<Rectangle<f64, Logical>> {
|
|
|
|
|
let col = self.columns.get(self.active_column_idx)?;
|
|
|
|
|
|
|
|
|
|
let final_view_offset = self.view_offset.target();
|
|
|
|
|
let view_off = Point::from((-final_view_offset, 0.));
|
|
|
|
|
|
|
|
|
|
let (tile, tile_off) = col.tiles().nth(col.active_tile_idx).unwrap();
|
|
|
|
|
|
|
|
|
|
let tile_pos = view_off + tile_off;
|
|
|
|
|
let tile_size = tile.tile_size();
|
2025-01-04 10:14:51 +03:00
|
|
|
let tile_rect = Rectangle::new(tile_pos, tile_size);
|
2024-11-30 09:18:33 +03:00
|
|
|
|
2025-01-04 10:14:51 +03:00
|
|
|
let view = Rectangle::from_size(self.view_size);
|
2024-11-30 09:18:33 +03:00
|
|
|
view.intersection(tile_rect)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn popup_target_rect(&self, id: &W::Id) -> Option<Rectangle<f64, Logical>> {
|
2025-06-09 13:39:08 +03:00
|
|
|
for col in &self.columns {
|
|
|
|
|
for (tile, pos) in col.tiles() {
|
|
|
|
|
if tile.window().id() == id {
|
|
|
|
|
// In the scrolling layout, we try to position popups horizontally within the
|
|
|
|
|
// window geometry (so they remain visible even if the window scrolls flush with
|
|
|
|
|
// the left/right edge of the screen), and vertically wihin the whole view size.
|
|
|
|
|
let width = tile.window_size().w;
|
|
|
|
|
let height = self.view_size.h;
|
|
|
|
|
|
|
|
|
|
let mut target = Rectangle::from_size(Size::from((width, height)));
|
|
|
|
|
target.loc.y -= pos.y;
|
|
|
|
|
target.loc.y -= tile.window_loc().y;
|
|
|
|
|
|
|
|
|
|
return Some(target);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn toggle_width(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col = &mut self.columns[self.active_column_idx];
|
2025-01-31 19:24:26 +03:00
|
|
|
col.toggle_width(None);
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn toggle_full_width(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col = &mut self.columns[self.active_column_idx];
|
|
|
|
|
col.toggle_full_width();
|
|
|
|
|
|
|
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-28 10:12:50 +03:00
|
|
|
pub fn set_window_width(&mut self, window: Option<&W::Id>, change: SizeChange) {
|
2024-11-30 09:18:33 +03:00
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-28 10:12:50 +03:00
|
|
|
let (col, tile_idx) = if let Some(window) = window {
|
|
|
|
|
self.columns
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.find_map(|col| {
|
|
|
|
|
col.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|tile| tile.window().id() == window)
|
|
|
|
|
.map(|tile_idx| (col, Some(tile_idx)))
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
} else {
|
|
|
|
|
(&mut self.columns[self.active_column_idx], None)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
col.set_column_width(change, tile_idx, true);
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn set_window_height(&mut self, window: Option<&W::Id>, change: SizeChange) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (col, tile_idx) = if let Some(window) = window {
|
|
|
|
|
self.columns
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.find_map(|col| {
|
|
|
|
|
col.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|tile| tile.window().id() == window)
|
|
|
|
|
.map(|tile_idx| (col, Some(tile_idx)))
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
} else {
|
|
|
|
|
(&mut self.columns[self.active_column_idx], None)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
col.set_window_height(change, tile_idx, true);
|
|
|
|
|
|
|
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn reset_window_height(&mut self, window: Option<&W::Id>) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (col, tile_idx) = if let Some(window) = window {
|
|
|
|
|
self.columns
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.find_map(|col| {
|
|
|
|
|
col.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|tile| tile.window().id() == window)
|
|
|
|
|
.map(|tile_idx| (col, Some(tile_idx)))
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
} else {
|
|
|
|
|
(&mut self.columns[self.active_column_idx], None)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
col.reset_window_height(tile_idx, true);
|
|
|
|
|
|
|
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-30 09:05:35 +03:00
|
|
|
pub fn toggle_window_width(&mut self, window: Option<&W::Id>) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-31 19:24:26 +03:00
|
|
|
let (col, tile_idx) = if let Some(window) = window {
|
2024-12-30 09:05:35 +03:00
|
|
|
self.columns
|
|
|
|
|
.iter_mut()
|
2025-01-31 19:24:26 +03:00
|
|
|
.find_map(|col| {
|
|
|
|
|
col.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|tile| tile.window().id() == window)
|
|
|
|
|
.map(|tile_idx| (col, Some(tile_idx)))
|
|
|
|
|
})
|
2024-12-30 09:05:35 +03:00
|
|
|
.unwrap()
|
|
|
|
|
} else {
|
2025-01-31 19:24:26 +03:00
|
|
|
(&mut self.columns[self.active_column_idx], None)
|
2024-12-30 09:05:35 +03:00
|
|
|
};
|
|
|
|
|
|
2025-01-31 19:24:26 +03:00
|
|
|
col.toggle_width(tile_idx);
|
2024-12-30 09:05:35 +03:00
|
|
|
|
|
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn toggle_window_height(&mut self, window: Option<&W::Id>) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let (col, tile_idx) = if let Some(window) = window {
|
|
|
|
|
self.columns
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.find_map(|col| {
|
|
|
|
|
col.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|tile| tile.window().id() == window)
|
|
|
|
|
.map(|tile_idx| (col, Some(tile_idx)))
|
|
|
|
|
})
|
|
|
|
|
.unwrap()
|
|
|
|
|
} else {
|
|
|
|
|
(&mut self.columns[self.active_column_idx], None)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
col.toggle_window_height(tile_idx, true);
|
|
|
|
|
|
|
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-17 21:22:10 +03:00
|
|
|
pub fn expand_column_to_available_width(&mut self) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col = &mut self.columns[self.active_column_idx];
|
|
|
|
|
if col.is_fullscreen || col.is_full_width {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.is_centering_focused_column() {
|
|
|
|
|
// Always-centered mode is different since the active window position cannot be
|
|
|
|
|
// controlled (it's always at the center). I guess you could come up with different
|
|
|
|
|
// logic here that computes the width in such a way so as to leave nearby columns fully
|
|
|
|
|
// on screen while taking into account that the active column will remain centered
|
|
|
|
|
// after resizing. But I'm not sure it's that useful? So let's do the simple thing.
|
|
|
|
|
let col = &mut self.columns[self.active_column_idx];
|
2025-02-17 21:47:28 +03:00
|
|
|
col.toggle_full_width();
|
2025-02-17 21:22:10 +03:00
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Consider the end of an ongoing animation because that's what compute to fit does too.
|
|
|
|
|
let view_x = self.target_view_pos();
|
|
|
|
|
let working_x = self.working_area.loc.x;
|
|
|
|
|
let working_w = self.working_area.size.w;
|
|
|
|
|
|
|
|
|
|
// Count all columns that are fully visible inside the working area.
|
|
|
|
|
let mut width_taken = 0.;
|
|
|
|
|
let mut leftmost_col_x = None;
|
|
|
|
|
let mut active_col_x = None;
|
2025-02-17 21:56:53 +03:00
|
|
|
let mut counted_non_active_column = false;
|
2025-02-17 21:22:10 +03:00
|
|
|
|
|
|
|
|
let gap = self.options.gaps;
|
|
|
|
|
let col_xs = self.column_xs(self.data.iter().copied());
|
|
|
|
|
for (idx, col_x) in col_xs.take(self.columns.len()).enumerate() {
|
|
|
|
|
if col_x < view_x + working_x + gap {
|
|
|
|
|
// Column goes off-screen to the left.
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
leftmost_col_x.get_or_insert(col_x);
|
|
|
|
|
|
|
|
|
|
let width = self.data[idx].width;
|
|
|
|
|
if view_x + working_x + working_w < col_x + width + gap {
|
|
|
|
|
// Column goes off-screen to the right. We can stop here.
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if idx == self.active_column_idx {
|
|
|
|
|
active_col_x = Some(col_x);
|
2025-02-17 21:56:53 +03:00
|
|
|
} else {
|
|
|
|
|
counted_non_active_column = true;
|
2025-02-17 21:22:10 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
width_taken += width + gap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if active_col_x.is_none() {
|
|
|
|
|
// The active column wasn't fully on screen, so we can't meaningfully do anything.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-17 22:36:09 +03:00
|
|
|
let col = &mut self.columns[self.active_column_idx];
|
|
|
|
|
|
|
|
|
|
let available_width = working_w - gap - width_taken - col.extra_size().w;
|
2025-02-17 21:22:10 +03:00
|
|
|
if available_width <= 0. {
|
|
|
|
|
// Nowhere to expand.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-17 21:56:53 +03:00
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
|
|
|
|
|
if !counted_non_active_column {
|
|
|
|
|
// Only the active column was fully on-screen (maybe it's the only column), so we're
|
|
|
|
|
// about to set its width to 100% of the working area. Let's do it via
|
|
|
|
|
// toggle_full_width() as it lets you back out of it more intuitively.
|
|
|
|
|
col.toggle_full_width();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let active_width = self.data[self.active_column_idx].width;
|
2025-02-17 21:22:10 +03:00
|
|
|
col.width = ColumnWidth::Fixed(active_width + available_width);
|
|
|
|
|
col.preset_width_idx = None;
|
|
|
|
|
col.is_full_width = false;
|
|
|
|
|
col.update_tile_sizes(true);
|
|
|
|
|
|
|
|
|
|
// Put the leftmost window into the view.
|
|
|
|
|
let new_view_x = leftmost_col_x.unwrap() - gap - working_x;
|
|
|
|
|
self.animate_view_offset(self.active_column_idx, new_view_x - active_col_x.unwrap());
|
|
|
|
|
// Just in case.
|
|
|
|
|
self.animate_view_offset_to_column(None, self.active_column_idx, None);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-16 09:03:50 +03:00
|
|
|
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) -> bool {
|
2024-11-30 09:18:33 +03:00
|
|
|
let (mut col_idx, tile_idx) = self
|
|
|
|
|
.columns
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.find_map(|(col_idx, col)| col.position(window).map(|tile_idx| (col_idx, tile_idx)))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
if is_fullscreen == self.columns[col_idx].is_fullscreen {
|
2024-12-16 09:03:50 +03:00
|
|
|
return false;
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
2025-02-04 09:51:23 +03:00
|
|
|
let mut col = &mut self.columns[col_idx];
|
|
|
|
|
let is_tabbed = col.display_mode == ColumnDisplay::Tabbed;
|
|
|
|
|
|
|
|
|
|
if is_fullscreen && col_idx == self.active_column_idx && (col.tiles.len() == 1 || is_tabbed)
|
2024-11-30 09:18:33 +03:00
|
|
|
{
|
|
|
|
|
self.view_offset_before_fullscreen = Some(self.view_offset.stationary());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cancel_resize_for_column(&mut self.interactive_resize, col);
|
|
|
|
|
|
2025-02-04 09:51:23 +03:00
|
|
|
if is_fullscreen && (col.tiles.len() > 1 && !is_tabbed) {
|
2024-11-30 09:18:33 +03:00
|
|
|
// This wasn't the only window in its column; extract it into a separate column.
|
|
|
|
|
let activate = self.active_column_idx == col_idx && col.active_tile_idx == tile_idx;
|
|
|
|
|
|
|
|
|
|
let removed = self.remove_tile_by_idx(col_idx, tile_idx, Transaction::new(), None);
|
|
|
|
|
// Create a column manually to disable the resize animation.
|
|
|
|
|
let column = Column::new_with_tile(
|
|
|
|
|
removed.tile,
|
|
|
|
|
self.view_size,
|
|
|
|
|
self.working_area,
|
|
|
|
|
self.scale,
|
|
|
|
|
removed.width,
|
|
|
|
|
removed.is_full_width,
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
self.add_column(Some(col_idx + 1), column, activate, None);
|
|
|
|
|
|
|
|
|
|
col_idx += 1;
|
|
|
|
|
col = &mut self.columns[col_idx];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
col.set_fullscreen(is_fullscreen);
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
// With place_within_column, the tab indicator changes the column size immediately.
|
|
|
|
|
self.data[col_idx].update(col);
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
// If we quickly fullscreen and unfullscreen before any window has a chance to receive the
|
|
|
|
|
// request, we need to reset the offset.
|
|
|
|
|
if col_idx == self.active_column_idx
|
|
|
|
|
&& !is_fullscreen
|
|
|
|
|
&& !col
|
|
|
|
|
.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|tile| tile.is_fullscreen() || tile.window().is_pending_fullscreen())
|
|
|
|
|
{
|
|
|
|
|
self.view_offset_before_fullscreen = None;
|
|
|
|
|
}
|
2024-12-16 09:03:50 +03:00
|
|
|
|
|
|
|
|
true
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn render_above_top_layer(&self) -> bool {
|
|
|
|
|
// Render above the top layer if we're on a fullscreen window and the view is stationary.
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !self.view_offset.is_static() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.columns[self.active_column_idx].is_fullscreen
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn render_elements<R: NiriRenderer>(
|
|
|
|
|
&self,
|
|
|
|
|
renderer: &mut R,
|
|
|
|
|
target: RenderTarget,
|
2024-12-29 11:04:08 +03:00
|
|
|
focus_ring: bool,
|
2024-11-30 09:18:33 +03:00
|
|
|
) -> Vec<ScrollingSpaceRenderElement<R>> {
|
|
|
|
|
let mut rv = vec![];
|
|
|
|
|
|
2025-04-15 09:10:50 +03:00
|
|
|
let scale = Scale::from(self.scale);
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
// Draw the closing windows on top of the other windows.
|
2025-01-04 10:14:51 +03:00
|
|
|
let view_rect = Rectangle::new(Point::from((self.view_pos(), 0.)), self.view_size);
|
2024-11-30 09:18:33 +03:00
|
|
|
for closing in self.closing_windows.iter().rev() {
|
|
|
|
|
let elem = closing.render(renderer.as_gles_renderer(), view_rect, scale, target);
|
|
|
|
|
rv.push(elem.into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return rv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut first = true;
|
|
|
|
|
|
2025-02-02 08:41:42 +03:00
|
|
|
// This matches self.tiles_in_render_order().
|
|
|
|
|
let view_off = Point::from((-self.view_pos(), 0.));
|
|
|
|
|
for (col, col_x) in self.columns_in_render_order() {
|
|
|
|
|
let col_off = Point::from((col_x, 0.));
|
|
|
|
|
let col_render_off = col.render_offset();
|
|
|
|
|
|
|
|
|
|
// Draw the tab indicator on top.
|
|
|
|
|
{
|
2025-02-09 19:14:04 +03:00
|
|
|
let pos = view_off + col_off + col_render_off;
|
2025-02-02 08:41:42 +03:00
|
|
|
let pos = pos.to_physical_precise_round(scale).to_logical(scale);
|
|
|
|
|
rv.extend(col.tab_indicator.render(renderer, pos).map(Into::into));
|
2025-02-01 10:46:52 +03:00
|
|
|
}
|
|
|
|
|
|
2025-02-02 08:41:42 +03:00
|
|
|
for (tile, tile_off, visible) in col.tiles_in_render_order() {
|
|
|
|
|
let tile_pos =
|
|
|
|
|
view_off + col_off + col_render_off + tile_off + tile.render_offset();
|
|
|
|
|
// Round to physical pixels.
|
|
|
|
|
let tile_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale);
|
|
|
|
|
|
|
|
|
|
// And now the drawing logic.
|
|
|
|
|
|
|
|
|
|
// For the active tile (which comes first), draw the focus ring.
|
|
|
|
|
let focus_ring = focus_ring && first;
|
|
|
|
|
first = false;
|
|
|
|
|
|
2025-02-08 15:24:02 +03:00
|
|
|
// In the scrolling layout, we currently use visible only for hidden tabs in the
|
|
|
|
|
// tabbed mode. We want to animate their opacity when going in and out of tabbed
|
|
|
|
|
// mode, so we don't want to apply "visible" immediately. However, "visible" is
|
|
|
|
|
// also used for input handling, and there we *do* want to apply it immediately.
|
|
|
|
|
// So, let's just selectively ignore "visible" here when animating alpha.
|
|
|
|
|
let visible = visible || tile.alpha_animation.is_some();
|
2025-02-02 08:41:42 +03:00
|
|
|
if !visible {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rv.extend(
|
2025-04-15 09:10:50 +03:00
|
|
|
tile.render(renderer, tile_pos, focus_ring, target)
|
2025-02-02 08:41:42 +03:00
|
|
|
.map(Into::into),
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rv
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-10 13:12:53 +03:00
|
|
|
pub fn window_under(&self, pos: Point<f64, Logical>) -> Option<(&W, HitType)> {
|
|
|
|
|
// This matches self.tiles_with_render_positions().
|
|
|
|
|
let scale = self.scale;
|
|
|
|
|
let view_off = Point::from((-self.view_pos(), 0.));
|
|
|
|
|
for (col, col_x) in self.columns_in_render_order() {
|
|
|
|
|
let col_off = Point::from((col_x, 0.));
|
|
|
|
|
let col_render_off = col.render_offset();
|
|
|
|
|
|
|
|
|
|
// Hit the tab indicator.
|
|
|
|
|
if col.display_mode == ColumnDisplay::Tabbed && !col.is_fullscreen {
|
|
|
|
|
let col_pos = view_off + col_off + col_render_off;
|
|
|
|
|
let col_pos = col_pos.to_physical_precise_round(scale).to_logical(scale);
|
|
|
|
|
|
|
|
|
|
if let Some(idx) = col.tab_indicator.hit(
|
|
|
|
|
col.tab_indicator_area(),
|
|
|
|
|
col.tiles.len(),
|
|
|
|
|
scale,
|
|
|
|
|
pos - col_pos,
|
|
|
|
|
) {
|
|
|
|
|
let hit = HitType::Activate {
|
|
|
|
|
is_tab_indicator: true,
|
|
|
|
|
};
|
|
|
|
|
return Some((col.tiles[idx].window(), hit));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (tile, tile_off, visible) in col.tiles_in_render_order() {
|
|
|
|
|
if !visible {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let tile_pos =
|
|
|
|
|
view_off + col_off + col_render_off + tile_off + tile.render_offset();
|
|
|
|
|
// Round to physical pixels.
|
|
|
|
|
let tile_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale);
|
|
|
|
|
|
|
|
|
|
if let Some(rv) = HitType::hit_tile(tile, tile_pos, pos) {
|
|
|
|
|
return Some(rv);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn view_offset_gesture_begin(&mut self, is_touchpad: bool) {
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.interactive_resize.is_some() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let gesture = ViewGesture {
|
|
|
|
|
current_view_offset: self.view_offset.current(),
|
2025-04-24 21:06:24 +03:00
|
|
|
animation: None,
|
2024-11-30 09:18:33 +03:00
|
|
|
tracker: SwipeTracker::new(),
|
|
|
|
|
delta_from_tracker: self.view_offset.current(),
|
|
|
|
|
stationary_view_offset: self.view_offset.stationary(),
|
|
|
|
|
is_touchpad,
|
2025-02-15 11:15:35 +03:00
|
|
|
dnd_last_event_time: None,
|
2025-02-16 08:16:14 +03:00
|
|
|
dnd_nonzero_start_time: None,
|
2024-11-30 09:18:33 +03:00
|
|
|
};
|
|
|
|
|
self.view_offset = ViewOffset::Gesture(gesture);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-15 11:15:35 +03:00
|
|
|
pub fn dnd_scroll_gesture_begin(&mut self) {
|
|
|
|
|
if let ViewOffset::Gesture(ViewGesture {
|
|
|
|
|
dnd_last_event_time: Some(_),
|
|
|
|
|
..
|
|
|
|
|
}) = &self.view_offset
|
|
|
|
|
{
|
|
|
|
|
// Already active.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let gesture = ViewGesture {
|
|
|
|
|
current_view_offset: self.view_offset.current(),
|
2025-04-24 21:06:24 +03:00
|
|
|
animation: None,
|
2025-02-15 11:15:35 +03:00
|
|
|
tracker: SwipeTracker::new(),
|
|
|
|
|
delta_from_tracker: self.view_offset.current(),
|
|
|
|
|
stationary_view_offset: self.view_offset.stationary(),
|
|
|
|
|
is_touchpad: false,
|
|
|
|
|
dnd_last_event_time: Some(self.clock.now_unadjusted()),
|
2025-02-16 08:16:14 +03:00
|
|
|
dnd_nonzero_start_time: None,
|
2025-02-15 11:15:35 +03:00
|
|
|
};
|
|
|
|
|
self.view_offset = ViewOffset::Gesture(gesture);
|
|
|
|
|
|
|
|
|
|
self.interactive_resize = None;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn view_offset_gesture_update(
|
|
|
|
|
&mut self,
|
|
|
|
|
delta_x: f64,
|
|
|
|
|
timestamp: Duration,
|
|
|
|
|
is_touchpad: bool,
|
|
|
|
|
) -> Option<bool> {
|
|
|
|
|
let ViewOffset::Gesture(gesture) = &mut self.view_offset else {
|
|
|
|
|
return None;
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-15 11:15:35 +03:00
|
|
|
if gesture.is_touchpad != is_touchpad || gesture.dnd_last_event_time.is_some() {
|
2024-11-30 09:18:33 +03:00
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
gesture.tracker.push(delta_x, timestamp);
|
|
|
|
|
|
|
|
|
|
let norm_factor = if gesture.is_touchpad {
|
|
|
|
|
self.working_area.size.w / VIEW_GESTURE_WORKING_AREA_MOVEMENT
|
|
|
|
|
} else {
|
|
|
|
|
1.
|
|
|
|
|
};
|
|
|
|
|
let pos = gesture.tracker.pos() * norm_factor;
|
|
|
|
|
let view_offset = pos + gesture.delta_from_tracker;
|
|
|
|
|
gesture.current_view_offset = view_offset;
|
|
|
|
|
|
|
|
|
|
Some(true)
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-25 10:06:46 +03:00
|
|
|
pub fn dnd_scroll_gesture_scroll(&mut self, delta: f64) -> bool {
|
2025-02-15 11:15:35 +03:00
|
|
|
let ViewOffset::Gesture(gesture) = &mut self.view_offset else {
|
2025-04-25 10:06:46 +03:00
|
|
|
return false;
|
2025-02-15 11:15:35 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let Some(last_time) = gesture.dnd_last_event_time else {
|
|
|
|
|
// Not a DnD scroll.
|
2025-04-25 10:06:46 +03:00
|
|
|
return false;
|
2025-02-15 11:15:35 +03:00
|
|
|
};
|
|
|
|
|
|
2025-02-16 08:46:38 +03:00
|
|
|
let config = &self.options.gestures.dnd_edge_view_scroll;
|
|
|
|
|
|
2025-02-15 11:15:35 +03:00
|
|
|
let now = self.clock.now_unadjusted();
|
|
|
|
|
gesture.dnd_last_event_time = Some(now);
|
2025-02-16 08:16:14 +03:00
|
|
|
|
|
|
|
|
if delta == 0. {
|
|
|
|
|
// We're outside the scrolling zone.
|
|
|
|
|
gesture.dnd_nonzero_start_time = None;
|
2025-04-25 10:06:46 +03:00
|
|
|
return false;
|
2025-02-16 08:16:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let nonzero_start = *gesture.dnd_nonzero_start_time.get_or_insert(now);
|
|
|
|
|
|
|
|
|
|
// Delay starting the gesture a bit to avoid unwanted movement when dragging across
|
|
|
|
|
// monitors.
|
2025-02-16 08:46:38 +03:00
|
|
|
let delay = Duration::from_millis(u64::from(config.delay_ms));
|
|
|
|
|
if now.saturating_sub(nonzero_start) < delay {
|
2025-04-25 10:06:46 +03:00
|
|
|
return true;
|
2025-02-16 08:16:14 +03:00
|
|
|
}
|
|
|
|
|
|
2025-02-15 11:15:35 +03:00
|
|
|
let time_delta = now.saturating_sub(last_time).as_secs_f64();
|
|
|
|
|
|
2025-02-16 08:46:38 +03:00
|
|
|
let delta = delta * time_delta * config.max_speed.0;
|
2025-02-15 11:15:35 +03:00
|
|
|
|
|
|
|
|
gesture.tracker.push(delta, now);
|
|
|
|
|
|
|
|
|
|
let view_offset = gesture.tracker.pos() + gesture.delta_from_tracker;
|
|
|
|
|
|
|
|
|
|
// Clamp it so that it doesn't go too much out of bounds.
|
|
|
|
|
let (leftmost, rightmost) = if self.columns.is_empty() {
|
|
|
|
|
(0., 0.)
|
|
|
|
|
} else {
|
|
|
|
|
let gaps = self.options.gaps;
|
|
|
|
|
|
|
|
|
|
let mut leftmost = -self.working_area.size.w;
|
|
|
|
|
|
|
|
|
|
let last_col_idx = self.columns.len() - 1;
|
|
|
|
|
let last_col_x = self
|
|
|
|
|
.columns
|
|
|
|
|
.iter()
|
|
|
|
|
.take(last_col_idx)
|
|
|
|
|
.fold(0., |col_x, col| col_x + col.width() + gaps);
|
|
|
|
|
let last_col_width = self.data[last_col_idx].width;
|
|
|
|
|
let mut rightmost = last_col_x + last_col_width - self.working_area.loc.x;
|
|
|
|
|
|
|
|
|
|
let active_col_x = self
|
|
|
|
|
.columns
|
|
|
|
|
.iter()
|
|
|
|
|
.take(self.active_column_idx)
|
|
|
|
|
.fold(0., |col_x, col| col_x + col.width() + gaps);
|
|
|
|
|
leftmost -= active_col_x;
|
|
|
|
|
rightmost -= active_col_x;
|
|
|
|
|
|
|
|
|
|
(leftmost, rightmost)
|
|
|
|
|
};
|
|
|
|
|
let min_offset = f64::min(leftmost, rightmost);
|
|
|
|
|
let max_offset = f64::max(leftmost, rightmost);
|
|
|
|
|
let clamped_offset = view_offset.clamp(min_offset, max_offset);
|
|
|
|
|
|
|
|
|
|
gesture.delta_from_tracker += clamped_offset - view_offset;
|
|
|
|
|
gesture.current_view_offset = clamped_offset;
|
2025-04-25 10:06:46 +03:00
|
|
|
true
|
2025-02-15 11:15:35 +03:00
|
|
|
}
|
|
|
|
|
|
2025-04-20 12:36:54 +03:00
|
|
|
pub fn view_offset_gesture_end(&mut self, is_touchpad: Option<bool>) -> bool {
|
2024-11-30 09:18:33 +03:00
|
|
|
let ViewOffset::Gesture(gesture) = &mut self.view_offset else {
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-10 09:01:23 +03:00
|
|
|
if is_touchpad.is_some_and(|x| gesture.is_touchpad != x) {
|
2024-11-30 09:18:33 +03:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We do not handle cancelling, just like GNOME Shell doesn't. For this gesture, proper
|
|
|
|
|
// cancelling would require keeping track of the original active column, and then updating
|
|
|
|
|
// it in all the right places (adding columns, removing columns, etc.) -- quite a bit of
|
|
|
|
|
// effort and bug potential.
|
|
|
|
|
|
2025-04-04 12:10:17 +03:00
|
|
|
// Take into account any idle time between the last event and now.
|
|
|
|
|
let now = self.clock.now_unadjusted();
|
|
|
|
|
gesture.tracker.push(0., now);
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
let norm_factor = if gesture.is_touchpad {
|
|
|
|
|
self.working_area.size.w / VIEW_GESTURE_WORKING_AREA_MOVEMENT
|
|
|
|
|
} else {
|
|
|
|
|
1.
|
|
|
|
|
};
|
|
|
|
|
let velocity = gesture.tracker.velocity() * norm_factor;
|
|
|
|
|
let pos = gesture.tracker.pos() * norm_factor;
|
|
|
|
|
let current_view_offset = pos + gesture.delta_from_tracker;
|
|
|
|
|
|
|
|
|
|
if self.columns.is_empty() {
|
|
|
|
|
self.view_offset = ViewOffset::Static(current_view_offset);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Figure out where the gesture would stop after deceleration.
|
|
|
|
|
let end_pos = gesture.tracker.projected_end_pos() * norm_factor;
|
|
|
|
|
let target_view_offset = end_pos + gesture.delta_from_tracker;
|
|
|
|
|
|
|
|
|
|
// Compute the snapping points. These are where the view aligns with column boundaries on
|
|
|
|
|
// either side.
|
|
|
|
|
struct Snap {
|
|
|
|
|
// View position relative to x = 0 (the first column).
|
|
|
|
|
view_pos: f64,
|
|
|
|
|
// Column to activate for this snapping point.
|
|
|
|
|
col_idx: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut snapping_points = Vec::new();
|
|
|
|
|
|
|
|
|
|
let left_strut = self.working_area.loc.x;
|
|
|
|
|
let right_strut = self.view_size.w - self.working_area.size.w - self.working_area.loc.x;
|
|
|
|
|
|
|
|
|
|
if self.is_centering_focused_column() {
|
|
|
|
|
let mut col_x = 0.;
|
|
|
|
|
for (col_idx, col) in self.columns.iter().enumerate() {
|
|
|
|
|
let col_w = col.width();
|
|
|
|
|
|
|
|
|
|
let view_pos = if col.is_fullscreen {
|
|
|
|
|
col_x
|
|
|
|
|
} else if self.working_area.size.w <= col_w {
|
|
|
|
|
col_x - left_strut
|
|
|
|
|
} else {
|
|
|
|
|
col_x - (self.working_area.size.w - col_w) / 2. - left_strut
|
|
|
|
|
};
|
|
|
|
|
snapping_points.push(Snap { view_pos, col_idx });
|
|
|
|
|
|
|
|
|
|
col_x += col_w + self.options.gaps;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-01-30 14:17:16 +00:00
|
|
|
let center_on_overflow = matches!(
|
|
|
|
|
self.options.center_focused_column,
|
|
|
|
|
CenterFocusedColumn::OnOverflow
|
|
|
|
|
);
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
let view_width = self.view_size.w;
|
|
|
|
|
let working_area_width = self.working_area.size.w;
|
|
|
|
|
let gaps = self.options.gaps;
|
|
|
|
|
|
2025-01-30 14:17:16 +00:00
|
|
|
let snap_points =
|
|
|
|
|
|col_x, col: &Column<W>, prev_col_w: Option<f64>, next_col_w: Option<f64>| {
|
|
|
|
|
let col_w = col.width();
|
|
|
|
|
|
|
|
|
|
// Normal columns align with the working area, but fullscreen columns align with
|
|
|
|
|
// the view size.
|
|
|
|
|
if col.is_fullscreen {
|
|
|
|
|
let left = col_x;
|
|
|
|
|
let right = col_x + col_w;
|
|
|
|
|
(left, right)
|
|
|
|
|
} else {
|
|
|
|
|
// Logic from compute_new_view_offset.
|
|
|
|
|
let padding = ((working_area_width - col_w) / 2.).clamp(0., gaps);
|
|
|
|
|
|
|
|
|
|
let center = if self.working_area.size.w <= col_w {
|
|
|
|
|
col_x - left_strut
|
|
|
|
|
} else {
|
|
|
|
|
col_x - (self.working_area.size.w - col_w) / 2. - left_strut
|
|
|
|
|
};
|
|
|
|
|
let is_overflowing = |adj_col_w: Option<f64>| {
|
|
|
|
|
center_on_overflow
|
|
|
|
|
&& adj_col_w
|
|
|
|
|
.filter(|adj_col_w| {
|
|
|
|
|
center_on_overflow
|
|
|
|
|
&& adj_col_w + 3.0 * gaps + col_w > working_area_width
|
|
|
|
|
})
|
|
|
|
|
.is_some()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let left = if is_overflowing(next_col_w) {
|
|
|
|
|
center
|
|
|
|
|
} else {
|
|
|
|
|
col_x - padding - left_strut
|
|
|
|
|
};
|
|
|
|
|
let right = if is_overflowing(prev_col_w) {
|
|
|
|
|
center + view_width
|
|
|
|
|
} else {
|
|
|
|
|
col_x + col_w + padding + right_strut
|
|
|
|
|
};
|
|
|
|
|
(left, right)
|
|
|
|
|
}
|
|
|
|
|
};
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
// Prevent the gesture from snapping further than the first/last column, as this is
|
|
|
|
|
// generally undesired.
|
|
|
|
|
//
|
|
|
|
|
// It's ok if leftmost_snap is > rightmost_snap (this happens if the columns on a
|
|
|
|
|
// workspace total up to less than the workspace width).
|
2025-01-30 14:17:16 +00:00
|
|
|
let leftmost_snap = snap_points(
|
|
|
|
|
0.,
|
|
|
|
|
&self.columns[0],
|
|
|
|
|
None,
|
|
|
|
|
self.columns.get(1).map(|c| c.width()),
|
|
|
|
|
)
|
|
|
|
|
.0;
|
2024-11-30 09:18:33 +03:00
|
|
|
let last_col_idx = self.columns.len() - 1;
|
|
|
|
|
let last_col_x = self
|
|
|
|
|
.columns
|
|
|
|
|
.iter()
|
|
|
|
|
.take(last_col_idx)
|
|
|
|
|
.fold(0., |col_x, col| col_x + col.width() + gaps);
|
2025-01-30 14:17:16 +00:00
|
|
|
let rightmost_snap = snap_points(
|
|
|
|
|
last_col_x,
|
|
|
|
|
&self.columns[last_col_idx],
|
|
|
|
|
last_col_idx
|
|
|
|
|
.checked_sub(1)
|
|
|
|
|
.and_then(|idx| self.columns.get(idx).map(|c| c.width())),
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
.1 - view_width;
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
snapping_points.push(Snap {
|
|
|
|
|
view_pos: leftmost_snap,
|
|
|
|
|
col_idx: 0,
|
|
|
|
|
});
|
|
|
|
|
snapping_points.push(Snap {
|
|
|
|
|
view_pos: rightmost_snap,
|
|
|
|
|
col_idx: last_col_idx,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let mut push = |col_idx, left, right| {
|
|
|
|
|
if leftmost_snap < left && left < rightmost_snap {
|
|
|
|
|
snapping_points.push(Snap {
|
|
|
|
|
view_pos: left,
|
|
|
|
|
col_idx,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let right = right - view_width;
|
|
|
|
|
if leftmost_snap < right && right < rightmost_snap {
|
|
|
|
|
snapping_points.push(Snap {
|
|
|
|
|
view_pos: right,
|
|
|
|
|
col_idx,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut col_x = 0.;
|
|
|
|
|
for (col_idx, col) in self.columns.iter().enumerate() {
|
2025-01-30 14:17:16 +00:00
|
|
|
let (left, right) = snap_points(
|
|
|
|
|
col_x,
|
|
|
|
|
col,
|
|
|
|
|
col_idx
|
|
|
|
|
.checked_sub(1)
|
|
|
|
|
.and_then(|idx| self.columns.get(idx).map(|c| c.width())),
|
|
|
|
|
self.columns.get(col_idx + 1).map(|c| c.width()),
|
|
|
|
|
);
|
2024-11-30 09:18:33 +03:00
|
|
|
push(col_idx, left, right);
|
|
|
|
|
|
|
|
|
|
col_x += col.width() + gaps;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the closest snapping point.
|
|
|
|
|
snapping_points.sort_by_key(|snap| NotNan::new(snap.view_pos).unwrap());
|
|
|
|
|
|
|
|
|
|
let active_col_x = self.column_x(self.active_column_idx);
|
|
|
|
|
let target_view_pos = active_col_x + target_view_offset;
|
|
|
|
|
let target_snap = snapping_points
|
|
|
|
|
.iter()
|
|
|
|
|
.min_by_key(|snap| NotNan::new((snap.view_pos - target_view_pos).abs()).unwrap())
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let mut new_col_idx = target_snap.col_idx;
|
|
|
|
|
|
|
|
|
|
if !self.is_centering_focused_column() {
|
|
|
|
|
// Focus the furthest window towards the direction of the gesture.
|
|
|
|
|
if target_view_offset >= current_view_offset {
|
|
|
|
|
for col_idx in (new_col_idx + 1)..self.columns.len() {
|
|
|
|
|
let col = &self.columns[col_idx];
|
|
|
|
|
let col_x = self.column_x(col_idx);
|
|
|
|
|
let col_w = col.width();
|
|
|
|
|
|
|
|
|
|
if col.is_fullscreen {
|
|
|
|
|
if target_snap.view_pos + self.view_size.w < col_x + col_w {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let padding =
|
|
|
|
|
((self.working_area.size.w - col_w) / 2.).clamp(0., self.options.gaps);
|
|
|
|
|
if target_snap.view_pos + left_strut + self.working_area.size.w
|
|
|
|
|
< col_x + col_w + padding
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
new_col_idx = col_idx;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for col_idx in (0..new_col_idx).rev() {
|
|
|
|
|
let col = &self.columns[col_idx];
|
|
|
|
|
let col_x = self.column_x(col_idx);
|
|
|
|
|
let col_w = col.width();
|
|
|
|
|
|
|
|
|
|
if col.is_fullscreen {
|
|
|
|
|
if col_x < target_snap.view_pos {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let padding =
|
|
|
|
|
((self.working_area.size.w - col_w) / 2.).clamp(0., self.options.gaps);
|
|
|
|
|
if col_x - padding < target_snap.view_pos + left_strut {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
new_col_idx = col_idx;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let new_col_x = self.column_x(new_col_idx);
|
|
|
|
|
let delta = active_col_x - new_col_x;
|
|
|
|
|
|
|
|
|
|
if self.active_column_idx != new_col_idx {
|
|
|
|
|
self.view_offset_before_fullscreen = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.active_column_idx = new_col_idx;
|
|
|
|
|
|
|
|
|
|
let target_view_offset = target_snap.view_pos - new_col_x;
|
|
|
|
|
|
|
|
|
|
self.view_offset = ViewOffset::Animation(Animation::new(
|
|
|
|
|
self.clock.clone(),
|
|
|
|
|
current_view_offset + delta,
|
|
|
|
|
target_view_offset,
|
|
|
|
|
velocity,
|
|
|
|
|
self.options.animations.horizontal_view_movement.0,
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
// HACK: deal with things like snapping to the right edge of a larger-than-view window.
|
|
|
|
|
self.animate_view_offset_to_column(None, new_col_idx, None);
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-18 18:56:09 +03:00
|
|
|
pub fn dnd_scroll_gesture_end(&mut self) {
|
|
|
|
|
let ViewOffset::Gesture(gesture) = &mut self.view_offset else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if gesture.dnd_last_event_time.is_some() && gesture.tracker.pos() == 0. {
|
|
|
|
|
// DnD didn't scroll anything, so preserve the current view position (rather than
|
|
|
|
|
// snapping the window).
|
|
|
|
|
self.view_offset = ViewOffset::Static(gesture.delta_from_tracker);
|
|
|
|
|
|
|
|
|
|
if !self.columns.is_empty() {
|
|
|
|
|
// Just in case, make sure the active window remains on screen.
|
|
|
|
|
self.animate_view_offset_to_column(None, self.active_column_idx, None);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-20 12:36:54 +03:00
|
|
|
self.view_offset_gesture_end(None);
|
2025-02-18 18:56:09 +03:00
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
|
|
|
|
|
if self.interactive_resize.is_some() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-31 14:21:33 +03:00
|
|
|
let (col_idx, col) = self
|
2024-11-30 09:18:33 +03:00
|
|
|
.columns
|
|
|
|
|
.iter_mut()
|
2025-03-31 14:21:33 +03:00
|
|
|
.enumerate()
|
|
|
|
|
.find(|(_, col)| col.contains(&window))
|
2024-11-30 09:18:33 +03:00
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
if col.is_fullscreen {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let tile = col
|
|
|
|
|
.tiles
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.find(|tile| tile.window().id() == &window)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let original_window_size = tile.window_size();
|
|
|
|
|
|
|
|
|
|
let resize = InteractiveResize {
|
|
|
|
|
window,
|
|
|
|
|
original_window_size,
|
|
|
|
|
data: InteractiveResizeData { edges },
|
|
|
|
|
};
|
|
|
|
|
self.interactive_resize = Some(resize);
|
|
|
|
|
|
|
|
|
|
self.view_offset.stop_anim_and_gesture();
|
|
|
|
|
|
2025-03-31 14:21:33 +03:00
|
|
|
// If this is the active column, clear the stored unfullscreen view offset in case one of
|
|
|
|
|
// the tiles in the column is still pending unfullscreen. Normally it is cleared and
|
|
|
|
|
// applied in update_window(), but we skip that during interactive resize because the view
|
|
|
|
|
// is frozen.
|
|
|
|
|
if col_idx == self.active_column_idx {
|
|
|
|
|
self.view_offset_before_fullscreen = None;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn interactive_resize_update(
|
|
|
|
|
&mut self,
|
|
|
|
|
window: &W::Id,
|
|
|
|
|
delta: Point<f64, Logical>,
|
|
|
|
|
) -> bool {
|
|
|
|
|
let Some(resize) = &self.interactive_resize else {
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if window != &resize.window {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let is_centering = self.is_centering_focused_column();
|
|
|
|
|
|
|
|
|
|
let col = self
|
|
|
|
|
.columns
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.find(|col| col.contains(window))
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let tile_idx = col
|
|
|
|
|
.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|tile| tile.window().id() == window)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
if resize.data.edges.intersects(ResizeEdge::LEFT_RIGHT) {
|
|
|
|
|
let mut dx = delta.x;
|
|
|
|
|
if resize.data.edges.contains(ResizeEdge::LEFT) {
|
|
|
|
|
dx = -dx;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if is_centering {
|
|
|
|
|
dx *= 2.;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let window_width = (resize.original_window_size.w + dx).round() as i32;
|
|
|
|
|
col.set_column_width(SizeChange::SetFixed(window_width), Some(tile_idx), false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if resize.data.edges.intersects(ResizeEdge::TOP_BOTTOM) {
|
|
|
|
|
// Prevent the simplest case of weird resizing (top edge when this is the topmost
|
|
|
|
|
// window).
|
|
|
|
|
if !(resize.data.edges.contains(ResizeEdge::TOP) && tile_idx == 0) {
|
|
|
|
|
let mut dy = delta.y;
|
|
|
|
|
if resize.data.edges.contains(ResizeEdge::TOP) {
|
|
|
|
|
dy = -dy;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// FIXME: some smarter height distribution would be nice here so that vertical
|
|
|
|
|
// resizes work as expected in more cases.
|
|
|
|
|
|
|
|
|
|
let window_height = (resize.original_window_size.h + dy).round() as i32;
|
|
|
|
|
col.set_window_height(SizeChange::SetFixed(window_height), Some(tile_idx), false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn interactive_resize_end(&mut self, window: Option<&W::Id>) {
|
|
|
|
|
let Some(resize) = &self.interactive_resize else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(window) = window {
|
|
|
|
|
if window != &resize.window {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Animate the active window into view right away.
|
|
|
|
|
if self.columns[self.active_column_idx].contains(window) {
|
|
|
|
|
self.animate_view_offset_to_column(None, self.active_column_idx, None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.interactive_resize = None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn refresh(&mut self, is_active: bool) {
|
|
|
|
|
for (col_idx, col) in self.columns.iter_mut().enumerate() {
|
|
|
|
|
let mut col_resize_data = None;
|
|
|
|
|
if let Some(resize) = &self.interactive_resize {
|
|
|
|
|
if col.contains(&resize.window) {
|
|
|
|
|
col_resize_data = Some(resize.data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
let is_tabbed = col.display_mode == ColumnDisplay::Tabbed;
|
2025-02-07 09:36:08 +03:00
|
|
|
let extra_size = col.extra_size();
|
2025-02-01 10:46:52 +03:00
|
|
|
|
|
|
|
|
// If transactions are disabled, also disable combined throttling, for more intuitive
|
|
|
|
|
// behavior. In tabbed display mode, only one window is visible, so individual
|
|
|
|
|
// throttling makes more sense.
|
|
|
|
|
let individual_throttling = self.options.disable_transactions || is_tabbed;
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
let intent = if self.options.disable_resize_throttling {
|
|
|
|
|
ConfigureIntent::CanSend
|
2025-02-01 10:46:52 +03:00
|
|
|
} else if individual_throttling {
|
|
|
|
|
// In this case, we don't use combined throttling, but rather compute throttling
|
|
|
|
|
// individually below.
|
2024-11-30 09:18:33 +03:00
|
|
|
ConfigureIntent::CanSend
|
|
|
|
|
} else {
|
|
|
|
|
col.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.fold(ConfigureIntent::NotNeeded, |intent, tile| {
|
|
|
|
|
match (intent, tile.window().configure_intent()) {
|
|
|
|
|
(_, ConfigureIntent::ShouldSend) => ConfigureIntent::ShouldSend,
|
|
|
|
|
(ConfigureIntent::NotNeeded, tile_intent) => tile_intent,
|
|
|
|
|
(ConfigureIntent::CanSend, ConfigureIntent::Throttled) => {
|
|
|
|
|
ConfigureIntent::Throttled
|
|
|
|
|
}
|
|
|
|
|
(intent, _) => intent,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (tile_idx, tile) in col.tiles.iter_mut().enumerate() {
|
|
|
|
|
let win = tile.window_mut();
|
|
|
|
|
|
|
|
|
|
let active_in_column = col.active_tile_idx == tile_idx;
|
|
|
|
|
win.set_active_in_column(active_in_column);
|
2024-12-27 11:20:03 +03:00
|
|
|
win.set_floating(false);
|
2024-11-30 09:18:33 +03:00
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
let active = is_active
|
|
|
|
|
&& self.active_column_idx == col_idx
|
|
|
|
|
// In tabbed mode, all tabs have activated state to reduce unnecessary
|
|
|
|
|
// animations when switching tabs.
|
|
|
|
|
&& (active_in_column || is_tabbed);
|
2024-11-30 09:18:33 +03:00
|
|
|
win.set_activated(active);
|
|
|
|
|
|
|
|
|
|
win.set_interactive_resize(col_resize_data);
|
|
|
|
|
|
|
|
|
|
let border_config = win.rules().border.resolve_against(self.options.border);
|
|
|
|
|
let bounds = compute_toplevel_bounds(
|
|
|
|
|
border_config,
|
|
|
|
|
self.working_area.size,
|
2025-02-07 09:36:08 +03:00
|
|
|
extra_size,
|
2024-11-30 09:18:33 +03:00
|
|
|
self.options.gaps,
|
|
|
|
|
);
|
|
|
|
|
win.set_bounds(bounds);
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
let intent = if individual_throttling {
|
2024-11-30 09:18:33 +03:00
|
|
|
win.configure_intent()
|
|
|
|
|
} else {
|
|
|
|
|
intent
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if matches!(
|
|
|
|
|
intent,
|
|
|
|
|
ConfigureIntent::CanSend | ConfigureIntent::ShouldSend
|
|
|
|
|
) {
|
|
|
|
|
win.send_pending_configure();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
win.refresh();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-04 22:08:47 +03:00
|
|
|
#[cfg(test)]
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn view_size(&self) -> Size<f64, Logical> {
|
|
|
|
|
self.view_size
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-09 13:39:08 +03:00
|
|
|
#[cfg(test)]
|
|
|
|
|
pub fn parent_area(&self) -> Rectangle<f64, Logical> {
|
|
|
|
|
self.parent_area
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-04 22:08:47 +03:00
|
|
|
#[cfg(test)]
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn clock(&self) -> &Clock {
|
|
|
|
|
&self.clock
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-04 22:08:47 +03:00
|
|
|
#[cfg(test)]
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn options(&self) -> &Rc<Options> {
|
|
|
|
|
&self.options
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
pub fn active_column_idx(&self) -> usize {
|
|
|
|
|
self.active_column_idx
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-15 11:15:35 +03:00
|
|
|
#[cfg(test)]
|
|
|
|
|
pub(super) fn view_offset(&self) -> &ViewOffset {
|
|
|
|
|
&self.view_offset
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
#[cfg(test)]
|
2025-06-09 13:39:08 +03:00
|
|
|
pub fn verify_invariants(&self) {
|
2024-11-30 09:18:33 +03:00
|
|
|
assert!(self.view_size.w > 0.);
|
|
|
|
|
assert!(self.view_size.h > 0.);
|
|
|
|
|
assert!(self.scale > 0.);
|
|
|
|
|
assert!(self.scale.is_finite());
|
|
|
|
|
assert_eq!(self.columns.len(), self.data.len());
|
|
|
|
|
assert_eq!(
|
|
|
|
|
self.working_area,
|
2025-06-09 13:39:08 +03:00
|
|
|
compute_working_area(self.parent_area, self.scale, self.options.struts)
|
2024-11-30 09:18:33 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if !self.columns.is_empty() {
|
|
|
|
|
assert!(self.active_column_idx < self.columns.len());
|
|
|
|
|
|
|
|
|
|
for (column, data) in zip(&self.columns, &self.data) {
|
|
|
|
|
assert!(Rc::ptr_eq(&self.options, &column.options));
|
|
|
|
|
assert_eq!(self.clock, column.clock);
|
|
|
|
|
assert_eq!(self.scale, column.scale);
|
|
|
|
|
column.verify_invariants();
|
|
|
|
|
|
|
|
|
|
let mut data2 = *data;
|
|
|
|
|
data2.update(column);
|
|
|
|
|
assert_eq!(data, &data2, "column data must be up to date");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let col = &self.columns[self.active_column_idx];
|
|
|
|
|
|
|
|
|
|
// When we have an unfullscreen view offset stored, the active column should have a
|
|
|
|
|
// fullscreen tile.
|
|
|
|
|
if self.view_offset_before_fullscreen.is_some() {
|
|
|
|
|
assert!(
|
|
|
|
|
col.is_fullscreen
|
|
|
|
|
|| col.tiles.iter().any(|tile| {
|
|
|
|
|
tile.is_fullscreen() || tile.window().is_pending_fullscreen()
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(resize) = &self.interactive_resize {
|
|
|
|
|
assert!(
|
|
|
|
|
self.columns
|
|
|
|
|
.iter()
|
|
|
|
|
.flat_map(|col| &col.tiles)
|
|
|
|
|
.any(|tile| tile.window().id() == &resize.window),
|
2024-11-29 21:11:02 +03:00
|
|
|
"interactive resize window must be present in the layout"
|
2024-11-30 09:18:33 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ViewOffset {
|
|
|
|
|
/// Returns the current view offset.
|
|
|
|
|
pub fn current(&self) -> f64 {
|
|
|
|
|
match self {
|
|
|
|
|
ViewOffset::Static(offset) => *offset,
|
|
|
|
|
ViewOffset::Animation(anim) => anim.value(),
|
2025-04-24 21:06:24 +03:00
|
|
|
ViewOffset::Gesture(gesture) => {
|
|
|
|
|
gesture.current_view_offset
|
|
|
|
|
+ gesture.animation.as_ref().map_or(0., |anim| anim.value())
|
|
|
|
|
}
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns the target view offset suitable for computing the new view offset.
|
|
|
|
|
pub fn target(&self) -> f64 {
|
|
|
|
|
match self {
|
|
|
|
|
ViewOffset::Static(offset) => *offset,
|
|
|
|
|
ViewOffset::Animation(anim) => anim.to(),
|
|
|
|
|
// This can be used for example if a gesture is interrupted.
|
|
|
|
|
ViewOffset::Gesture(gesture) => gesture.current_view_offset,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Returns a view offset value suitable for saving and later restoration.
|
|
|
|
|
///
|
|
|
|
|
/// This means that it shouldn't return an in-progress animation or gesture value.
|
|
|
|
|
fn stationary(&self) -> f64 {
|
|
|
|
|
match self {
|
|
|
|
|
ViewOffset::Static(offset) => *offset,
|
|
|
|
|
// For animations we can return the final value.
|
|
|
|
|
ViewOffset::Animation(anim) => anim.to(),
|
|
|
|
|
ViewOffset::Gesture(gesture) => gesture.stationary_view_offset,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_static(&self) -> bool {
|
|
|
|
|
matches!(self, Self::Static(_))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_gesture(&self) -> bool {
|
|
|
|
|
matches!(self, Self::Gesture(_))
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-25 10:06:46 +03:00
|
|
|
pub fn is_dnd_scroll(&self) -> bool {
|
|
|
|
|
matches!(&self, ViewOffset::Gesture(gesture) if gesture.dnd_last_event_time.is_some())
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-24 21:06:24 +03:00
|
|
|
pub fn is_animation_ongoing(&self) -> bool {
|
|
|
|
|
match self {
|
|
|
|
|
ViewOffset::Static(_) => false,
|
|
|
|
|
ViewOffset::Animation(_) => true,
|
|
|
|
|
ViewOffset::Gesture(gesture) => gesture.animation.is_some(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn offset(&mut self, delta: f64) {
|
|
|
|
|
match self {
|
|
|
|
|
ViewOffset::Static(offset) => *offset += delta,
|
|
|
|
|
ViewOffset::Animation(anim) => anim.offset(delta),
|
2025-04-24 21:06:24 +03:00
|
|
|
ViewOffset::Gesture(gesture) => {
|
|
|
|
|
gesture.stationary_view_offset += delta;
|
|
|
|
|
gesture.delta_from_tracker += delta;
|
|
|
|
|
gesture.current_view_offset += delta;
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn cancel_gesture(&mut self) {
|
|
|
|
|
if let ViewOffset::Gesture(gesture) = self {
|
|
|
|
|
*self = ViewOffset::Static(gesture.current_view_offset);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn stop_anim_and_gesture(&mut self) {
|
|
|
|
|
*self = ViewOffset::Static(self.current());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-24 21:06:24 +03:00
|
|
|
impl ViewGesture {
|
|
|
|
|
fn animate_from(&mut self, from: f64, clock: Clock, config: niri_config::Animation) {
|
|
|
|
|
let current = self.animation.as_ref().map_or(0., Animation::value);
|
|
|
|
|
self.animation = Some(Animation::new(clock, from + current, 0., 0., config));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
impl ColumnData {
|
|
|
|
|
pub fn new<W: LayoutElement>(column: &Column<W>) -> Self {
|
|
|
|
|
let mut rv = Self { width: 0. };
|
|
|
|
|
rv.update(column);
|
|
|
|
|
rv
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update<W: LayoutElement>(&mut self, column: &Column<W>) {
|
|
|
|
|
self.width = column.width();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TileData {
|
|
|
|
|
pub fn new<W: LayoutElement>(tile: &Tile<W>, height: WindowHeight) -> Self {
|
|
|
|
|
let mut rv = Self {
|
|
|
|
|
height,
|
|
|
|
|
size: Size::default(),
|
|
|
|
|
interactively_resizing_by_left_edge: false,
|
|
|
|
|
};
|
|
|
|
|
rv.update(tile);
|
|
|
|
|
rv
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update<W: LayoutElement>(&mut self, tile: &Tile<W>) {
|
|
|
|
|
self.size = tile.tile_size();
|
|
|
|
|
self.interactively_resizing_by_left_edge = tile
|
|
|
|
|
.window()
|
|
|
|
|
.interactive_resize_data()
|
2025-01-10 09:01:23 +03:00
|
|
|
.is_some_and(|data| data.edges.contains(ResizeEdge::LEFT));
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<PresetSize> for ColumnWidth {
|
|
|
|
|
fn from(value: PresetSize) -> Self {
|
|
|
|
|
match value {
|
|
|
|
|
PresetSize::Proportion(p) => Self::Proportion(p.clamp(0., 10000.)),
|
|
|
|
|
PresetSize::Fixed(f) => Self::Fixed(f64::from(f.clamp(1, 100000))),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl WindowHeight {
|
|
|
|
|
const fn auto_1() -> Self {
|
|
|
|
|
Self::Auto { weight: 1. }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<W: LayoutElement> Column<W> {
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
fn new_with_tile(
|
|
|
|
|
tile: Tile<W>,
|
|
|
|
|
view_size: Size<f64, Logical>,
|
|
|
|
|
working_area: Rectangle<f64, Logical>,
|
|
|
|
|
scale: f64,
|
|
|
|
|
width: ColumnWidth,
|
|
|
|
|
is_full_width: bool,
|
|
|
|
|
animate_resize: bool,
|
|
|
|
|
) -> Self {
|
2025-02-01 10:46:52 +03:00
|
|
|
let options = tile.options.clone();
|
|
|
|
|
|
2025-02-06 09:09:07 +03:00
|
|
|
let display_mode = tile
|
|
|
|
|
.window()
|
|
|
|
|
.rules()
|
|
|
|
|
.default_column_display
|
|
|
|
|
.unwrap_or(options.default_column_display);
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
let mut rv = Self {
|
|
|
|
|
tiles: vec![],
|
|
|
|
|
data: vec![],
|
|
|
|
|
active_tile_idx: 0,
|
|
|
|
|
width,
|
2025-01-31 19:24:26 +03:00
|
|
|
preset_width_idx: None,
|
2024-11-30 09:18:33 +03:00
|
|
|
is_full_width,
|
|
|
|
|
is_fullscreen: false,
|
2025-02-06 09:09:07 +03:00
|
|
|
display_mode,
|
2025-02-02 08:41:42 +03:00
|
|
|
tab_indicator: TabIndicator::new(options.tab_indicator),
|
2024-11-30 09:18:33 +03:00
|
|
|
move_animation: None,
|
|
|
|
|
view_size,
|
|
|
|
|
working_area,
|
|
|
|
|
scale,
|
|
|
|
|
clock: tile.clock.clone(),
|
2025-02-01 10:46:52 +03:00
|
|
|
options,
|
2024-11-30 09:18:33 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let is_pending_fullscreen = tile.window().is_pending_fullscreen();
|
|
|
|
|
|
|
|
|
|
rv.add_tile_at(0, tile, animate_resize);
|
|
|
|
|
|
|
|
|
|
if is_pending_fullscreen {
|
|
|
|
|
rv.set_fullscreen(true);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-10 14:12:54 +03:00
|
|
|
// Animate the tab indicator for new columns.
|
|
|
|
|
if display_mode == ColumnDisplay::Tabbed
|
|
|
|
|
&& !rv.options.tab_indicator.hide_when_single_tab
|
|
|
|
|
&& !is_pending_fullscreen
|
|
|
|
|
{
|
|
|
|
|
// Usually new columns are created together with window movement actions. For new
|
|
|
|
|
// windows, we handle that in start_open_animation().
|
|
|
|
|
rv.tab_indicator
|
|
|
|
|
.start_open_animation(rv.clock.clone(), rv.options.animations.window_movement.0);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
rv
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_config(
|
|
|
|
|
&mut self,
|
|
|
|
|
view_size: Size<f64, Logical>,
|
|
|
|
|
working_area: Rectangle<f64, Logical>,
|
|
|
|
|
scale: f64,
|
|
|
|
|
options: Rc<Options>,
|
|
|
|
|
) {
|
|
|
|
|
let mut update_sizes = false;
|
|
|
|
|
|
|
|
|
|
if self.view_size != view_size || self.working_area != working_area {
|
|
|
|
|
update_sizes = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-31 19:24:26 +03:00
|
|
|
// If preset widths changed, clear our stored preset index.
|
2024-11-30 09:18:33 +03:00
|
|
|
if self.options.preset_column_widths != options.preset_column_widths {
|
2025-01-31 19:24:26 +03:00
|
|
|
self.preset_width_idx = None;
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If preset heights changed, make our heights non-preset.
|
|
|
|
|
if self.options.preset_window_heights != options.preset_window_heights {
|
|
|
|
|
self.convert_heights_to_auto();
|
|
|
|
|
update_sizes = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.options.gaps != options.gaps {
|
|
|
|
|
update_sizes = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.options.border.off != options.border.off
|
|
|
|
|
|| self.options.border.width != options.border.width
|
|
|
|
|
{
|
|
|
|
|
update_sizes = true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
if self.options.tab_indicator != options.tab_indicator {
|
|
|
|
|
update_sizes = true;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
for (tile, data) in zip(&mut self.tiles, &mut self.data) {
|
2024-12-26 08:54:56 +03:00
|
|
|
tile.update_config(view_size, scale, options.clone());
|
2024-11-30 09:18:33 +03:00
|
|
|
data.update(tile);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-02 08:41:42 +03:00
|
|
|
self.tab_indicator.update_config(options.tab_indicator);
|
2024-11-30 09:18:33 +03:00
|
|
|
self.view_size = view_size;
|
|
|
|
|
self.working_area = working_area;
|
|
|
|
|
self.scale = scale;
|
|
|
|
|
self.options = options;
|
|
|
|
|
|
|
|
|
|
if update_sizes {
|
|
|
|
|
self.update_tile_sizes(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn advance_animations(&mut self) {
|
|
|
|
|
if let Some(anim) = &mut self.move_animation {
|
|
|
|
|
if anim.is_done() {
|
|
|
|
|
self.move_animation = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for tile in &mut self.tiles {
|
|
|
|
|
tile.advance_animations();
|
|
|
|
|
}
|
2025-02-10 14:12:54 +03:00
|
|
|
|
|
|
|
|
self.tab_indicator.advance_animations();
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn are_animations_ongoing(&self) -> bool {
|
2025-02-10 14:12:54 +03:00
|
|
|
self.move_animation.is_some()
|
|
|
|
|
|| self.tab_indicator.are_animations_ongoing()
|
|
|
|
|
|| self.tiles.iter().any(Tile::are_animations_ongoing)
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
2025-02-11 15:35:06 +03:00
|
|
|
pub fn are_transitions_ongoing(&self) -> bool {
|
|
|
|
|
self.move_animation.is_some()
|
|
|
|
|
|| self.tab_indicator.are_animations_ongoing()
|
|
|
|
|
|| self.tiles.iter().any(Tile::are_transitions_ongoing)
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pub fn update_render_elements(&mut self, is_active: bool, view_rect: Rectangle<f64, Logical>) {
|
|
|
|
|
let active_idx = self.active_tile_idx;
|
|
|
|
|
for (tile_idx, (tile, tile_off)) in self.tiles_mut().enumerate() {
|
|
|
|
|
let is_active = is_active && tile_idx == active_idx;
|
|
|
|
|
|
|
|
|
|
let mut tile_view_rect = view_rect;
|
|
|
|
|
tile_view_rect.loc -= tile_off + tile.render_offset();
|
2025-01-21 09:40:00 +03:00
|
|
|
tile.update_render_elements(is_active, tile_view_rect);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
2025-02-02 08:41:42 +03:00
|
|
|
|
|
|
|
|
let config = self.tab_indicator.config();
|
2025-02-09 19:14:04 +03:00
|
|
|
let offsets = self.tile_offsets_iter(self.data.iter().copied());
|
|
|
|
|
let tabs = zip(&self.tiles, offsets)
|
|
|
|
|
.enumerate()
|
|
|
|
|
.map(|(tile_idx, (tile, tile_off))| {
|
|
|
|
|
let is_active = tile_idx == active_idx;
|
2025-05-10 22:34:53 +03:00
|
|
|
let is_urgent = tile.window().is_urgent();
|
2025-02-09 19:14:04 +03:00
|
|
|
let tile_pos = tile_off + tile.render_offset();
|
2025-05-10 22:34:53 +03:00
|
|
|
TabInfo::from_tile(tile, tile_pos, is_active, is_urgent, &config)
|
2025-02-09 19:14:04 +03:00
|
|
|
});
|
2025-02-02 08:41:42 +03:00
|
|
|
|
|
|
|
|
// Hide the tab indicator in fullscreen. If you have it configured to overlap the window,
|
|
|
|
|
// you don't want that to happen in fullscreen. Also, laying things out correctly when the
|
|
|
|
|
// tab indicator is within the column and the column goes fullscreen, would require too
|
|
|
|
|
// many changes to the code for too little benefit (it's mostly invisible anyway).
|
|
|
|
|
let enabled = self.display_mode == ColumnDisplay::Tabbed && !self.is_fullscreen;
|
|
|
|
|
|
|
|
|
|
self.tab_indicator.update_render_elements(
|
|
|
|
|
enabled,
|
2025-02-10 13:10:36 +03:00
|
|
|
self.tab_indicator_area(),
|
2025-02-09 19:14:04 +03:00
|
|
|
view_rect,
|
|
|
|
|
self.tiles.len(),
|
2025-02-02 08:41:42 +03:00
|
|
|
tabs,
|
|
|
|
|
is_active,
|
|
|
|
|
self.scale,
|
|
|
|
|
);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn render_offset(&self) -> Point<f64, Logical> {
|
|
|
|
|
let mut offset = Point::from((0., 0.));
|
|
|
|
|
|
|
|
|
|
if let Some(anim) = &self.move_animation {
|
|
|
|
|
offset.x += anim.value();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
offset
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn animate_move_from(&mut self, from_x_offset: f64) {
|
|
|
|
|
self.animate_move_from_with_config(
|
|
|
|
|
from_x_offset,
|
|
|
|
|
self.options.animations.window_movement.0,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn animate_move_from_with_config(
|
|
|
|
|
&mut self,
|
|
|
|
|
from_x_offset: f64,
|
|
|
|
|
config: niri_config::Animation,
|
|
|
|
|
) {
|
|
|
|
|
let current_offset = self.move_animation.as_ref().map_or(0., Animation::value);
|
|
|
|
|
|
|
|
|
|
self.move_animation = Some(Animation::new(
|
|
|
|
|
self.clock.clone(),
|
|
|
|
|
from_x_offset + current_offset,
|
|
|
|
|
0.,
|
|
|
|
|
0.,
|
|
|
|
|
config,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn contains(&self, window: &W::Id) -> bool {
|
|
|
|
|
self.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.map(Tile::window)
|
|
|
|
|
.any(|win| win.id() == window)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn position(&self, window: &W::Id) -> Option<usize> {
|
|
|
|
|
self.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.map(Tile::window)
|
|
|
|
|
.position(|win| win.id() == window)
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-08 15:22:11 +03:00
|
|
|
fn activate_idx(&mut self, idx: usize) -> bool {
|
|
|
|
|
if self.active_tile_idx == idx {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.active_tile_idx = idx;
|
|
|
|
|
|
2025-02-08 15:24:02 +03:00
|
|
|
self.tiles[idx].ensure_alpha_animates_to_1();
|
|
|
|
|
|
2025-02-08 15:22:11 +03:00
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
fn activate_window(&mut self, window: &W::Id) {
|
|
|
|
|
let idx = self.position(window).unwrap();
|
2025-02-08 15:22:11 +03:00
|
|
|
self.activate_idx(idx);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn add_tile_at(&mut self, idx: usize, mut tile: Tile<W>, animate: bool) {
|
2024-12-26 08:54:56 +03:00
|
|
|
tile.update_config(self.view_size, self.scale, self.options.clone());
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
// Inserting a tile pushes down all tiles below it, but also in always-centering mode it
|
|
|
|
|
// will affect the X position of all tiles in the column.
|
|
|
|
|
let mut prev_offsets = Vec::with_capacity(self.tiles.len() + 1);
|
|
|
|
|
prev_offsets.extend(self.tile_offsets().take(self.tiles.len()));
|
|
|
|
|
|
2025-02-04 09:51:23 +03:00
|
|
|
if self.display_mode != ColumnDisplay::Tabbed {
|
|
|
|
|
self.is_fullscreen = false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
self.data
|
|
|
|
|
.insert(idx, TileData::new(&tile, WindowHeight::auto_1()));
|
|
|
|
|
self.tiles.insert(idx, tile);
|
|
|
|
|
self.update_tile_sizes(animate);
|
|
|
|
|
|
|
|
|
|
// Animate tiles according to the offset changes.
|
|
|
|
|
prev_offsets.insert(idx, Point::default());
|
|
|
|
|
for (i, ((tile, offset), prev)) in zip(self.tiles_mut(), prev_offsets).enumerate() {
|
|
|
|
|
if i == idx {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tile.animate_move_from(prev - offset);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_window(&mut self, window: &W::Id) {
|
|
|
|
|
let (tile_idx, tile) = self
|
|
|
|
|
.tiles
|
|
|
|
|
.iter_mut()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.find(|(_, tile)| tile.window().id() == window)
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
let height = f64::from(tile.window().size().h);
|
|
|
|
|
let offset = tile
|
|
|
|
|
.window()
|
|
|
|
|
.animation_snapshot()
|
|
|
|
|
.map_or(0., |from| from.size.h - height);
|
|
|
|
|
|
|
|
|
|
tile.update_window();
|
|
|
|
|
self.data[tile_idx].update(tile);
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
let is_tabbed = self.display_mode == ColumnDisplay::Tabbed;
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
// Move windows below in tandem with resizing.
|
|
|
|
|
//
|
|
|
|
|
// FIXME: in always-centering mode, window resizing will affect the offsets of all other
|
|
|
|
|
// windows in the column, so they should all be animated. How should this interact with
|
|
|
|
|
// animated vs. non-animated resizes? For example, an animated +20 resize followed by two
|
|
|
|
|
// non-animated -10 resizes.
|
2025-02-01 10:46:52 +03:00
|
|
|
if !is_tabbed && tile.resize_animation().is_some() && offset != 0. {
|
2024-11-30 09:18:33 +03:00
|
|
|
for tile in &mut self.tiles[tile_idx + 1..] {
|
|
|
|
|
tile.animate_move_y_from_with_config(
|
|
|
|
|
offset,
|
|
|
|
|
self.options.animations.window_resize.anim,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
/// Extra size taken up by elements in the column such as the tab indicator.
|
|
|
|
|
fn extra_size(&self) -> Size<f64, Logical> {
|
|
|
|
|
if self.display_mode == ColumnDisplay::Tabbed {
|
|
|
|
|
self.tab_indicator.extra_size(self.tiles.len(), self.scale)
|
|
|
|
|
} else {
|
|
|
|
|
Size::from((0., 0.))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-07 09:35:44 +03:00
|
|
|
fn resolve_preset_width(&self, preset: PresetSize) -> ResolvedSize {
|
2025-02-07 09:36:08 +03:00
|
|
|
let extra = self.extra_size();
|
|
|
|
|
resolve_preset_size(preset, &self.options, self.working_area.size.w, extra.w)
|
2025-02-07 09:35:44 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn resolve_preset_height(&self, preset: PresetSize) -> ResolvedSize {
|
2025-02-07 09:36:08 +03:00
|
|
|
let extra = self.extra_size();
|
|
|
|
|
resolve_preset_size(preset, &self.options, self.working_area.size.h, extra.h)
|
2025-02-07 09:35:44 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn resolve_column_width(&self, width: ColumnWidth) -> f64 {
|
|
|
|
|
let working_size = self.working_area.size;
|
|
|
|
|
let gaps = self.options.gaps;
|
2025-02-07 09:36:08 +03:00
|
|
|
let extra = self.extra_size();
|
2025-02-07 09:35:44 +03:00
|
|
|
|
|
|
|
|
match width {
|
2025-02-07 09:36:08 +03:00
|
|
|
ColumnWidth::Proportion(proportion) => {
|
|
|
|
|
(working_size.w - gaps) * proportion - gaps - extra.w
|
|
|
|
|
}
|
2025-02-07 09:35:44 +03:00
|
|
|
ColumnWidth::Fixed(width) => width,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
fn update_tile_sizes(&mut self, animate: bool) {
|
|
|
|
|
self.update_tile_sizes_with_transaction(animate, Transaction::new());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn update_tile_sizes_with_transaction(&mut self, animate: bool, transaction: Transaction) {
|
|
|
|
|
if self.is_fullscreen {
|
2025-03-17 07:45:26 +03:00
|
|
|
for (tile_idx, tile) in self.tiles.iter_mut().enumerate() {
|
|
|
|
|
// In tabbed mode, only the visible window participates in the transaction.
|
|
|
|
|
let is_active = tile_idx == self.active_tile_idx;
|
|
|
|
|
let transaction = if self.display_mode == ColumnDisplay::Tabbed && !is_active {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(transaction.clone())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
tile.request_fullscreen(animate, transaction);
|
2025-02-04 09:51:23 +03:00
|
|
|
}
|
2024-11-30 09:18:33 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
let is_tabbed = self.display_mode == ColumnDisplay::Tabbed;
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
let min_size: Vec<_> = self
|
|
|
|
|
.tiles
|
|
|
|
|
.iter()
|
2025-03-17 07:45:26 +03:00
|
|
|
.map(Tile::min_size_nonfullscreen)
|
2024-11-30 09:18:33 +03:00
|
|
|
.map(|mut size| {
|
|
|
|
|
size.w = size.w.max(1.);
|
|
|
|
|
size.h = size.h.max(1.);
|
|
|
|
|
size
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
2025-03-17 07:45:26 +03:00
|
|
|
let max_size: Vec<_> = self
|
|
|
|
|
.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.map(Tile::max_size_nonfullscreen)
|
|
|
|
|
.collect();
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
// Compute the column width.
|
|
|
|
|
let min_width = min_size
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|size| NotNan::new(size.w).unwrap())
|
|
|
|
|
.max()
|
|
|
|
|
.map(NotNan::into_inner)
|
|
|
|
|
.unwrap();
|
|
|
|
|
let max_width = max_size
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|size| {
|
|
|
|
|
let w = size.w;
|
|
|
|
|
if w == 0. {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(NotNan::new(w).unwrap())
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.min()
|
|
|
|
|
.map(NotNan::into_inner)
|
|
|
|
|
.unwrap_or(f64::from(i32::MAX));
|
|
|
|
|
let max_width = f64::max(max_width, min_width);
|
|
|
|
|
|
|
|
|
|
let width = if self.is_full_width {
|
|
|
|
|
ColumnWidth::Proportion(1.)
|
|
|
|
|
} else {
|
|
|
|
|
self.width
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
let working_size = self.working_area.size;
|
2025-02-07 09:36:08 +03:00
|
|
|
let extra_size = self.extra_size();
|
2025-02-07 09:36:08 +03:00
|
|
|
|
2025-02-07 09:35:44 +03:00
|
|
|
let width = self.resolve_column_width(width);
|
2024-11-30 09:18:33 +03:00
|
|
|
let width = f64::max(f64::min(width, max_width), min_width);
|
2025-02-07 09:36:08 +03:00
|
|
|
let max_tile_height = working_size.h - self.options.gaps * 2. - extra_size.h;
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
// If there are multiple windows in a column, clamp the non-auto window's height according
|
|
|
|
|
// to other windows' min sizes.
|
|
|
|
|
let mut max_non_auto_window_height = None;
|
2025-02-01 10:46:52 +03:00
|
|
|
if self.tiles.len() > 1 && !is_tabbed {
|
2024-11-30 09:18:33 +03:00
|
|
|
if let Some(non_auto_idx) = self
|
|
|
|
|
.data
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|data| !matches!(data.height, WindowHeight::Auto { .. }))
|
|
|
|
|
{
|
|
|
|
|
let min_height_taken = min_size
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.filter(|(idx, _)| *idx != non_auto_idx)
|
|
|
|
|
.map(|(_, min_size)| min_size.h + self.options.gaps)
|
|
|
|
|
.sum::<f64>();
|
|
|
|
|
|
|
|
|
|
let tile = &self.tiles[non_auto_idx];
|
2025-02-01 13:04:10 +03:00
|
|
|
let height_left = max_tile_height - min_height_taken;
|
2024-11-30 09:18:33 +03:00
|
|
|
max_non_auto_window_height = Some(f64::max(
|
|
|
|
|
1.,
|
|
|
|
|
tile.window_height_for_tile_height(height_left).round(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compute the tile heights. Start by converting window heights to tile heights.
|
|
|
|
|
let mut heights = zip(&self.tiles, &self.data)
|
|
|
|
|
.map(|(tile, data)| match data.height {
|
|
|
|
|
auto @ WindowHeight::Auto { .. } => auto,
|
|
|
|
|
WindowHeight::Fixed(height) => {
|
|
|
|
|
let mut window_height = height.round().max(1.);
|
|
|
|
|
if let Some(max) = max_non_auto_window_height {
|
|
|
|
|
window_height = f64::min(window_height, max);
|
2025-02-01 10:46:52 +03:00
|
|
|
} else {
|
|
|
|
|
// In any case, clamp to the working area height.
|
2025-02-01 13:04:10 +03:00
|
|
|
let max = tile.window_height_for_tile_height(max_tile_height).round();
|
2025-02-01 10:46:52 +03:00
|
|
|
window_height = f64::min(window_height, max);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WindowHeight::Fixed(tile.tile_height_for_window_height(window_height))
|
|
|
|
|
}
|
|
|
|
|
WindowHeight::Preset(idx) => {
|
|
|
|
|
let preset = self.options.preset_window_heights[idx];
|
2025-02-07 09:35:44 +03:00
|
|
|
let window_height = match self.resolve_preset_height(preset) {
|
|
|
|
|
ResolvedSize::Tile(h) => tile.window_height_for_tile_height(h),
|
|
|
|
|
ResolvedSize::Window(h) => h,
|
|
|
|
|
};
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
let mut window_height = window_height.round().clamp(1., 100000.);
|
|
|
|
|
if let Some(max) = max_non_auto_window_height {
|
|
|
|
|
window_height = f64::min(window_height, max);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let tile_height = tile.tile_height_for_window_height(window_height);
|
|
|
|
|
WindowHeight::Fixed(tile_height)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
// In tabbed display mode, fill fixed heights right away.
|
|
|
|
|
if is_tabbed {
|
|
|
|
|
// All tiles have the same height, equal to the height of the only fixed tile (if any).
|
|
|
|
|
let tabbed_height = heights
|
|
|
|
|
.iter()
|
|
|
|
|
.find_map(|h| {
|
|
|
|
|
if let WindowHeight::Fixed(h) = h {
|
|
|
|
|
Some(*h)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(max_tile_height);
|
|
|
|
|
|
|
|
|
|
// We also take min height of all tabs into account.
|
|
|
|
|
let min_height = min_size
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|size| NotNan::new(size.h).unwrap())
|
|
|
|
|
.max()
|
|
|
|
|
.map(NotNan::into_inner)
|
|
|
|
|
.unwrap();
|
|
|
|
|
// But, if there's a larger-than-workspace tab, we don't want to force all tabs to that
|
|
|
|
|
// size.
|
|
|
|
|
let min_height = f64::min(max_tile_height, min_height);
|
|
|
|
|
let tabbed_height = f64::max(tabbed_height, min_height);
|
|
|
|
|
|
2025-04-02 22:22:36 +03:00
|
|
|
heights.fill(WindowHeight::Fixed(tabbed_height));
|
2025-02-01 10:46:52 +03:00
|
|
|
|
|
|
|
|
// The following logic will apply individual min/max height, etc.
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
let gaps_left = self.options.gaps * (self.tiles.len() + 1) as f64;
|
2025-02-07 09:36:08 +03:00
|
|
|
let mut height_left = working_size.h - gaps_left;
|
2024-11-30 09:18:33 +03:00
|
|
|
let mut auto_tiles_left = self.tiles.len();
|
|
|
|
|
|
|
|
|
|
// Subtract all fixed-height tiles.
|
|
|
|
|
for (h, (min_size, max_size)) in zip(&mut heights, zip(&min_size, &max_size)) {
|
|
|
|
|
// Check if the tile has an exact height constraint.
|
|
|
|
|
if min_size.h == max_size.h {
|
|
|
|
|
*h = WindowHeight::Fixed(min_size.h);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let WindowHeight::Fixed(h) = h {
|
|
|
|
|
if max_size.h > 0. {
|
|
|
|
|
*h = f64::min(*h, max_size.h);
|
|
|
|
|
}
|
|
|
|
|
*h = f64::max(*h, min_size.h);
|
|
|
|
|
|
|
|
|
|
height_left -= *h;
|
|
|
|
|
auto_tiles_left -= 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut total_weight: f64 = heights
|
|
|
|
|
.iter()
|
|
|
|
|
.filter_map(|h| {
|
|
|
|
|
if let WindowHeight::Auto { weight } = *h {
|
|
|
|
|
Some(weight)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.sum();
|
|
|
|
|
|
|
|
|
|
// Iteratively try to distribute the remaining height, checking against tile min heights.
|
|
|
|
|
// Pick an auto height according to the current sizes, then check if it satisfies all
|
|
|
|
|
// remaining min heights. If not, allocate fixed height to those tiles and repeat the
|
|
|
|
|
// loop. On each iteration the auto height will get smaller.
|
|
|
|
|
//
|
|
|
|
|
// NOTE: we do not respect max height here. Doing so would complicate things: if the current
|
|
|
|
|
// auto height is above some tile's max height, then the auto height can become larger.
|
|
|
|
|
// Combining this with the min height loop is where the complexity appears.
|
|
|
|
|
//
|
|
|
|
|
// However, most max height uses are for fixed-size dialogs, where min height == max_height.
|
|
|
|
|
// This case is separately handled above.
|
|
|
|
|
'outer: while auto_tiles_left > 0 {
|
|
|
|
|
// Wayland requires us to round the requested size for a window to integer logical
|
|
|
|
|
// pixels, therefore we compute the remaining auto height dynamically.
|
|
|
|
|
let mut height_left_2 = height_left;
|
|
|
|
|
let mut total_weight_2 = total_weight;
|
|
|
|
|
for ((h, tile), min_size) in zip(zip(&mut heights, &self.tiles), &min_size) {
|
|
|
|
|
let weight = match *h {
|
|
|
|
|
WindowHeight::Auto { weight } => weight,
|
|
|
|
|
WindowHeight::Fixed(_) => continue,
|
|
|
|
|
WindowHeight::Preset(_) => unreachable!(),
|
|
|
|
|
};
|
|
|
|
|
let factor = weight / total_weight_2;
|
|
|
|
|
|
|
|
|
|
// Compute the current auto height.
|
|
|
|
|
let mut auto = height_left_2 * factor;
|
|
|
|
|
|
|
|
|
|
// Check if the auto height satisfies the min height.
|
|
|
|
|
if min_size.h > auto {
|
|
|
|
|
auto = min_size.h;
|
|
|
|
|
*h = WindowHeight::Fixed(auto);
|
|
|
|
|
height_left -= auto;
|
|
|
|
|
total_weight -= weight;
|
|
|
|
|
auto_tiles_left -= 1;
|
|
|
|
|
|
|
|
|
|
// If a min height was unsatisfied, then we allocate the tile more than the
|
|
|
|
|
// auto height, which means that the remaining auto tiles now have less height
|
|
|
|
|
// to work with, and the loop must run again.
|
|
|
|
|
//
|
|
|
|
|
// If we keep going in this loop and break out later, we may allocate less
|
|
|
|
|
// height to the subsequent tiles than would be available next iteration and
|
|
|
|
|
// potentially trip their min height check earlier than necessary, leading to
|
|
|
|
|
// visible snapping.
|
|
|
|
|
continue 'outer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto = tile.tile_height_for_window_height(
|
|
|
|
|
tile.window_height_for_tile_height(auto).round().max(1.),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
height_left_2 -= auto;
|
|
|
|
|
total_weight_2 -= weight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All min heights were satisfied, fill them in.
|
|
|
|
|
for (h, tile) in zip(&mut heights, &self.tiles) {
|
|
|
|
|
let weight = match *h {
|
|
|
|
|
WindowHeight::Auto { weight } => weight,
|
|
|
|
|
WindowHeight::Fixed(_) => continue,
|
|
|
|
|
WindowHeight::Preset(_) => unreachable!(),
|
|
|
|
|
};
|
|
|
|
|
let factor = weight / total_weight;
|
|
|
|
|
|
|
|
|
|
// Compute the current auto height.
|
|
|
|
|
let auto = height_left * factor;
|
|
|
|
|
let auto = tile.tile_height_for_window_height(
|
|
|
|
|
tile.window_height_for_tile_height(auto).round().max(1.),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
*h = WindowHeight::Fixed(auto);
|
|
|
|
|
height_left -= auto;
|
|
|
|
|
total_weight -= weight;
|
|
|
|
|
auto_tiles_left -= 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert_eq!(auto_tiles_left, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
for (tile_idx, (tile, h)) in zip(&mut self.tiles, heights).enumerate() {
|
2024-11-30 09:18:33 +03:00
|
|
|
let WindowHeight::Fixed(height) = h else {
|
|
|
|
|
unreachable!()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let size = Size::from((width, height));
|
2025-02-01 10:46:52 +03:00
|
|
|
|
|
|
|
|
// In tabbed mode, only the visible window participates in the transaction.
|
|
|
|
|
let is_active = tile_idx == self.active_tile_idx;
|
|
|
|
|
let transaction = if self.display_mode == ColumnDisplay::Tabbed && !is_active {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(transaction.clone())
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
tile.request_tile_size(size, animate, transaction);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn width(&self) -> f64 {
|
2025-02-07 09:36:08 +03:00
|
|
|
let mut tiles_width = self
|
|
|
|
|
.data
|
2024-11-30 09:18:33 +03:00
|
|
|
.iter()
|
|
|
|
|
.map(|data| NotNan::new(data.size.w).unwrap())
|
|
|
|
|
.max()
|
|
|
|
|
.map(NotNan::into_inner)
|
2025-02-07 09:36:08 +03:00
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
|
|
if self.display_mode == ColumnDisplay::Tabbed && !self.is_fullscreen {
|
|
|
|
|
let extra_size = self.tab_indicator.extra_size(self.tiles.len(), self.scale);
|
|
|
|
|
tiles_width += extra_size.w;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tiles_width
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
2025-02-05 17:18:21 +03:00
|
|
|
fn focus_index(&mut self, index: u8) {
|
2025-02-08 15:22:11 +03:00
|
|
|
let idx = min(usize::from(index.saturating_sub(1)), self.tiles.len() - 1);
|
|
|
|
|
self.activate_idx(idx);
|
2025-02-05 17:18:21 +03:00
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
fn focus_up(&mut self) -> bool {
|
2025-02-08 15:22:11 +03:00
|
|
|
self.activate_idx(self.active_tile_idx.saturating_sub(1))
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn focus_down(&mut self) -> bool {
|
2025-02-08 15:22:11 +03:00
|
|
|
self.activate_idx(min(self.active_tile_idx + 1, self.tiles.len() - 1))
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
2025-02-05 17:18:21 +03:00
|
|
|
fn focus_top(&mut self) {
|
2025-02-08 15:22:11 +03:00
|
|
|
self.activate_idx(0);
|
2025-02-05 17:18:21 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn focus_bottom(&mut self) {
|
2025-02-08 15:22:11 +03:00
|
|
|
self.activate_idx(self.tiles.len() - 1);
|
2025-02-05 17:18:21 +03:00
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
fn move_up(&mut self) -> bool {
|
|
|
|
|
let new_idx = self.active_tile_idx.saturating_sub(1);
|
|
|
|
|
if self.active_tile_idx == new_idx {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut ys = self.tile_offsets().skip(self.active_tile_idx);
|
|
|
|
|
let active_y = ys.next().unwrap().y;
|
|
|
|
|
let next_y = ys.next().unwrap().y;
|
|
|
|
|
drop(ys);
|
|
|
|
|
|
|
|
|
|
self.tiles.swap(self.active_tile_idx, new_idx);
|
|
|
|
|
self.data.swap(self.active_tile_idx, new_idx);
|
|
|
|
|
self.active_tile_idx = new_idx;
|
|
|
|
|
|
|
|
|
|
// Animate the movement.
|
|
|
|
|
let new_active_y = self.tile_offset(new_idx).y;
|
|
|
|
|
self.tiles[new_idx].animate_move_y_from(active_y - new_active_y);
|
|
|
|
|
self.tiles[new_idx + 1].animate_move_y_from(active_y - next_y);
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn move_down(&mut self) -> bool {
|
|
|
|
|
let new_idx = min(self.active_tile_idx + 1, self.tiles.len() - 1);
|
|
|
|
|
if self.active_tile_idx == new_idx {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut ys = self.tile_offsets().skip(self.active_tile_idx);
|
|
|
|
|
let active_y = ys.next().unwrap().y;
|
|
|
|
|
let next_y = ys.next().unwrap().y;
|
|
|
|
|
drop(ys);
|
|
|
|
|
|
|
|
|
|
self.tiles.swap(self.active_tile_idx, new_idx);
|
|
|
|
|
self.data.swap(self.active_tile_idx, new_idx);
|
|
|
|
|
self.active_tile_idx = new_idx;
|
|
|
|
|
|
|
|
|
|
// Animate the movement.
|
|
|
|
|
let new_active_y = self.tile_offset(new_idx).y;
|
|
|
|
|
self.tiles[new_idx].animate_move_y_from(active_y - new_active_y);
|
|
|
|
|
self.tiles[new_idx - 1].animate_move_y_from(next_y - active_y);
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-31 19:24:26 +03:00
|
|
|
fn toggle_width(&mut self, tile_idx: Option<usize>) {
|
|
|
|
|
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
|
|
|
|
|
|
|
|
|
|
let preset_idx = if self.is_full_width {
|
|
|
|
|
None
|
2024-11-30 09:18:33 +03:00
|
|
|
} else {
|
2025-01-31 19:24:26 +03:00
|
|
|
self.preset_width_idx
|
2024-11-30 09:18:33 +03:00
|
|
|
};
|
|
|
|
|
|
2025-01-31 19:24:26 +03:00
|
|
|
let preset_idx = if let Some(idx) = preset_idx {
|
|
|
|
|
(idx + 1) % self.options.preset_column_widths.len()
|
|
|
|
|
} else {
|
|
|
|
|
let tile = &self.tiles[tile_idx];
|
|
|
|
|
let current_window = tile.window_expected_or_current_size().w;
|
|
|
|
|
let current_tile = tile.tile_expected_or_current_size().w;
|
|
|
|
|
|
|
|
|
|
self.options
|
|
|
|
|
.preset_column_widths
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|prop| {
|
2025-02-07 09:35:44 +03:00
|
|
|
match self.resolve_preset_width(*prop) {
|
2024-11-30 09:18:33 +03:00
|
|
|
// Some allowance for fractional scaling purposes.
|
2025-01-31 19:24:26 +03:00
|
|
|
ResolvedSize::Tile(resolved) => current_tile + 1. < resolved,
|
|
|
|
|
ResolvedSize::Window(resolved) => current_window + 1. < resolved,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(0)
|
2024-11-30 09:18:33 +03:00
|
|
|
};
|
2025-01-31 19:24:26 +03:00
|
|
|
|
|
|
|
|
let preset = self.options.preset_column_widths[preset_idx];
|
|
|
|
|
self.set_column_width(SizeChange::from(preset), Some(tile_idx), true);
|
|
|
|
|
|
|
|
|
|
self.preset_width_idx = Some(preset_idx);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn toggle_full_width(&mut self) {
|
|
|
|
|
self.is_full_width = !self.is_full_width;
|
|
|
|
|
self.update_tile_sizes(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_column_width(&mut self, change: SizeChange, tile_idx: Option<usize>, animate: bool) {
|
2025-01-31 19:24:26 +03:00
|
|
|
let current = if self.is_full_width {
|
2024-11-30 09:18:33 +03:00
|
|
|
ColumnWidth::Proportion(1.)
|
|
|
|
|
} else {
|
|
|
|
|
self.width
|
|
|
|
|
};
|
|
|
|
|
|
2025-02-07 09:35:44 +03:00
|
|
|
let current_px = self.resolve_column_width(current);
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
// FIXME: fix overflows then remove limits.
|
|
|
|
|
const MAX_PX: f64 = 100000.;
|
|
|
|
|
const MAX_F: f64 = 10000.;
|
|
|
|
|
|
|
|
|
|
let width = match (current, change) {
|
|
|
|
|
(_, SizeChange::SetFixed(fixed)) => {
|
|
|
|
|
// As a special case, setting a fixed column width will compute it in such a way
|
|
|
|
|
// that the specified (usually active) window gets that width. This is the
|
|
|
|
|
// intention behind the ability to set a fixed size.
|
|
|
|
|
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
|
|
|
|
|
let tile = &self.tiles[tile_idx];
|
|
|
|
|
ColumnWidth::Fixed(
|
|
|
|
|
tile.tile_width_for_window_width(f64::from(fixed))
|
|
|
|
|
.clamp(1., MAX_PX),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
(_, SizeChange::SetProportion(proportion)) => {
|
|
|
|
|
ColumnWidth::Proportion((proportion / 100.).clamp(0., MAX_F))
|
|
|
|
|
}
|
|
|
|
|
(_, SizeChange::AdjustFixed(delta)) => {
|
|
|
|
|
let width = (current_px + f64::from(delta)).clamp(1., MAX_PX);
|
|
|
|
|
ColumnWidth::Fixed(width)
|
|
|
|
|
}
|
|
|
|
|
(ColumnWidth::Proportion(current), SizeChange::AdjustProportion(delta)) => {
|
|
|
|
|
let proportion = (current + delta / 100.).clamp(0., MAX_F);
|
|
|
|
|
ColumnWidth::Proportion(proportion)
|
|
|
|
|
}
|
|
|
|
|
(ColumnWidth::Fixed(_), SizeChange::AdjustProportion(delta)) => {
|
|
|
|
|
let full = self.working_area.size.w - self.options.gaps;
|
|
|
|
|
let current = if full == 0. {
|
|
|
|
|
1.
|
|
|
|
|
} else {
|
2025-02-07 09:36:08 +03:00
|
|
|
(current_px + self.options.gaps + self.extra_size().w) / full
|
2024-11-30 09:18:33 +03:00
|
|
|
};
|
|
|
|
|
let proportion = (current + delta / 100.).clamp(0., MAX_F);
|
|
|
|
|
ColumnWidth::Proportion(proportion)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-31 19:24:26 +03:00
|
|
|
self.width = width;
|
|
|
|
|
self.preset_width_idx = None;
|
|
|
|
|
self.is_full_width = false;
|
|
|
|
|
self.update_tile_sizes(animate);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_window_height(&mut self, change: SizeChange, tile_idx: Option<usize>, animate: bool) {
|
|
|
|
|
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
|
|
|
|
|
|
|
|
|
|
// Start by converting all heights to automatic, since only one window in the column can be
|
|
|
|
|
// non-auto-height. If the current tile is already non-auto, however, we can skip that
|
|
|
|
|
// step. Which is not only for optimization, but also preserves automatic weights in case
|
|
|
|
|
// one window is resized in such a way that other windows hit their min size, and then
|
|
|
|
|
// back.
|
|
|
|
|
if matches!(self.data[tile_idx].height, WindowHeight::Auto { .. }) {
|
|
|
|
|
self.convert_heights_to_auto();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let current = self.data[tile_idx].height;
|
|
|
|
|
let tile = &self.tiles[tile_idx];
|
|
|
|
|
let current_window_px = match current {
|
|
|
|
|
WindowHeight::Auto { .. } | WindowHeight::Preset(_) => tile.window_size().h,
|
|
|
|
|
WindowHeight::Fixed(height) => height,
|
|
|
|
|
};
|
|
|
|
|
let current_tile_px = tile.tile_height_for_window_height(current_window_px);
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
let working_size = self.working_area.size.h;
|
|
|
|
|
let gaps = self.options.gaps;
|
2025-02-07 09:36:08 +03:00
|
|
|
let extra_size = self.extra_size().h;
|
2025-02-07 09:36:08 +03:00
|
|
|
let full = working_size - gaps;
|
2024-11-30 09:18:33 +03:00
|
|
|
let current_prop = if full == 0. {
|
|
|
|
|
1.
|
|
|
|
|
} else {
|
2025-02-07 09:36:08 +03:00
|
|
|
(current_tile_px + gaps) / full
|
2024-11-30 09:18:33 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// FIXME: fix overflows then remove limits.
|
|
|
|
|
const MAX_PX: f64 = 100000.;
|
|
|
|
|
|
|
|
|
|
let mut window_height = match change {
|
|
|
|
|
SizeChange::SetFixed(fixed) => f64::from(fixed),
|
|
|
|
|
SizeChange::SetProportion(proportion) => {
|
2025-02-07 09:36:08 +03:00
|
|
|
let tile_height = (working_size - gaps) * (proportion / 100.) - gaps - extra_size;
|
2024-11-30 09:18:33 +03:00
|
|
|
tile.window_height_for_tile_height(tile_height)
|
|
|
|
|
}
|
|
|
|
|
SizeChange::AdjustFixed(delta) => current_window_px + f64::from(delta),
|
|
|
|
|
SizeChange::AdjustProportion(delta) => {
|
|
|
|
|
let proportion = current_prop + delta / 100.;
|
2025-02-07 09:36:08 +03:00
|
|
|
let tile_height = (working_size - gaps) * proportion - gaps - extra_size;
|
2024-11-30 09:18:33 +03:00
|
|
|
tile.window_height_for_tile_height(tile_height)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-12-29 22:22:55 +03:00
|
|
|
// Clamp the height according to other windows' min sizes, or simply to working area height.
|
2025-02-01 10:46:52 +03:00
|
|
|
let min_height_taken = if self.display_mode == ColumnDisplay::Tabbed {
|
|
|
|
|
0.
|
|
|
|
|
} else {
|
|
|
|
|
self.tiles
|
|
|
|
|
.iter()
|
|
|
|
|
.enumerate()
|
|
|
|
|
.filter(|(idx, _)| *idx != tile_idx)
|
2025-03-17 07:45:26 +03:00
|
|
|
.map(|(_, tile)| f64::max(1., tile.min_size_nonfullscreen().h) + gaps)
|
2025-02-01 10:46:52 +03:00
|
|
|
.sum::<f64>()
|
|
|
|
|
};
|
2025-02-07 09:36:08 +03:00
|
|
|
let height_left = working_size - extra_size - gaps - min_height_taken - gaps;
|
2024-12-29 22:22:55 +03:00
|
|
|
let height_left = f64::max(1., tile.window_height_for_tile_height(height_left));
|
|
|
|
|
window_height = f64::min(height_left, window_height);
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
// Clamp it against the window height constraints.
|
|
|
|
|
let win = &self.tiles[tile_idx].window();
|
|
|
|
|
let min_h = win.min_size().h;
|
|
|
|
|
let max_h = win.max_size().h;
|
|
|
|
|
|
|
|
|
|
if max_h > 0 {
|
|
|
|
|
window_height = f64::min(window_height, f64::from(max_h));
|
|
|
|
|
}
|
|
|
|
|
if min_h > 0 {
|
|
|
|
|
window_height = f64::max(window_height, f64::from(min_h));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.data[tile_idx].height = WindowHeight::Fixed(window_height.clamp(1., MAX_PX));
|
|
|
|
|
self.update_tile_sizes(animate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn reset_window_height(&mut self, tile_idx: Option<usize>, animate: bool) {
|
2025-02-01 10:46:52 +03:00
|
|
|
if self.display_mode == ColumnDisplay::Tabbed {
|
|
|
|
|
// When tabbed, reset window height should work on any window, not just the fixed-size
|
|
|
|
|
// one.
|
|
|
|
|
for data in &mut self.data {
|
|
|
|
|
data.height = WindowHeight::auto_1();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
|
|
|
|
|
self.data[tile_idx].height = WindowHeight::auto_1();
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
self.update_tile_sizes(animate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn toggle_window_height(&mut self, tile_idx: Option<usize>, animate: bool) {
|
|
|
|
|
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
|
|
|
|
|
|
|
|
|
|
// Start by converting all heights to automatic, since only one window in the column can be
|
|
|
|
|
// non-auto-height. If the current tile is already non-auto, however, we can skip that
|
|
|
|
|
// step. Which is not only for optimization, but also preserves automatic weights in case
|
|
|
|
|
// one window is resized in such a way that other windows hit their min size, and then
|
|
|
|
|
// back.
|
|
|
|
|
if matches!(self.data[tile_idx].height, WindowHeight::Auto { .. }) {
|
|
|
|
|
self.convert_heights_to_auto();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let preset_idx = match self.data[tile_idx].height {
|
|
|
|
|
WindowHeight::Preset(idx) => (idx + 1) % self.options.preset_window_heights.len(),
|
|
|
|
|
_ => {
|
|
|
|
|
let current = self.data[tile_idx].size.h;
|
|
|
|
|
let tile = &self.tiles[tile_idx];
|
|
|
|
|
self.options
|
|
|
|
|
.preset_window_heights
|
|
|
|
|
.iter()
|
|
|
|
|
.copied()
|
|
|
|
|
.position(|preset| {
|
2025-02-07 09:35:44 +03:00
|
|
|
let window_height = match self.resolve_preset_height(preset) {
|
2024-11-30 09:18:33 +03:00
|
|
|
ResolvedSize::Tile(h) => tile.window_height_for_tile_height(h),
|
|
|
|
|
ResolvedSize::Window(h) => h,
|
|
|
|
|
};
|
|
|
|
|
let resolved = tile.tile_height_for_window_height(
|
|
|
|
|
window_height.round().clamp(1., 100000.),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Some allowance for fractional scaling purposes.
|
|
|
|
|
current + 1. < resolved
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or(0)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
self.data[tile_idx].height = WindowHeight::Preset(preset_idx);
|
|
|
|
|
self.update_tile_sizes(animate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Converts all heights in the column to automatic, preserving the apparent heights.
|
|
|
|
|
///
|
|
|
|
|
/// All weights are recomputed to preserve the current tile heights while "centering" the
|
|
|
|
|
/// weights at the median window height (it gets weight = 1).
|
|
|
|
|
///
|
|
|
|
|
/// One case where apparent heights will not be preserved is when the column is taller than the
|
|
|
|
|
/// working area.
|
|
|
|
|
fn convert_heights_to_auto(&mut self) {
|
|
|
|
|
let heights: Vec<_> = self.tiles.iter().map(|tile| tile.tile_size().h).collect();
|
|
|
|
|
|
|
|
|
|
// Weights are invariant to multiplication: a column with weights 2, 2, 1 is equivalent to
|
|
|
|
|
// a column with weights 4, 4, 2. So we find the median window height and use that as 1.
|
|
|
|
|
let mut sorted = heights.clone();
|
|
|
|
|
sorted.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
|
|
|
|
|
let median = sorted[sorted.len() / 2];
|
|
|
|
|
|
|
|
|
|
for (data, height) in zip(&mut self.data, heights) {
|
|
|
|
|
let weight = height / median;
|
|
|
|
|
data.height = WindowHeight::Auto { weight };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_fullscreen(&mut self, is_fullscreen: bool) {
|
|
|
|
|
if self.is_fullscreen == is_fullscreen {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-04 09:51:23 +03:00
|
|
|
if is_fullscreen {
|
|
|
|
|
assert!(self.tiles.len() == 1 || self.display_mode == ColumnDisplay::Tabbed);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
self.is_fullscreen = is_fullscreen;
|
|
|
|
|
self.update_tile_sizes(false);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-06 08:42:09 +03:00
|
|
|
fn set_column_display(&mut self, display: ColumnDisplay) {
|
|
|
|
|
if self.display_mode == display {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-08 15:24:02 +03:00
|
|
|
// Animate the movement.
|
|
|
|
|
//
|
|
|
|
|
// We're doing some shortcuts here because we know that currently normal vs. tabbed can
|
|
|
|
|
// only cause a vertical shift + a shift to the origin.
|
|
|
|
|
//
|
|
|
|
|
// Doing it this way to avoid storing all tile positions in a vector. If more display modes
|
|
|
|
|
// are added it might be simpler to just collect everything into a smallvec.
|
|
|
|
|
let prev_origin = self.tiles_origin();
|
|
|
|
|
self.display_mode = display;
|
|
|
|
|
let new_origin = self.tiles_origin();
|
|
|
|
|
let origin_delta = prev_origin - new_origin;
|
|
|
|
|
|
|
|
|
|
// When need to walk the tiles in the normal display mode to get the right offsets.
|
|
|
|
|
self.display_mode = ColumnDisplay::Normal;
|
|
|
|
|
for (tile, pos) in self.tiles_mut() {
|
|
|
|
|
let mut y_delta = pos.y - prev_origin.y;
|
|
|
|
|
|
|
|
|
|
// Invert the Y motion when transitioning *to* normal display mode.
|
|
|
|
|
if display == ColumnDisplay::Normal {
|
|
|
|
|
y_delta *= -1.;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut delta = origin_delta;
|
|
|
|
|
delta.y += y_delta;
|
|
|
|
|
tile.animate_move_from(delta);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Animate the opacity.
|
|
|
|
|
for (idx, tile) in self.tiles.iter_mut().enumerate() {
|
|
|
|
|
let is_active = idx == self.active_tile_idx;
|
|
|
|
|
if !is_active {
|
|
|
|
|
let (from, to) = if display == ColumnDisplay::Tabbed {
|
|
|
|
|
(1., 0.)
|
|
|
|
|
} else {
|
|
|
|
|
(0., 1.)
|
|
|
|
|
};
|
|
|
|
|
tile.animate_alpha(from, to, self.options.animations.window_movement.0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-10 14:12:54 +03:00
|
|
|
// Animate the appearance of the tab indicator.
|
|
|
|
|
if display == ColumnDisplay::Tabbed {
|
|
|
|
|
self.tab_indicator.start_open_animation(
|
|
|
|
|
self.clock.clone(),
|
|
|
|
|
self.options.animations.window_movement.0,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-08 15:24:02 +03:00
|
|
|
// Now switch the display mode for real.
|
2025-02-06 08:42:09 +03:00
|
|
|
self.display_mode = display;
|
2025-02-01 10:46:52 +03:00
|
|
|
self.update_tile_sizes(true);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-04 09:29:30 +03:00
|
|
|
fn tiles_origin(&self) -> Point<f64, Logical> {
|
2025-02-07 09:36:08 +03:00
|
|
|
let mut origin = Point::from((0., 0.));
|
|
|
|
|
|
2025-02-04 09:29:30 +03:00
|
|
|
if self.is_fullscreen {
|
2025-02-07 09:36:08 +03:00
|
|
|
return origin;
|
2025-02-04 09:29:30 +03:00
|
|
|
}
|
2025-02-07 09:36:08 +03:00
|
|
|
|
|
|
|
|
origin.y += self.working_area.loc.y + self.options.gaps;
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
if self.display_mode == ColumnDisplay::Tabbed {
|
|
|
|
|
origin += self
|
|
|
|
|
.tab_indicator
|
|
|
|
|
.content_offset(self.tiles.len(), self.scale);
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
origin
|
2025-02-04 09:29:30 +03:00
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
// HACK: pass a self.data iterator in manually as a workaround for the lack of method partial
|
|
|
|
|
// borrowing. Note that this method's return value does not borrow the entire &Self!
|
|
|
|
|
fn tile_offsets_iter(
|
|
|
|
|
&self,
|
|
|
|
|
data: impl Iterator<Item = TileData>,
|
|
|
|
|
) -> impl Iterator<Item = Point<f64, Logical>> {
|
|
|
|
|
// FIXME: this should take into account always-center-single-column, which means that
|
|
|
|
|
// Column should somehow know when it is being centered due to being the single column on
|
|
|
|
|
// the workspace or some other reason.
|
|
|
|
|
let center = self.options.center_focused_column == CenterFocusedColumn::Always;
|
|
|
|
|
let gaps = self.options.gaps;
|
2025-02-01 10:46:52 +03:00
|
|
|
let tabbed = self.display_mode == ColumnDisplay::Tabbed;
|
2025-02-07 09:36:08 +03:00
|
|
|
|
|
|
|
|
// Does not include extra size from the tab indicator.
|
|
|
|
|
let tiles_width = self
|
|
|
|
|
.data
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|data| NotNan::new(data.size.w).unwrap())
|
|
|
|
|
.max()
|
|
|
|
|
.map(NotNan::into_inner)
|
|
|
|
|
.unwrap_or(0.);
|
|
|
|
|
|
2025-02-04 09:29:30 +03:00
|
|
|
let mut origin = self.tiles_origin();
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
// Chain with a dummy value to be able to get one past all tiles' Y.
|
|
|
|
|
let dummy = TileData {
|
|
|
|
|
height: WindowHeight::auto_1(),
|
|
|
|
|
size: Size::default(),
|
|
|
|
|
interactively_resizing_by_left_edge: false,
|
|
|
|
|
};
|
|
|
|
|
let data = data.chain(iter::once(dummy));
|
|
|
|
|
|
|
|
|
|
data.map(move |data| {
|
2025-02-04 09:29:30 +03:00
|
|
|
let mut pos = origin;
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
if center {
|
2025-02-07 09:36:08 +03:00
|
|
|
pos.x += (tiles_width - data.size.w) / 2.;
|
2024-11-30 09:18:33 +03:00
|
|
|
} else if data.interactively_resizing_by_left_edge {
|
2025-02-07 09:36:08 +03:00
|
|
|
pos.x += tiles_width - data.size.w;
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
if !tabbed {
|
|
|
|
|
origin.y += data.size.h + gaps;
|
|
|
|
|
}
|
2025-02-04 09:29:30 +03:00
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
pos
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn tile_offsets(&self) -> impl Iterator<Item = Point<f64, Logical>> + '_ {
|
|
|
|
|
self.tile_offsets_iter(self.data.iter().copied())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn tile_offset(&self, tile_idx: usize) -> Point<f64, Logical> {
|
|
|
|
|
self.tile_offsets().nth(tile_idx).unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn tile_offsets_in_render_order(
|
|
|
|
|
&self,
|
|
|
|
|
data: impl Iterator<Item = TileData>,
|
|
|
|
|
) -> impl Iterator<Item = Point<f64, Logical>> {
|
|
|
|
|
let active_idx = self.active_tile_idx;
|
|
|
|
|
let active_pos = self.tile_offset(active_idx);
|
|
|
|
|
let offsets = self
|
|
|
|
|
.tile_offsets_iter(data)
|
|
|
|
|
.enumerate()
|
|
|
|
|
.filter_map(move |(idx, pos)| (idx != active_idx).then_some(pos));
|
|
|
|
|
iter::once(active_pos).chain(offsets)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn tiles(&self) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> + '_ {
|
|
|
|
|
let offsets = self.tile_offsets_iter(self.data.iter().copied());
|
|
|
|
|
zip(&self.tiles, offsets)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn tiles_mut(&mut self) -> impl Iterator<Item = (&mut Tile<W>, Point<f64, Logical>)> + '_ {
|
|
|
|
|
let offsets = self.tile_offsets_iter(self.data.iter().copied());
|
|
|
|
|
zip(&mut self.tiles, offsets)
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
fn tiles_in_render_order(
|
|
|
|
|
&self,
|
|
|
|
|
) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>, bool)> + '_ {
|
2024-11-30 09:18:33 +03:00
|
|
|
let offsets = self.tile_offsets_in_render_order(self.data.iter().copied());
|
|
|
|
|
|
|
|
|
|
let (first, rest) = self.tiles.split_at(self.active_tile_idx);
|
|
|
|
|
let (active, rest) = rest.split_at(1);
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
let active = active.iter().map(|tile| (tile, true));
|
|
|
|
|
|
|
|
|
|
let rest_visible = self.display_mode != ColumnDisplay::Tabbed;
|
|
|
|
|
let rest = first.iter().chain(rest);
|
|
|
|
|
let rest = rest.map(move |tile| (tile, rest_visible));
|
|
|
|
|
|
|
|
|
|
let tiles = active.chain(rest);
|
|
|
|
|
zip(tiles, offsets).map(|((tile, visible), pos)| (tile, pos, visible))
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn tiles_in_render_order_mut(
|
|
|
|
|
&mut self,
|
|
|
|
|
) -> impl Iterator<Item = (&mut Tile<W>, Point<f64, Logical>)> + '_ {
|
|
|
|
|
let offsets = self.tile_offsets_in_render_order(self.data.iter().copied());
|
|
|
|
|
|
|
|
|
|
let (first, rest) = self.tiles.split_at_mut(self.active_tile_idx);
|
|
|
|
|
let (active, rest) = rest.split_at_mut(1);
|
|
|
|
|
|
|
|
|
|
let tiles = active.iter_mut().chain(first).chain(rest);
|
|
|
|
|
zip(tiles, offsets)
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-10 13:10:36 +03:00
|
|
|
fn tab_indicator_area(&self) -> Rectangle<f64, Logical> {
|
|
|
|
|
// We'd like to use the active tile's animated size for the tab indicator, however we need
|
|
|
|
|
// to be mindful of the case where the active tile is smaller than some other tile in the
|
|
|
|
|
// column. The column assumes the size of the largest tile.
|
|
|
|
|
//
|
|
|
|
|
// We expect users to mainly resize tabbed columns by width, so matching the animated size
|
|
|
|
|
// is more important here. Besides, we always try to resize all windows in a column to the
|
|
|
|
|
// same width when possible, and also the animation for going into tabbed mode doesn't move
|
|
|
|
|
// tiles horizontally as much.
|
|
|
|
|
//
|
|
|
|
|
// For height though, it's a different story. First, users probably aren't resizing a
|
|
|
|
|
// tabbed column by height. Second, we don't match windows by height, so it's easy to have
|
|
|
|
|
// a smaller active tile than the rest of the column, e.g. by adding a fixed-size dialog.
|
|
|
|
|
// Then, switching to that dialog and back should ideally keep the tab indicator position
|
|
|
|
|
// fixed. Third, the animation for making a column tabbed moves tiles vertically, and using
|
|
|
|
|
// the active tile's animated size in this case only works for the topmost tile, and looks
|
|
|
|
|
// broken otherwise.
|
|
|
|
|
let mut max_height = 0.;
|
|
|
|
|
for tile in &self.tiles {
|
|
|
|
|
max_height = f64::max(max_height, tile.tile_size().h);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let tile = &self.tiles[self.active_tile_idx];
|
|
|
|
|
let area_size = Size::from((tile.animated_tile_size().w, max_height));
|
|
|
|
|
|
|
|
|
|
Rectangle::new(self.tiles_origin(), area_size)
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-10 14:12:54 +03:00
|
|
|
pub fn start_open_animation(&mut self, id: &W::Id) -> bool {
|
|
|
|
|
for tile in &mut self.tiles {
|
|
|
|
|
if tile.window().id() == id {
|
|
|
|
|
tile.start_open_animation();
|
|
|
|
|
|
|
|
|
|
// Animate the appearance of the tab indicator.
|
|
|
|
|
if self.display_mode == ColumnDisplay::Tabbed
|
|
|
|
|
&& !self.is_fullscreen
|
|
|
|
|
&& self.tiles.len() == 1
|
|
|
|
|
&& !self.tab_indicator.config().hide_when_single_tab
|
|
|
|
|
{
|
|
|
|
|
self.tab_indicator.start_open_animation(
|
|
|
|
|
self.clock.clone(),
|
|
|
|
|
self.options.animations.window_open.anim,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
#[cfg(test)]
|
|
|
|
|
fn verify_invariants(&self) {
|
|
|
|
|
assert!(!self.tiles.is_empty(), "columns can't be empty");
|
|
|
|
|
assert!(self.active_tile_idx < self.tiles.len());
|
|
|
|
|
assert_eq!(self.tiles.len(), self.data.len());
|
|
|
|
|
|
|
|
|
|
if self.is_fullscreen {
|
2025-02-04 09:51:23 +03:00
|
|
|
assert!(self.tiles.len() == 1 || self.display_mode == ColumnDisplay::Tabbed);
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
2025-01-31 19:24:26 +03:00
|
|
|
if let Some(idx) = self.preset_width_idx {
|
|
|
|
|
assert!(idx < self.options.preset_column_widths.len());
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
let is_tabbed = self.display_mode == ColumnDisplay::Tabbed;
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
let tile_count = self.tiles.len();
|
|
|
|
|
if tile_count == 1 {
|
|
|
|
|
if let WindowHeight::Auto { weight } = self.data[0].height {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
weight, 1.,
|
|
|
|
|
"auto height weight must reset to 1 for a single window"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
let working_size = self.working_area.size;
|
2025-02-07 09:36:08 +03:00
|
|
|
let extra_size = self.extra_size();
|
2025-02-07 09:36:08 +03:00
|
|
|
let gaps = self.options.gaps;
|
|
|
|
|
|
2024-11-30 09:18:33 +03:00
|
|
|
let mut found_fixed = false;
|
|
|
|
|
let mut total_height = 0.;
|
|
|
|
|
let mut total_min_height = 0.;
|
|
|
|
|
for (tile, data) in zip(&self.tiles, &self.data) {
|
|
|
|
|
assert!(Rc::ptr_eq(&self.options, &tile.options));
|
|
|
|
|
assert_eq!(self.clock, tile.clock);
|
|
|
|
|
assert_eq!(self.scale, tile.scale());
|
|
|
|
|
assert_eq!(self.is_fullscreen, tile.window().is_pending_fullscreen());
|
2024-12-26 08:54:56 +03:00
|
|
|
assert_eq!(self.view_size, tile.view_size());
|
2024-12-04 22:08:47 +03:00
|
|
|
tile.verify_invariants();
|
2024-11-30 09:18:33 +03:00
|
|
|
|
|
|
|
|
let mut data2 = *data;
|
|
|
|
|
data2.update(tile);
|
|
|
|
|
assert_eq!(data, &data2, "tile data must be up to date");
|
|
|
|
|
|
|
|
|
|
if matches!(data.height, WindowHeight::Fixed(_)) {
|
|
|
|
|
assert!(
|
|
|
|
|
!found_fixed,
|
|
|
|
|
"there can only be one fixed-height window in a column"
|
|
|
|
|
);
|
|
|
|
|
found_fixed = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let WindowHeight::Preset(idx) = data.height {
|
|
|
|
|
assert!(self.options.preset_window_heights.len() > idx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let requested_size = tile.window().requested_size().unwrap();
|
2025-02-01 10:46:52 +03:00
|
|
|
let requested_tile_height =
|
|
|
|
|
tile.tile_height_for_window_height(f64::from(requested_size.h));
|
2025-03-17 07:45:26 +03:00
|
|
|
let min_tile_height = f64::max(1., tile.min_size_nonfullscreen().h);
|
2025-02-01 10:46:52 +03:00
|
|
|
|
|
|
|
|
if !self.is_fullscreen
|
|
|
|
|
&& self.scale.round() == self.scale
|
2025-02-07 09:36:08 +03:00
|
|
|
&& working_size.h.round() == working_size.h
|
|
|
|
|
&& gaps.round() == gaps
|
2025-02-01 10:46:52 +03:00
|
|
|
{
|
2025-02-07 09:36:08 +03:00
|
|
|
let total_height = requested_tile_height + gaps * 2. + extra_size.h;
|
|
|
|
|
let total_min_height = min_tile_height + gaps * 2. + extra_size.h;
|
2025-02-07 09:36:08 +03:00
|
|
|
let max_height = f64::max(total_min_height, working_size.h);
|
2025-02-01 10:46:52 +03:00
|
|
|
assert!(
|
|
|
|
|
total_height <= max_height,
|
|
|
|
|
"each tile in a column mustn't go beyond working area height \
|
|
|
|
|
(tile height {total_height} > max height {max_height})"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
total_height += requested_tile_height;
|
|
|
|
|
total_min_height += min_tile_height;
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
|
2025-02-01 10:46:52 +03:00
|
|
|
if !is_tabbed
|
|
|
|
|
&& tile_count > 1
|
2024-11-30 09:18:33 +03:00
|
|
|
&& self.scale.round() == self.scale
|
2025-02-07 09:36:08 +03:00
|
|
|
&& working_size.h.round() == working_size.h
|
|
|
|
|
&& gaps.round() == gaps
|
2024-11-30 09:18:33 +03:00
|
|
|
{
|
2025-02-07 09:36:08 +03:00
|
|
|
total_height += gaps * (tile_count + 1) as f64 + extra_size.h;
|
|
|
|
|
total_min_height += gaps * (tile_count + 1) as f64 + extra_size.h;
|
2025-02-07 09:36:08 +03:00
|
|
|
let max_height = f64::max(total_min_height, working_size.h);
|
2024-11-30 09:18:33 +03:00
|
|
|
assert!(
|
|
|
|
|
total_height <= max_height,
|
|
|
|
|
"multiple tiles in a column mustn't go beyond working area height \
|
|
|
|
|
(total height {total_height} > max height {max_height})"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compute_new_view_offset(
|
|
|
|
|
cur_x: f64,
|
|
|
|
|
view_width: f64,
|
|
|
|
|
new_col_x: f64,
|
|
|
|
|
new_col_width: f64,
|
|
|
|
|
gaps: f64,
|
|
|
|
|
) -> f64 {
|
|
|
|
|
// If the column is wider than the view, always left-align it.
|
|
|
|
|
if view_width <= new_col_width {
|
|
|
|
|
return 0.;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compute the padding in case it needs to be smaller due to large tile width.
|
|
|
|
|
let padding = ((view_width - new_col_width) / 2.).clamp(0., gaps);
|
|
|
|
|
|
|
|
|
|
// Compute the desired new X with padding.
|
|
|
|
|
let new_x = new_col_x - padding;
|
|
|
|
|
let new_right_x = new_col_x + new_col_width + padding;
|
|
|
|
|
|
|
|
|
|
// If the column is already fully visible, leave the view as is.
|
|
|
|
|
if cur_x <= new_x && new_right_x <= cur_x + view_width {
|
|
|
|
|
return -(new_col_x - cur_x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Otherwise, prefer the alignment that results in less motion from the current position.
|
|
|
|
|
let dist_to_left = (cur_x - new_x).abs();
|
|
|
|
|
let dist_to_right = ((cur_x + view_width) - new_right_x).abs();
|
|
|
|
|
if dist_to_left <= dist_to_right {
|
|
|
|
|
-padding
|
|
|
|
|
} else {
|
|
|
|
|
-(view_width - padding - new_col_width)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compute_working_area(
|
|
|
|
|
parent_area: Rectangle<f64, Logical>,
|
|
|
|
|
scale: f64,
|
|
|
|
|
struts: Struts,
|
|
|
|
|
) -> Rectangle<f64, Logical> {
|
|
|
|
|
let mut working_area = parent_area;
|
|
|
|
|
|
|
|
|
|
// Add struts.
|
|
|
|
|
working_area.size.w = f64::max(0., working_area.size.w - struts.left.0 - struts.right.0);
|
|
|
|
|
working_area.loc.x += struts.left.0;
|
|
|
|
|
|
|
|
|
|
working_area.size.h = f64::max(0., working_area.size.h - struts.top.0 - struts.bottom.0);
|
|
|
|
|
working_area.loc.y += struts.top.0;
|
|
|
|
|
|
|
|
|
|
// Round location to start at a physical pixel.
|
|
|
|
|
let loc = working_area
|
|
|
|
|
.loc
|
|
|
|
|
.to_physical_precise_ceil(scale)
|
|
|
|
|
.to_logical(scale);
|
|
|
|
|
|
|
|
|
|
let mut size_diff = (loc - working_area.loc).to_size();
|
|
|
|
|
size_diff.w = f64::min(working_area.size.w, size_diff.w);
|
|
|
|
|
size_diff.h = f64::min(working_area.size.h, size_diff.h);
|
|
|
|
|
|
|
|
|
|
working_area.size -= size_diff;
|
|
|
|
|
working_area.loc = loc;
|
|
|
|
|
|
|
|
|
|
working_area
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compute_toplevel_bounds(
|
|
|
|
|
border_config: niri_config::Border,
|
|
|
|
|
working_area_size: Size<f64, Logical>,
|
2025-02-07 09:36:08 +03:00
|
|
|
extra_size: Size<f64, Logical>,
|
2024-11-30 09:18:33 +03:00
|
|
|
gaps: f64,
|
|
|
|
|
) -> Size<i32, Logical> {
|
|
|
|
|
let mut border = 0.;
|
|
|
|
|
if !border_config.off {
|
|
|
|
|
border = border_config.width.0 * 2.;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Size::from((
|
2025-02-07 09:36:08 +03:00
|
|
|
f64::max(working_area_size.w - gaps * 2. - extra_size.w - border, 1.),
|
|
|
|
|
f64::max(working_area_size.h - gaps * 2. - extra_size.h - border, 1.),
|
2024-11-30 09:18:33 +03:00
|
|
|
))
|
|
|
|
|
.to_i32_floor()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn cancel_resize_for_column<W: LayoutElement>(
|
|
|
|
|
interactive_resize: &mut Option<InteractiveResize<W>>,
|
|
|
|
|
column: &mut Column<W>,
|
|
|
|
|
) {
|
|
|
|
|
if let Some(resize) = interactive_resize {
|
|
|
|
|
if column.contains(&resize.window) {
|
|
|
|
|
*interactive_resize = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for tile in &mut column.tiles {
|
|
|
|
|
tile.window_mut().cancel_interactive_resize();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-07 09:36:08 +03:00
|
|
|
fn resolve_preset_size(
|
|
|
|
|
preset: PresetSize,
|
|
|
|
|
options: &Options,
|
|
|
|
|
view_size: f64,
|
|
|
|
|
extra_size: f64,
|
|
|
|
|
) -> ResolvedSize {
|
2024-11-30 09:18:33 +03:00
|
|
|
match preset {
|
|
|
|
|
PresetSize::Proportion(proportion) => {
|
2025-02-07 09:36:08 +03:00
|
|
|
ResolvedSize::Tile((view_size - options.gaps) * proportion - options.gaps - extra_size)
|
2024-11-30 09:18:33 +03:00
|
|
|
}
|
|
|
|
|
PresetSize::Fixed(width) => ResolvedSize::Window(f64::from(width)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use niri_config::FloatOrInt;
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
use crate::utils::round_logical_in_physical;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn working_area_starts_at_physical_pixel() {
|
|
|
|
|
let struts = Struts {
|
|
|
|
|
left: FloatOrInt(0.5),
|
|
|
|
|
right: FloatOrInt(1.),
|
|
|
|
|
top: FloatOrInt(0.75),
|
|
|
|
|
bottom: FloatOrInt(1.),
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-04 10:14:51 +03:00
|
|
|
let parent_area = Rectangle::from_size(Size::from((1280., 720.)));
|
2024-11-30 09:18:33 +03:00
|
|
|
let area = compute_working_area(parent_area, 1., struts);
|
|
|
|
|
|
|
|
|
|
assert_eq!(round_logical_in_physical(1., area.loc.x), area.loc.x);
|
|
|
|
|
assert_eq!(round_logical_in_physical(1., area.loc.y), area.loc.y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn large_fractional_strut() {
|
|
|
|
|
let struts = Struts {
|
|
|
|
|
left: FloatOrInt(0.),
|
|
|
|
|
right: FloatOrInt(0.),
|
|
|
|
|
top: FloatOrInt(50000.5),
|
|
|
|
|
bottom: FloatOrInt(0.),
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-04 10:14:51 +03:00
|
|
|
let parent_area = Rectangle::from_size(Size::from((1280., 720.)));
|
2024-11-30 09:18:33 +03:00
|
|
|
compute_working_area(parent_area, 1., struts);
|
|
|
|
|
}
|
|
|
|
|
}
|