Files
niri/src/layout/workspace.rs
T

4209 lines
145 KiB
Rust
Raw Normal View History

2023-12-24 15:10:09 +04:00
use std::cmp::{max, min};
use std::iter::{self, zip};
2023-12-24 15:10:09 +04:00
use std::rc::Rc;
use std::time::Duration;
use niri_config::{
2024-11-02 09:42:46 +03:00
CenterFocusedColumn, CornerRadius, OutputName, PresetSize, Struts, Workspace as WorkspaceConfig,
};
2024-02-10 09:33:32 +04:00
use niri_ipc::SizeChange;
2024-06-17 09:16:28 +03:00
use ordered_float::NotNan;
2024-04-09 22:37:10 +04:00
use smithay::backend::renderer::gles::GlesRenderer;
2023-12-24 15:10:09 +04:00
use smithay::desktop::{layer_map_for_output, Window};
use smithay::output::Output;
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
2023-12-24 15:10:09 +04:00
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Point, Rectangle, Scale, Serial, Size, Transform};
2023-12-24 15:10:09 +04:00
2024-04-09 22:37:10 +04:00
use super::closing_window::{ClosingWindow, ClosingWindowRenderElement};
2024-11-02 09:33:44 +03:00
use super::insert_hint_element::{InsertHintElement, InsertHintRenderElement};
2024-07-15 15:51:48 +02:00
use super::tile::{Tile, TileRenderElement, TileRenderSnapshot};
2024-10-14 18:08:44 +03:00
use super::{ConfigureIntent, InteractiveResizeData, LayoutElement, Options, RemovedTile};
2023-12-24 15:10:09 +04:00
use crate::animation::Animation;
2024-05-11 13:21:05 +04:00
use crate::input::swipe_tracker::SwipeTracker;
use crate::niri_render_elements;
2024-02-06 11:24:50 +04:00
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::RenderTarget;
2024-03-19 14:27:52 +00:00
use crate::utils::id::IdCounter;
2024-08-23 15:41:06 +03:00
use crate::utils::transaction::{Transaction, TransactionBlocker};
2024-05-29 13:32:11 +03:00
use crate::utils::{output_size, send_scale_transform, ResizeEdge};
2024-04-24 21:49:07 +04:00
use crate::window::ResolvedWindowRules;
2023-12-24 15:10:09 +04:00
/// Amount of touchpad movement to scroll the view for the width of one working area.
const VIEW_GESTURE_WORKING_AREA_MOVEMENT: f64 = 1200.;
2023-12-24 15:10:09 +04:00
#[derive(Debug)]
pub struct Workspace<W: LayoutElement> {
/// The original output of this workspace.
///
/// Most of the time this will be the workspace's current output, however, after an output
/// disconnection, it may remain pointing to the disconnected output.
2024-10-10 09:24:20 +03:00
pub(super) original_output: OutputId,
2023-12-24 15:10:09 +04:00
/// Current output of this workspace.
output: Option<Output>,
/// Latest known output scale for this workspace.
///
/// This should be set from the current workspace output, or, if all outputs have been
/// disconnected, preserved until a new output is connected.
scale: smithay::output::Scale,
/// Latest known output transform for this workspace.
///
/// This should be set from the current workspace output, or, if all outputs have been
/// disconnected, preserved until a new output is connected.
transform: Transform,
2023-12-24 15:10:09 +04:00
/// Latest known view size for this workspace.
///
/// This should be computed from the current workspace output size, or, if all outputs have
/// been disconnected, preserved until a new output is connected.
2024-06-17 09:16:28 +03:00
view_size: Size<f64, Logical>,
2023-12-24 15:10:09 +04:00
/// Latest known working area for this workspace.
///
/// This is similar to view size, but takes into account things like layer shell exclusive
/// zones.
2024-06-17 09:16:28 +03:00
working_area: Rectangle<f64, Logical>,
2023-12-24 15:10:09 +04:00
/// Columns of windows on this workspace.
2024-10-10 09:24:20 +03:00
pub(super) columns: Vec<Column<W>>,
2023-12-24 15:10:09 +04:00
/// Extra per-column data.
data: Vec<ColumnData>,
2023-12-24 15:10:09 +04:00
/// Index of the currently active column, if any.
2024-10-10 09:24:20 +03:00
pub(super) active_column_idx: usize,
2023-12-24 15:10:09 +04:00
2024-05-10 16:58:53 +04:00
/// Ongoing interactive resize.
interactive_resize: Option<InteractiveResize<W>>,
2023-12-24 15:10:09 +04:00
/// 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.
2024-06-17 09:16:28 +03:00
view_offset: f64,
2023-12-24 15:10:09 +04:00
2024-02-29 08:56:20 +04:00
/// Adjustment of the view offset, if one is currently ongoing.
view_offset_adj: Option<ViewOffsetAdjustment>,
2023-12-24 15:10:09 +04:00
/// 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.
2024-06-17 09:16:28 +03:00
activate_prev_column_on_removal: Option<f64>,
2023-12-24 15:10:09 +04:00
2024-04-13 18:17:49 +04:00
/// View offset to restore after unfullscreening.
2024-06-17 09:16:28 +03:00
view_offset_before_fullscreen: Option<f64>,
2024-04-13 18:17:49 +04:00
2024-04-09 22:37:10 +04:00
/// Windows in the closing animation.
closing_windows: Vec<ClosingWindow>,
2024-07-15 15:51:48 +02:00
/// Indication where an interactively-moved window is about to be placed.
insert_hint: Option<InsertHint>,
2024-11-02 09:33:44 +03:00
/// Insert hint element for rendering.
insert_hint_element: InsertHintElement,
2024-07-15 15:51:48 +02:00
2024-06-17 09:16:28 +03:00
/// Configurable properties of the layout as received from the parent monitor.
2024-10-10 09:24:20 +03:00
pub(super) base_options: Rc<Options>,
2024-06-17 09:16:28 +03:00
/// Configurable properties of the layout with logical sizes adjusted for the current `scale`.
2024-10-10 09:24:20 +03:00
pub(super) options: Rc<Options>,
2024-03-19 14:27:52 +00:00
2024-05-11 22:40:30 +02:00
/// Optional name of this workspace.
2024-10-10 09:24:20 +03:00
pub(super) name: Option<String>,
2024-05-11 22:40:30 +02:00
2024-03-19 14:27:52 +00:00
/// Unique ID of this workspace.
id: WorkspaceId,
2023-12-24 15:10:09 +04:00
}
2024-07-15 15:51:48 +02:00
#[derive(Debug, PartialEq)]
pub enum InsertPosition {
NewColumn(usize),
InColumn(usize, usize),
}
#[derive(Debug)]
pub struct InsertHint {
pub position: InsertPosition,
pub width: ColumnWidth,
pub is_full_width: bool,
2024-11-02 09:42:46 +03:00
pub corner_radius: CornerRadius,
2024-07-15 15:51:48 +02:00
}
#[derive(Debug, Clone)]
2023-12-24 15:10:09 +04:00
pub struct OutputId(String);
impl OutputId {
pub fn matches(&self, output: &Output) -> bool {
let output_name = output.user_data().get::<OutputName>().unwrap();
output_name.matches(&self.0)
}
}
2024-03-19 14:27:52 +00:00
static WORKSPACE_ID_COUNTER: IdCounter = IdCounter::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WorkspaceId(u64);
2024-03-19 14:27:52 +00:00
impl WorkspaceId {
fn next() -> WorkspaceId {
WorkspaceId(WORKSPACE_ID_COUNTER.next())
}
2024-08-31 10:25:56 +03:00
pub fn get(self) -> u64 {
self.0
2024-08-31 10:25:56 +03:00
}
2024-09-02 09:20:23 +03:00
pub fn specific(id: u64) -> Self {
Self(id)
}
2024-03-19 14:27:52 +00:00
}
niri_render_elements! {
WorkspaceRenderElement<R> => {
Tile = TileRenderElement<R>,
2024-04-09 22:37:10 +04:00
ClosingWindow = ClosingWindowRenderElement,
2024-11-02 09:33:44 +03:00
InsertHint = InsertHintRenderElement,
}
2023-12-24 15:10:09 +04:00
}
/// Extra per-column data.
#[derive(Debug, Clone, Copy, PartialEq)]
struct ColumnData {
/// Cached actual column width.
2024-06-17 09:16:28 +03:00
width: f64,
}
2024-02-29 08:56:20 +04:00
#[derive(Debug)]
enum ViewOffsetAdjustment {
Animation(Animation),
Gesture(ViewGesture),
}
#[derive(Debug)]
struct ViewGesture {
current_view_offset: f64,
tracker: SwipeTracker,
delta_from_tracker: f64,
// The view offset we'll use if needed for activate_prev_column_on_removal.
2024-06-17 09:16:28 +03:00
static_view_offset: f64,
2024-05-11 14:01:48 +04:00
/// Whether the gesture is controlled by the touchpad.
is_touchpad: bool,
2024-02-29 08:56:20 +04:00
}
2024-05-10 16:58:53 +04:00
#[derive(Debug)]
struct InteractiveResize<W: LayoutElement> {
window: W::Id,
2024-06-17 09:16:28 +03:00
original_window_size: Size<f64, Logical>,
2024-05-10 16:58:53 +04:00
data: InteractiveResizeData,
}
2023-12-24 15:10:09 +04:00
/// 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.
2024-06-17 09:16:28 +03:00
Fixed(f64),
2024-09-12 13:36:00 +03:00
/// One of the preset widths.
Preset(usize),
2023-12-24 15:10:09 +04:00
}
/// Height of a window in a column.
///
2024-09-05 23:37:10 +02:00
/// 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.
2023-12-24 15:10:09 +04:00
///
2024-09-05 23:37:10 +02:00
/// 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.
2024-06-17 09:16:28 +03:00
#[derive(Debug, Clone, Copy, PartialEq)]
2023-12-24 15:10:09 +04:00
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.
2024-06-17 09:16:28 +03:00
Fixed(f64),
2024-09-12 13:36:00 +03:00
/// One of the preset heights (tile or window).
2024-09-05 23:37:10 +02:00
Preset(usize),
}
2024-09-12 13:36:00 +03:00
/// Resolved width or height in logical pixels.
2024-09-05 23:37:10 +02:00
#[derive(Debug, Clone, Copy)]
pub enum ResolvedSize {
/// Size of the tile including borders.
Tile(f64),
/// Size of the window excluding borders.
Window(f64),
2023-12-24 15:10:09 +04:00
}
#[derive(Debug)]
pub struct Column<W: LayoutElement> {
/// Tiles in this column.
2023-12-24 15:10:09 +04:00
///
/// Must be non-empty.
2024-10-10 09:24:20 +03:00
pub(super) tiles: Vec<Tile<W>>,
2023-12-24 15:10:09 +04:00
/// Extra per-tile data.
2023-12-24 15:10:09 +04:00
///
/// Must have the same number of elements as `tiles`.
data: Vec<TileData>,
2023-12-24 15:10:09 +04:00
/// Index of the currently active tile.
2024-10-10 09:24:20 +03:00
pub(super) active_tile_idx: usize,
2023-12-24 15:10:09 +04:00
/// 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.
2024-10-10 09:24:20 +03:00
pub(super) width: ColumnWidth,
2023-12-24 15:10:09 +04:00
/// Whether this column is full-width.
2024-10-10 09:24:20 +03:00
pub(super) is_full_width: bool,
2023-12-24 15:10:09 +04:00
/// Whether this column contains a single full-screened window.
2024-10-10 09:24:20 +03:00
pub(super) is_fullscreen: bool,
2023-12-24 15:10:09 +04:00
2024-04-08 18:23:18 +04:00
/// Animation of the render offset during window swapping.
move_animation: Option<Animation>,
2023-12-24 15:10:09 +04:00
/// Latest known view size for this column's workspace.
2024-06-17 09:16:28 +03:00
view_size: Size<f64, Logical>,
2023-12-24 15:10:09 +04:00
/// Latest known working area for this column's workspace.
2024-06-17 09:16:28 +03:00
working_area: Rectangle<f64, Logical>,
/// Scale of the output the column is on (and rounds its sizes to).
scale: f64,
2023-12-24 15:10:09 +04:00
/// 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.
2024-06-17 09:16:28 +03:00
size: Size<f64, Logical>,
/// Cached whether the tile is being interactively resized by its left edge.
interactively_resizing_by_left_edge: bool,
}
2023-12-24 15:10:09 +04:00
impl OutputId {
pub fn new(output: &Output) -> Self {
let output_name = output.user_data().get::<OutputName>().unwrap();
Self(output_name.format_make_model_serial_or_connector())
2023-12-24 15:10:09 +04:00
}
}
2024-02-26 18:47:46 +01:00
impl ViewOffsetAdjustment {
pub fn is_animation(&self) -> bool {
matches!(self, Self::Animation(_))
}
2024-02-26 18:47:46 +01:00
pub fn target_view_offset(&self) -> f64 {
match self {
ViewOffsetAdjustment::Animation(anim) => anim.to(),
ViewOffsetAdjustment::Gesture(gesture) => gesture.current_view_offset,
}
}
}
impl ColumnData {
pub fn new<W: LayoutElement>(column: &Column<W>) -> Self {
2024-06-17 09:16:28 +03:00
let mut rv = Self { width: 0. };
rv.update(column);
rv
}
pub fn update<W: LayoutElement>(&mut self, column: &Column<W>) {
self.width = column.width();
}
}
2023-12-24 15:10:09 +04:00
impl ColumnWidth {
2024-06-17 09:16:28 +03:00
fn resolve(self, options: &Options, view_width: f64) -> f64 {
2023-12-24 15:10:09 +04:00
match self {
ColumnWidth::Proportion(proportion) => {
2024-06-17 09:16:28 +03:00
(view_width - options.gaps) * proportion - options.gaps
2023-12-24 15:10:09 +04:00
}
2024-09-05 23:37:10 +02:00
ColumnWidth::Preset(idx) => {
options.preset_column_widths[idx].resolve(options, view_width)
}
2023-12-24 15:10:09 +04:00
ColumnWidth::Fixed(width) => width,
}
}
}
2024-09-05 23:37:10 +02:00
impl From<PresetSize> for ColumnWidth {
fn from(value: PresetSize) -> Self {
2023-12-24 15:10:09 +04:00
match value {
2024-09-05 23:37:10 +02:00
PresetSize::Proportion(p) => Self::Proportion(p.clamp(0., 10000.)),
PresetSize::Fixed(f) => Self::Fixed(f64::from(f.clamp(1, 100000))),
}
}
}
fn resolve_preset_size(preset: PresetSize, options: &Options, view_size: f64) -> ResolvedSize {
match preset {
PresetSize::Proportion(proportion) => {
ResolvedSize::Tile((view_size - options.gaps) * proportion - options.gaps)
2023-12-24 15:10:09 +04:00
}
2024-09-05 23:37:10 +02:00
PresetSize::Fixed(width) => ResolvedSize::Window(f64::from(width)),
2023-12-24 15:10:09 +04:00
}
}
impl WindowHeight {
const fn auto_1() -> Self {
Self::Auto { weight: 1. }
}
}
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()
.map_or(false, |data| data.edges.contains(ResizeEdge::LEFT));
}
}
2023-12-24 15:10:09 +04:00
impl<W: LayoutElement> Workspace<W> {
pub fn new(output: Output, options: Rc<Options>) -> Self {
2024-05-11 22:40:30 +02:00
Self::new_with_config(output, None, options)
}
pub fn new_with_config(
output: Output,
config: Option<WorkspaceConfig>,
2024-06-17 09:16:28 +03:00
base_options: Rc<Options>,
2024-05-11 22:40:30 +02:00
) -> Self {
let original_output = config
.as_ref()
.and_then(|c| c.open_on_output.clone())
.map(OutputId)
.unwrap_or(OutputId::new(&output));
2024-06-17 09:16:28 +03:00
let scale = output.current_scale();
let options =
Rc::new(Options::clone(&base_options).adjusted_for_scale(scale.fractional_scale()));
2023-12-24 15:10:09 +04:00
let working_area = compute_working_area(&output, options.struts);
2024-06-17 09:16:28 +03:00
2023-12-24 15:10:09 +04:00
Self {
2024-05-11 22:40:30 +02:00
original_output,
2024-06-17 09:16:28 +03:00
scale,
transform: output.current_transform(),
2023-12-24 15:10:09 +04:00
view_size: output_size(&output),
working_area,
output: Some(output),
columns: vec![],
data: vec![],
2023-12-24 15:10:09 +04:00
active_column_idx: 0,
2024-05-10 16:58:53 +04:00
interactive_resize: None,
2024-06-17 09:16:28 +03:00
view_offset: 0.,
2024-02-29 08:56:20 +04:00
view_offset_adj: None,
activate_prev_column_on_removal: None,
2024-04-13 18:17:49 +04:00
view_offset_before_fullscreen: None,
2024-04-09 22:37:10 +04:00
closing_windows: vec![],
2024-07-15 15:51:48 +02:00
insert_hint: None,
2024-11-02 09:33:44 +03:00
insert_hint_element: InsertHintElement::new(options.insert_hint),
2024-06-17 09:16:28 +03:00
base_options,
2023-12-24 15:10:09 +04:00
options,
2024-05-11 22:40:30 +02:00
name: config.map(|c| c.name.0),
2024-03-19 14:27:52 +00:00
id: WorkspaceId::next(),
2023-12-24 15:10:09 +04:00
}
}
2024-05-11 22:40:30 +02:00
pub fn new_with_config_no_outputs(
config: Option<WorkspaceConfig>,
2024-06-17 09:16:28 +03:00
base_options: Rc<Options>,
2024-05-11 22:40:30 +02:00
) -> Self {
let original_output = OutputId(
config
.as_ref()
.and_then(|c| c.open_on_output.clone())
2024-05-11 22:40:30 +02:00
.unwrap_or_default(),
);
2024-06-17 09:16:28 +03:00
let scale = smithay::output::Scale::Integer(1);
let options =
Rc::new(Options::clone(&base_options).adjusted_for_scale(scale.fractional_scale()));
2023-12-24 15:10:09 +04:00
Self {
output: None,
2024-06-17 09:16:28 +03:00
scale,
transform: Transform::Normal,
2024-05-11 22:40:30 +02:00
original_output,
2024-06-17 09:16:28 +03:00
view_size: Size::from((1280., 720.)),
working_area: Rectangle::from_loc_and_size((0., 0.), (1280., 720.)),
2023-12-24 15:10:09 +04:00
columns: vec![],
data: vec![],
2023-12-24 15:10:09 +04:00
active_column_idx: 0,
2024-05-10 16:58:53 +04:00
interactive_resize: None,
2024-06-17 09:16:28 +03:00
view_offset: 0.,
2024-02-29 08:56:20 +04:00
view_offset_adj: None,
activate_prev_column_on_removal: None,
2024-04-13 18:17:49 +04:00
view_offset_before_fullscreen: None,
2024-04-09 22:37:10 +04:00
closing_windows: vec![],
2024-07-15 15:51:48 +02:00
insert_hint: None,
2024-11-02 09:33:44 +03:00
insert_hint_element: InsertHintElement::new(options.insert_hint),
2024-06-17 09:16:28 +03:00
base_options,
2023-12-24 15:10:09 +04:00
options,
2024-05-11 22:40:30 +02:00
name: config.map(|c| c.name.0),
2024-03-19 14:27:52 +00:00
id: WorkspaceId::next(),
2023-12-24 15:10:09 +04:00
}
}
2024-05-11 22:40:30 +02:00
pub fn new_no_outputs(options: Rc<Options>) -> Self {
Self::new_with_config_no_outputs(None, options)
}
2024-03-19 14:27:52 +00:00
pub fn id(&self) -> WorkspaceId {
self.id
}
2024-10-10 09:24:20 +03:00
pub fn name(&self) -> Option<&String> {
self.name.as_ref()
}
2024-05-11 22:40:30 +02:00
pub fn unname(&mut self) {
self.name = None;
}
2024-06-17 09:16:28 +03:00
pub fn scale(&self) -> smithay::output::Scale {
self.scale
}
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)
}
pub fn advance_animations(&mut self, current_time: Duration) {
2024-02-29 08:56:20 +04:00
if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj {
anim.set_current_time(current_time);
2024-06-17 09:16:28 +03:00
self.view_offset = anim.value();
2024-02-29 08:56:20 +04:00
if anim.is_done() {
self.view_offset_adj = None;
2023-12-24 15:10:09 +04:00
}
2024-02-29 08:56:20 +04:00
} else if let Some(ViewOffsetAdjustment::Gesture(gesture)) = &self.view_offset_adj {
2024-06-17 09:16:28 +03:00
self.view_offset = gesture.current_view_offset;
2023-12-24 15:10:09 +04:00
}
for col in &mut self.columns {
col.advance_animations(current_time);
}
2024-04-09 22:37:10 +04:00
self.closing_windows.retain_mut(|closing| {
closing.advance_animations(current_time);
closing.are_animations_ongoing()
});
2023-12-24 15:10:09 +04:00
}
pub fn are_animations_ongoing(&self) -> bool {
self.view_offset_adj
.as_ref()
.is_some_and(|s| s.is_animation())
|| self.columns.iter().any(Column::are_animations_ongoing)
|| !self.closing_windows.is_empty()
}
pub fn are_transitions_ongoing(&self) -> bool {
2024-04-09 22:37:10 +04:00
self.view_offset_adj.is_some()
|| self.columns.iter().any(Column::are_animations_ongoing)
|| !self.closing_windows.is_empty()
2023-12-24 15:10:09 +04:00
}
pub fn update_render_elements(&mut self, is_active: bool) {
2024-06-17 09:16:28 +03:00
let view_pos = Point::from((self.view_pos(), 0.));
let view_size = self.view_size();
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;
2024-06-17 09:16:28 +03:00
let col_off = Point::from((col_x, 0.));
let col_pos = view_pos - col_off - col.render_offset();
let view_rect = Rectangle::from_loc_and_size(col_pos, view_size);
col.update_render_elements(is_active, view_rect);
}
2024-07-15 15:51:48 +02:00
if let Some(insert_hint) = &self.insert_hint {
if let Some(area) = self.insert_hint_area(insert_hint) {
2024-11-02 09:33:44 +03:00
let view_rect = Rectangle::from_loc_and_size(area.loc.upscale(-1.), view_size);
self.insert_hint_element.update_render_elements(
area.size,
view_rect,
2024-11-02 09:42:46 +03:00
insert_hint.corner_radius,
2024-11-02 09:33:44 +03:00
self.scale.fractional_scale(),
);
2024-07-15 15:51:48 +02:00
}
}
}
2024-06-17 09:16:28 +03:00
pub fn update_config(&mut self, base_options: Rc<Options>) {
let scale = self.scale.fractional_scale();
let options = Rc::new(Options::clone(&base_options).adjusted_for_scale(scale));
for (column, data) in zip(&mut self.columns, &mut self.data) {
2024-06-17 09:16:28 +03:00
column.update_config(scale, options.clone());
data.update(column);
2023-12-24 15:10:09 +04:00
}
2024-11-02 09:33:44 +03:00
self.insert_hint_element.update_config(options.insert_hint);
2024-06-17 09:16:28 +03:00
self.base_options = base_options;
2023-12-24 15:10:09 +04:00
self.options = options;
}
pub fn update_shaders(&mut self) {
for col in &mut self.columns {
for tile in &mut col.tiles {
tile.update_shaders();
}
}
2024-11-02 09:33:44 +03:00
self.insert_hint_element.update_shaders();
}
2023-12-24 15:10:09 +04:00
pub fn windows(&self) -> impl Iterator<Item = &W> + '_ {
self.columns
.iter()
.flat_map(|col| col.tiles.iter())
.map(Tile::window)
2023-12-24 15:10:09 +04:00
}
2024-03-19 14:41:17 +04:00
pub fn windows_mut(&mut self) -> impl Iterator<Item = &mut W> + '_ {
2024-07-15 15:51:48 +02:00
self.tiles_mut().map(Tile::window_mut)
}
pub fn tiles_mut(&mut self) -> impl Iterator<Item = &mut Tile<W>> + '_ {
self.columns.iter_mut().flat_map(|col| col.tiles.iter_mut())
2024-03-19 14:41:17 +04:00
}
2024-05-11 22:40:30 +02:00
pub fn current_output(&self) -> Option<&Output> {
self.output.as_ref()
}
2024-06-20 12:04:10 +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())
}
2024-05-11 22:40:30 +02:00
2023-12-24 15:10:09 +04:00
pub fn set_output(&mut self, output: Option<Output>) {
if self.output == output {
return;
}
if let Some(output) = self.output.take() {
for win in self.windows() {
win.output_leave(&output);
}
}
self.output = output;
if let Some(output) = &self.output {
// Normalize original output: possibly replace connector with make/model/serial.
if self.original_output.matches(output) {
self.original_output = OutputId::new(output);
}
let scale = output.current_scale();
let transform = output.current_transform();
2023-12-24 15:10:09 +04:00
let working_area = compute_working_area(output, self.options.struts);
self.set_view_size(scale, transform, output_size(output), working_area);
2023-12-24 15:10:09 +04:00
for win in self.windows() {
self.enter_output_for_window(win);
}
}
}
fn enter_output_for_window(&self, window: &W) {
if let Some(output) = &self.output {
window.set_preferred_scale_transform(self.scale, self.transform);
window.output_enter(output);
2023-12-24 15:10:09 +04:00
}
}
pub fn set_view_size(
&mut self,
scale: smithay::output::Scale,
transform: Transform,
2024-06-17 09:16:28 +03:00
size: Size<f64, Logical>,
working_area: Rectangle<f64, Logical>,
2023-12-24 15:10:09 +04:00
) {
let scale_transform_changed = self.transform != transform
|| self.scale.integer_scale() != scale.integer_scale()
|| self.scale.fractional_scale() != scale.fractional_scale();
if !scale_transform_changed && self.view_size == size && self.working_area == working_area {
2023-12-24 15:10:09 +04:00
return;
}
2024-06-17 09:16:28 +03:00
let fractional_scale_changed = self.scale.fractional_scale() != scale.fractional_scale();
self.scale = scale;
self.transform = transform;
2023-12-24 15:10:09 +04:00
self.view_size = size;
self.working_area = working_area;
2024-06-17 09:16:28 +03:00
if fractional_scale_changed {
// Options need to be recomputed for the new scale.
self.update_config(self.base_options.clone());
}
2023-12-24 15:10:09 +04:00
for col in &mut self.columns {
col.set_view_size(self.view_size, self.working_area);
}
if scale_transform_changed {
for window in self.windows() {
window.set_preferred_scale_transform(self.scale, self.transform);
}
}
2023-12-24 15:10:09 +04:00
}
2024-06-17 09:16:28 +03:00
pub fn view_size(&self) -> Size<f64, Logical> {
2024-02-03 09:45:26 +04:00
self.view_size
}
2024-04-24 21:49:07 +04:00
fn toplevel_bounds(&self, rules: &ResolvedWindowRules) -> Size<i32, Logical> {
let border_config = rules.border.resolve_against(self.options.border);
compute_toplevel_bounds(border_config, self.working_area.size, self.options.gaps)
2023-12-24 15:10:09 +04:00
}
pub fn resolve_default_width(
2024-02-13 17:46:37 +04:00
&self,
default_width: Option<Option<ColumnWidth>>,
) -> Option<ColumnWidth> {
match default_width {
2024-02-13 17:46:37 +04:00
Some(Some(width)) => Some(width),
Some(None) => None,
2024-09-05 23:37:10 +02:00
None => self.options.default_column_width,
}
}
2024-02-13 17:46:37 +04:00
2024-04-24 21:49:07 +04:00
pub fn new_window_size(
&self,
width: Option<ColumnWidth>,
rules: &ResolvedWindowRules,
) -> Size<i32, Logical> {
let border = rules.border.resolve_against(self.options.border);
let width = if let Some(width) = width {
let is_fixed = matches!(width, ColumnWidth::Fixed(_));
let mut width = width.resolve(&self.options, self.working_area.size.w);
2024-04-24 21:49:07 +04:00
if !is_fixed && !border.off {
2024-06-17 09:16:28 +03:00
width -= border.width.0 * 2.;
}
2024-06-17 09:16:28 +03:00
max(1, width.floor() as i32)
2023-12-24 15:10:09 +04:00
} else {
0
};
2024-06-17 09:16:28 +03:00
let mut height = self.working_area.size.h - self.options.gaps * 2.;
2024-04-24 21:49:07 +04:00
if !border.off {
2024-06-17 09:16:28 +03:00
height -= border.width.0 * 2.;
}
2024-06-17 09:16:28 +03:00
Size::from((width, max(height.floor() as i32, 1)))
2024-02-03 09:45:26 +04:00
}
2023-12-24 15:10:09 +04:00
2024-04-24 21:49:07 +04:00
pub fn configure_new_window(
&self,
window: &Window,
width: Option<ColumnWidth>,
rules: &ResolvedWindowRules,
) {
window.with_surfaces(|surface, data| {
send_scale_transform(surface, data, self.scale, self.transform);
});
2023-12-24 15:10:09 +04:00
2024-02-24 18:31:54 +01:00
window
.toplevel()
.expect("no x11 support")
.with_pending_state(|state| {
if state.states.contains(xdg_toplevel::State::Fullscreen) {
2024-06-17 09:16:28 +03:00
state.size = Some(self.view_size.to_i32_round());
2024-02-24 18:31:54 +01:00
} else {
2024-04-24 21:49:07 +04:00
state.size = Some(self.new_window_size(width, rules));
2024-02-24 18:31:54 +01:00
}
2024-04-24 21:49:07 +04:00
state.bounds = Some(self.toplevel_bounds(rules));
2024-02-24 18:31:54 +01:00
});
2023-12-24 15:10:09 +04:00
}
2024-07-15 15:51:48 +02:00
fn compute_new_view_offset_fit(
&self,
current_x: f64,
col_x: f64,
width: f64,
is_fullscreen: bool,
) -> f64 {
if is_fullscreen {
2024-06-17 09:16:28 +03:00
return 0.;
2023-12-24 15:10:09 +04:00
}
2024-03-01 08:09:03 +04:00
let final_x = if let Some(ViewOffsetAdjustment::Animation(anim)) = &self.view_offset_adj {
2024-06-17 09:16:28 +03:00
current_x - self.view_offset + anim.to()
2023-12-24 15:10:09 +04:00
} else {
current_x
};
let new_offset = compute_new_view_offset(
final_x + self.working_area.loc.x,
self.working_area.size.w,
2024-07-15 15:51:48 +02:00
col_x,
width,
2023-12-24 15:10:09 +04:00
self.options.gaps,
);
// Non-fullscreen windows are always offset at least by the working area position.
new_offset - self.working_area.loc.x
}
2024-07-15 15:51:48 +02:00
fn compute_new_view_offset_centered(
&self,
current_x: f64,
col_x: f64,
width: f64,
is_fullscreen: bool,
) -> f64 {
if is_fullscreen {
return self.compute_new_view_offset_fit(current_x, col_x, width, is_fullscreen);
}
// Columns wider than the view are left-aligned (the fit code can deal with that).
if self.working_area.size.w <= width {
2024-07-15 15:51:48 +02:00
return self.compute_new_view_offset_fit(current_x, col_x, width, is_fullscreen);
}
-(self.working_area.size.w - width) / 2. - self.working_area.loc.x
}
2024-07-15 15:51:48 +02:00
fn compute_new_view_offset_for_column_fit(&self, current_x: f64, idx: usize) -> f64 {
let col = &self.columns[idx];
self.compute_new_view_offset_fit(
current_x,
self.column_x(idx),
col.width(),
col.is_fullscreen,
)
}
fn compute_new_view_offset_for_column_centered(&self, current_x: f64, idx: usize) -> f64 {
let col = &self.columns[idx];
self.compute_new_view_offset_centered(
current_x,
self.column_x(idx),
col.width(),
col.is_fullscreen,
)
}
fn compute_new_view_offset_for_column(
&self,
current_x: f64,
idx: usize,
prev_idx: Option<usize>,
) -> f64 {
if self.is_centering_focused_column() {
return self.compute_new_view_offset_for_column_centered(current_x, idx);
}
match self.options.center_focused_column {
CenterFocusedColumn::Always => {
self.compute_new_view_offset_for_column_centered(current_x, idx)
}
CenterFocusedColumn::OnOverflow => {
let Some(prev_idx) = prev_idx else {
return self.compute_new_view_offset_for_column_fit(current_x, idx);
};
// 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_x = self.column_x(source_idx);
let source_width = self.columns[source_idx].width();
let target_x = self.column_x(idx);
let target_width = self.columns[idx].width();
let total_width = if source_x < target_x {
// Source is left from target.
target_x - source_x + target_width
} else {
// Source is right from target.
source_x - target_x + source_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(current_x, idx)
} else {
self.compute_new_view_offset_for_column_centered(current_x, idx)
}
}
CenterFocusedColumn::Never => {
self.compute_new_view_offset_for_column_fit(current_x, idx)
}
}
}
2024-06-17 09:16:28 +03:00
fn animate_view_offset(&mut self, current_x: f64, idx: usize, new_view_offset: f64) {
self.animate_view_offset_with_config(
current_x,
idx,
new_view_offset,
self.options.animations.horizontal_view_movement.0,
);
}
fn animate_view_offset_with_config(
&mut self,
2024-06-17 09:16:28 +03:00
current_x: f64,
idx: usize,
2024-06-17 09:16:28 +03:00
new_view_offset: f64,
config: niri_config::Animation,
) {
2023-12-24 15:10:09 +04:00
let new_col_x = self.column_x(idx);
let old_col_x = current_x - self.view_offset;
let offset_delta = old_col_x - new_col_x;
self.view_offset += offset_delta;
2023-12-24 15:10:09 +04:00
let pixel = 1. / self.scale.fractional_scale();
2023-12-24 15:10:09 +04:00
// If we're already animating towards that, don't restart it.
if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj {
// Offset the animation for the active column change.
anim.offset(offset_delta);
let to_diff = new_view_offset - anim.to();
if (anim.value() - self.view_offset).abs() < pixel && to_diff.abs() < pixel {
// Correct for any inaccuracy.
anim.offset(to_diff);
2023-12-24 15:10:09 +04:00
return;
}
}
// If our view offset is already this, we don't need to do anything.
if (self.view_offset - new_view_offset).abs() < pixel {
// Correct for any inaccuracy.
self.view_offset = new_view_offset;
2024-02-29 08:56:20 +04:00
self.view_offset_adj = None;
2023-12-24 15:10:09 +04:00
return;
}
2024-03-05 13:32:30 +04:00
// FIXME: also compute and use current velocity.
2024-02-29 08:56:20 +04:00
self.view_offset_adj = Some(ViewOffsetAdjustment::Animation(Animation::new(
2024-06-17 09:16:28 +03:00
self.view_offset,
new_view_offset,
2024-03-05 13:32:30 +04:00
0.,
config,
2024-02-29 08:56:20 +04:00
)));
2023-12-24 15:10:09 +04:00
}
fn animate_view_offset_to_column_centered(
&mut self,
2024-06-17 09:16:28 +03:00
current_x: f64,
idx: usize,
config: niri_config::Animation,
) {
let new_view_offset = self.compute_new_view_offset_for_column_centered(current_x, idx);
self.animate_view_offset_with_config(current_x, idx, new_view_offset, config);
2024-01-08 17:17:19 +04:00
}
fn animate_view_offset_to_column_with_config(
&mut self,
2024-06-17 09:16:28 +03:00
current_x: f64,
idx: usize,
prev_idx: Option<usize>,
config: niri_config::Animation,
) {
let new_view_offset = self.compute_new_view_offset_for_column(current_x, idx, prev_idx);
self.animate_view_offset_with_config(current_x, idx, new_view_offset, config);
2024-01-08 17:17:19 +04:00
}
fn animate_view_offset_to_column(
&mut self,
2024-06-17 09:16:28 +03:00
current_x: f64,
idx: usize,
prev_idx: Option<usize>,
) {
self.animate_view_offset_to_column_with_config(
current_x,
idx,
prev_idx,
self.options.animations.horizontal_view_movement.0,
)
}
2024-02-28 17:23:03 +04:00
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) {
2024-02-28 17:23:03 +04:00
if self.active_column_idx == idx {
return;
}
let current_x = self.view_pos();
self.animate_view_offset_to_column_with_config(
current_x,
idx,
Some(self.active_column_idx),
config,
);
2023-12-24 15:10:09 +04:00
self.active_column_idx = idx;
// A different column was activated; reset the flag.
self.activate_prev_column_on_removal = None;
2024-04-13 18:17:49 +04:00
self.view_offset_before_fullscreen = None;
2024-05-10 16:58:53 +04:00
self.interactive_resize = None;
2023-12-24 15:10:09 +04:00
}
pub fn has_windows(&self) -> bool {
self.windows().next().is_some()
}
pub fn has_window(&self, window: &W::Id) -> bool {
self.windows().any(|win| win.id() == window)
2023-12-24 15:10:09 +04:00
}
pub fn find_wl_surface(&self, wl_surface: &WlSurface) -> Option<&W> {
self.windows().find(|win| win.is_wl_surface(wl_surface))
}
2024-03-19 14:41:17 +04:00
pub fn find_wl_surface_mut(&mut self, wl_surface: &WlSurface) -> Option<&mut W> {
self.windows_mut().find(|win| win.is_wl_surface(wl_surface))
}
2024-07-15 15:51:48 +02:00
pub fn set_insert_hint(&mut self, insert_hint: InsertHint) {
if self.options.insert_hint.off {
return;
}
self.insert_hint = Some(insert_hint);
}
pub fn clear_insert_hint(&mut self) {
self.insert_hint = None;
}
pub fn get_insert_position(&self, pos: Point<f64, Logical>) -> InsertPosition {
if self.columns.is_empty() {
return InsertPosition::NewColumn(0);
}
let x = pos.x + self.view_pos();
// Aim for the center of the gap.
let x = x + self.options.gaps / 2.;
let y = pos.y + self.options.gaps / 2.;
// Insert position is before the first column.
if x < 0. {
return InsertPosition::NewColumn(0);
}
// Find the closest gap between columns.
let (closest_col_idx, col_x) = self
.column_xs(self.data.iter().copied())
.enumerate()
.min_by_key(|(_, col_x)| NotNan::new((col_x - x).abs()).unwrap())
.unwrap();
// Find the column containing the position.
let (col_idx, _) = self
.column_xs(self.data.iter().copied())
.enumerate()
.take_while(|(_, col_x)| *col_x <= x)
.last()
.unwrap_or((0, 0.));
// Insert position is past the last column.
if col_idx == self.columns.len() {
return InsertPosition::NewColumn(closest_col_idx);
}
// Find the closest gap between tiles.
let col = &self.columns[col_idx];
let (closest_tile_idx, tile_off) = col
.tile_offsets()
.enumerate()
.min_by_key(|(_, tile_off)| NotNan::new((tile_off.y - y).abs()).unwrap())
.unwrap();
// Return the closest among the vertical and the horizontal gap.
let vert_dist = (col_x - x).abs();
let hor_dist = (tile_off.y - y).abs();
if vert_dist <= hor_dist {
InsertPosition::NewColumn(closest_col_idx)
} else {
InsertPosition::InColumn(col_idx, closest_tile_idx)
}
}
pub fn add_window(
2023-12-24 15:10:09 +04:00
&mut self,
col_idx: Option<usize>,
2023-12-24 15:10:09 +04:00
window: W,
activate: bool,
width: ColumnWidth,
is_full_width: bool,
) {
2024-06-17 09:16:28 +03:00
let tile = Tile::new(window, self.scale.fractional_scale(), self.options.clone());
self.add_tile(col_idx, tile, activate, width, is_full_width, None);
}
2024-07-15 15:51:48 +02:00
pub fn add_tile(
&mut self,
col_idx: Option<usize>,
tile: Tile<W>,
activate: bool,
width: ColumnWidth,
is_full_width: bool,
2024-04-17 18:03:17 +04:00
anim_config: Option<niri_config::Animation>,
) {
let column = Column::new_with_tile(
tile,
2023-12-24 15:10:09 +04:00
self.view_size,
self.working_area,
2024-06-17 09:16:28 +03:00
self.scale.fractional_scale(),
2023-12-24 15:10:09 +04:00
self.options.clone(),
width,
is_full_width,
2024-04-16 09:58:39 +04:00
true,
2023-12-24 15:10:09 +04:00
);
self.add_column(col_idx, column, activate, anim_config);
}
2024-10-11 11:02:32 +03:00
pub fn add_tile_to_column(
&mut self,
col_idx: usize,
tile_idx: Option<usize>,
tile: Tile<W>,
activate: bool,
) {
self.enter_output_for_window(tile.window());
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());
let was_fullscreen = target_column.tiles[target_column.active_tile_idx].is_fullscreen();
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
// gone into fullscreen yet, then clear the stored view offset, since we just asked it to
// stop going into fullscreen.
if col_idx == self.active_column_idx && !was_fullscreen {
self.view_offset_before_fullscreen = None;
}
if activate {
target_column.active_tile_idx = tile_idx;
if self.active_column_idx != col_idx {
self.activate_column(col_idx);
}
} else if tile_idx <= target_column.active_tile_idx {
target_column.active_tile_idx += 1;
}
// 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_window_right_of(
&mut self,
right_of: &W::Id,
window: W,
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;
// Activate the new window if right_of was active.
let activate = self.active_column_idx == right_of_idx;
self.add_window(Some(col_idx), window, activate, width, is_full_width);
}
pub fn add_column(
&mut self,
idx: Option<usize>,
mut column: Column<W>,
activate: bool,
anim_config: Option<niri_config::Animation>,
) {
2024-01-15 10:31:44 +04:00
for tile in &column.tiles {
self.enter_output_for_window(tile.window());
}
let was_empty = self.columns.is_empty();
let idx = idx.unwrap_or_else(|| {
if was_empty {
0
} else {
self.active_column_idx + 1
}
});
2024-01-15 10:31:44 +04:00
2024-06-17 09:16:28 +03:00
column.update_config(self.scale.fractional_scale(), self.options.clone());
2024-01-15 10:31:44 +04:00
column.set_view_size(self.view_size, self.working_area);
self.data.insert(idx, ColumnData::new(&column));
2024-01-15 10:31:44 +04:00
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.
2024-01-15 10:31:44 +04:00
if was_empty {
self.view_offset = 0.;
2024-02-29 08:56:20 +04:00
self.view_offset_adj = None;
self.view_offset =
self.compute_new_view_offset_for_column(self.view_pos(), idx, None);
2024-01-15 10:31:44 +04:00
}
let prev_offset = (!was_empty && idx == self.active_column_idx + 1)
.then(|| self.static_view_offset());
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;
2024-01-15 10:31:44 +04:00
}
// 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);
}
}
2024-01-15 10:31:44 +04:00
}
pub fn remove_tile_by_idx(
&mut self,
column_idx: usize,
2024-10-14 18:08:44 +03:00
tile_idx: usize,
2024-08-23 15:41:06 +03:00
transaction: Transaction,
anim_config: Option<niri_config::Animation>,
2024-10-14 18:08:44 +03:00
) -> 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,
};
}
2023-12-24 18:00:46 +04:00
let column = &mut self.columns[column_idx];
let prev_width = self.data[column_idx].width;
// 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.
2024-10-14 18:08:44 +03:00
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..] {
2024-04-19 12:44:24 +04:00
tile.animate_move_y_from(offset_y);
}
2024-10-14 18:08:44 +03:00
let tile = column.tiles.remove(tile_idx);
column.data.remove(tile_idx);
2023-12-24 18:00:46 +04: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.;
}
}
2023-12-24 15:10:09 +04:00
if let Some(output) = &self.output {
tile.window().output_leave(output);
2023-12-24 15:10:09 +04:00
}
2024-05-10 16:58:53 +04:00
// Stop interactive resize.
if let Some(resize) = &self.interactive_resize {
if tile.window().id() == &resize.window {
self.interactive_resize = None;
}
}
2024-10-14 18:08:44 +03:00
let tile = RemovedTile {
tile,
width: column.width,
is_full_width: column.is_full_width,
};
column.active_tile_idx = min(column.active_tile_idx, column.tiles.len() - 1);
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.
let movement_config = anim_config.unwrap_or(self.options.animations.window_movement.0);
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
2023-12-24 18:00:46 +04:00
}
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);
}
}
2024-01-15 10:31:44 +04:00
let column = self.columns.remove(column_idx);
self.data.remove(column_idx);
2024-01-15 10:31:44 +04:00
if let Some(output) = &self.output {
for tile in &column.tiles {
tile.window().output_leave(output);
}
}
2024-05-10 16:58:53 +04:00
// 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;
}
}
2024-01-15 10:31:44 +04:00
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;
2024-01-15 10:31:44 +04:00
}
2024-04-13 18:17:49 +04:00
if column_idx == self.active_column_idx {
self.view_offset_before_fullscreen = None;
}
2024-01-15 10:31:44 +04:00
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 {
2024-01-15 10:31:44 +04:00
// 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.
let current_x = self.view_pos();
self.animate_view_offset_with_config(
current_x,
self.active_column_idx,
prev_offset,
view_config,
);
self.animate_view_offset_to_column_with_config(
current_x,
self.active_column_idx,
None,
view_config,
);
}
2024-01-15 10:31:44 +04:00
} else {
self.activate_column_with_anim_config(
min(self.active_column_idx, self.columns.len() - 1),
view_config,
);
2024-01-15 10:31:44 +04:00
}
column
}
2024-10-14 18:08:44 +03:00
pub fn remove_tile(&mut self, window: &W::Id, transaction: Transaction) -> RemovedTile<W> {
2023-12-24 18:00:46 +04:00
let column_idx = self
.columns
.iter()
.position(|col| col.contains(window))
.unwrap();
let column = &self.columns[column_idx];
2024-10-14 18:08:44 +03:00
let tile_idx = column.position(window).unwrap();
self.remove_tile_by_idx(column_idx, tile_idx, transaction, None)
2023-12-24 15:10:09 +04:00
}
2024-05-10 16:58:53 +04:00
pub fn update_window(&mut self, window: &W::Id, serial: Option<Serial>) {
2024-04-13 11:07:23 +04:00
let (col_idx, column) = self
2023-12-24 15:10:09 +04:00
.columns
.iter_mut()
.enumerate()
.find(|(_, col)| col.contains(window))
.unwrap();
let (tile_idx, tile) = column
2024-04-13 11:07:23 +04:00
.tiles
.iter_mut()
.enumerate()
.find(|(_, tile)| tile.window().id() == window)
2024-04-13 11:07:23 +04:00
.unwrap();
let was_fullscreen = tile.is_fullscreen();
let resize = tile.window_mut().interactive_resize_data();
2024-04-13 11:07:23 +04:00
// Do this before calling update_window() so it can get up-to-date info.
if let Some(serial) = serial {
tile.window_mut().update_interactive_resize(serial);
}
let prev_width = self.data[col_idx].width;
2024-04-13 18:17:49 +04:00
column.update_window(window);
self.data[col_idx].update(column);
2024-04-13 11:07:23 +04:00
column.update_tile_sizes(false);
2023-12-24 15:10:09 +04:00
let offset = prev_width - self.data[col_idx].width;
2024-04-13 11:07:23 +04:00
// Move other columns in tandem with resizing.
let started_resize_anim =
2024-06-17 09:16:28 +03:00
column.tiles[tile_idx].resize_animation().is_some() && offset != 0.;
if started_resize_anim {
2024-04-13 11:07:23 +04:00
if self.active_column_idx <= col_idx {
for col in &mut self.columns[col_idx + 1..] {
col.animate_move_from_with_config(
offset,
2024-04-21 20:10:35 +04:00
self.options.animations.window_resize.anim,
2024-04-13 11:07:23 +04:00
);
}
} else {
2024-04-17 10:35:46 +04:00
for col in &mut self.columns[..=col_idx] {
2024-04-13 11:07:23 +04:00
col.animate_move_from_with_config(
-offset,
2024-04-21 20:10:35 +04:00
self.options.animations.window_resize.anim,
2024-04-13 11:07:23 +04:00
);
}
}
}
2024-05-11 10:59:46 +04:00
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.
2024-06-17 09:16:28 +03:00
let resize = if offset != 0. { resize } else { None };
2024-05-10 16:58:53 +04:00
if let Some(resize) = resize {
// 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();
2024-05-10 16:58:53 +04:00
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 =
2024-06-17 09:16:28 +03:00
-(self.working_area.size.w - width) / 2. - self.working_area.loc.x;
new_offset - self.view_offset
2024-05-10 16:58:53 +04:00
} else if resize.edges.contains(ResizeEdge::LEFT) {
-offset
} else {
2024-06-17 09:16:28 +03:00
0.
};
2024-05-10 16:58:53 +04:00
self.view_offset += offset;
if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj {
2024-06-17 09:16:28 +03:00
anim.offset(offset);
} else {
// Don't bother with the gesture.
self.view_offset_adj = None;
}
2024-04-13 18:17:49 +04:00
}
if self.interactive_resize.is_none()
2024-05-10 16:58:53 +04:00
&& !matches!(self.view_offset_adj, Some(ViewOffsetAdjustment::Gesture(_)))
{
// We might need to move the view to ensure the resized window is still visible.
let current_x = self.view_pos();
2024-05-10 16:58:53 +04:00
// Upon unfullscreening, restore the view offset.
let is_fullscreen = self.columns[col_idx].tiles[tile_idx].is_fullscreen();
if was_fullscreen && !is_fullscreen {
if let Some(prev_offset) = self.view_offset_before_fullscreen.take() {
self.animate_view_offset(current_x, 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(current_x, col_idx, None, config);
}
2023-12-24 15:10:09 +04:00
}
}
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.;
}
let current_x = self.view_pos();
let new_view_offset = self.compute_new_view_offset_for_column(
current_x,
column_idx,
Some(self.active_column_idx),
);
// Consider the end of an ongoing animation because that's what compute to fit does too.
let final_x = if let Some(ViewOffsetAdjustment::Animation(anim)) = &self.view_offset_adj {
current_x - self.view_offset + anim.to()
} else {
current_x
};
let new_col_x = self.column_x(column_idx);
let from_view_offset = final_x - new_col_x;
(from_view_offset - new_view_offset).abs() / self.working_area.size.w
}
pub fn activate_window(&mut self, window: &W::Id) {
2023-12-24 15:10:09 +04:00
let column_idx = self
.columns
.iter()
.position(|col| col.contains(window))
.unwrap();
let column = &mut self.columns[column_idx];
column.activate_window(window);
self.activate_column(column_idx);
}
2024-05-01 19:00:11 +04:00
pub fn store_unmap_snapshot_if_empty(&mut self, renderer: &mut GlesRenderer, window: &W::Id) {
let output_scale = Scale::from(self.scale.fractional_scale());
let view_size = self.view_size();
2024-06-17 09:16:28 +03:00
for (tile, tile_pos) in self.tiles_with_render_positions_mut(false) {
if tile.window().id() == window {
let view_pos = Point::from((-tile_pos.x, -tile_pos.y));
let view_rect = Rectangle::from_loc_and_size(view_pos, view_size);
tile.update(false, view_rect);
tile.store_unmap_snapshot_if_empty(renderer, output_scale);
return;
2024-05-04 11:37:58 +04:00
}
}
2024-05-01 19:00:11 +04:00
}
pub fn clear_unmap_snapshot(&mut self, window: &W::Id) {
2024-05-04 11:37:58 +04:00
for col in &mut self.columns {
for tile in &mut col.tiles {
if tile.window().id() == window {
let _ = tile.take_unmap_snapshot();
return;
}
}
}
2024-05-01 19:00:11 +04:00
}
2024-04-09 22:37:10 +04:00
pub fn start_close_animation_for_window(
&mut self,
renderer: &mut GlesRenderer,
window: &W::Id,
2024-08-23 15:41:06 +03:00
blocker: TransactionBlocker,
2024-04-09 22:37:10 +04:00
) {
let (tile, mut tile_pos) = self
2024-06-17 09:16:28 +03:00
.tiles_with_render_positions_mut(false)
.find(|(tile, _)| tile.window().id() == window)
.unwrap();
2024-05-01 19:00:11 +04:00
let Some(snapshot) = tile.take_unmap_snapshot() else {
2024-04-09 22:37:10 +04:00
return;
};
let tile_size = tile.tile_size();
let (col_idx, tile_idx) = self
2024-04-09 22:37:10 +04:00
.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))
})
2024-04-09 22:37:10 +04:00
.unwrap();
let col = &self.columns[col_idx];
let removing_last = col.tiles.len() == 1;
2024-04-09 22:37:10 +04: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()
2024-06-17 09:16:28 +03:00
.filter_map(|(idx, data)| {
(idx != tile_idx).then_some(NotNan::new(data.size.w).unwrap())
})
.max()
2024-06-17 09:16:28 +03:00
.map(NotNan::into_inner)
.unwrap()
};
2024-04-09 22:37:10 +04:00
tile_pos.x -= offset;
}
2024-07-15 15:51:48 +02:00
self.start_close_animation_for_tile(renderer, snapshot, tile_size, tile_pos, blocker);
}
pub fn start_close_animation_for_tile(
&mut self,
renderer: &mut GlesRenderer,
snapshot: TileRenderSnapshot,
tile_size: Size<f64, Logical>,
tile_pos: Point<f64, Logical>,
blocker: TransactionBlocker,
) {
let output_scale = Scale::from(self.scale.fractional_scale());
let anim = Animation::new(0., 1., 0., self.options.animations.window_close.anim);
2024-04-09 22:37:10 +04:00
2024-08-23 15:41:06 +03:00
let blocker = if self.options.disable_transactions {
TransactionBlocker::completed()
} else {
blocker
};
let res = ClosingWindow::new(
renderer,
snapshot,
output_scale,
tile_size,
tile_pos,
blocker,
anim,
);
2024-04-09 22:37:10 +04:00
match res {
Ok(closing) => {
self.closing_windows.push(closing);
}
Err(err) => {
warn!("error creating a closing window animation: {err:?}");
}
}
}
2023-12-24 15:10:09 +04:00
#[cfg(test)]
2024-07-15 15:51:48 +02:00
pub fn verify_invariants(&self, move_win_id: Option<&W::Id>) {
2024-06-17 09:16:28 +03:00
use approx::assert_abs_diff_eq;
let scale = self.scale.fractional_scale();
assert!(self.view_size.w > 0.);
assert!(self.view_size.h > 0.);
assert!(scale > 0.);
assert!(scale.is_finite());
assert_eq!(self.columns.len(), self.data.len());
2023-12-24 15:10:09 +04: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));
2024-06-17 09:16:28 +03:00
assert_eq!(self.scale.fractional_scale(), column.scale);
2023-12-24 15:10:09 +04:00
column.verify_invariants();
let mut data2 = *data;
data2.update(column);
assert_eq!(data, &data2, "column data must be up to date");
2023-12-24 15:10:09 +04:00
}
2024-04-13 18:17:49 +04:00
2024-05-10 16:58:53 +04:00
let col = &self.columns[self.active_column_idx];
2024-04-13 18:17:49 +04:00
// 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()
})
);
}
2024-06-17 09:16:28 +03:00
2024-07-15 15:51:48 +02:00
for (tile, tile_pos) in self.tiles_with_render_positions() {
if Some(tile.window().id()) != move_win_id {
assert_eq!(tile.interactive_move_offset, Point::from((0., 0.)));
}
2024-06-17 09:16:28 +03:00
let rounded_pos = tile_pos.to_physical_precise_round(scale).to_logical(scale);
// Tile positions must be rounded to physical pixels.
assert_abs_diff_eq!(tile_pos.x, rounded_pos.x, epsilon = 1e-5);
assert_abs_diff_eq!(tile_pos.y, rounded_pos.y, epsilon = 1e-5);
}
2023-12-24 15:10:09 +04:00
}
2024-05-10 16:58:53 +04:00
if let Some(resize) = &self.interactive_resize {
assert!(
self.columns
.iter()
.flat_map(|col| &col.tiles)
.any(|tile| tile.window().id() == &resize.window),
"interactive resize window must be present on the workspace"
);
}
2023-12-24 15:10:09 +04:00
}
pub fn focus_left(&mut self) {
self.activate_column(self.active_column_idx.saturating_sub(1));
}
pub fn focus_right(&mut self) {
if self.columns.is_empty() {
return;
}
self.activate_column(min(self.active_column_idx + 1, self.columns.len() - 1));
}
2023-12-29 07:51:14 +04:00
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);
}
pub fn focus_column_right_or_first(&mut self) {
if self.columns.is_empty() {
return;
}
let column_idx = (self.active_column_idx + 1) % self.columns.len();
self.activate_column(column_idx);
}
pub fn focus_column_left_or_last(&mut self) {
if self.columns.is_empty() {
return;
}
let column_idx = if self.active_column_idx == 0 {
self.columns.len() - 1
} else {
self.active_column_idx - 1
};
self.activate_column(column_idx);
}
2023-12-24 15:10:09 +04:00
pub fn focus_down(&mut self) {
if self.columns.is_empty() {
return;
}
self.columns[self.active_column_idx].focus_down();
}
pub fn focus_up(&mut self) {
if self.columns.is_empty() {
return;
}
self.columns[self.active_column_idx].focus_up();
}
2023-12-29 08:00:40 +04:00
fn move_column_to(&mut self, new_idx: usize) {
2023-12-24 15:10:09 +04:00
if self.active_column_idx == new_idx {
return;
}
2024-04-08 18:23:18 +04:00
let current_col_x = self.column_x(self.active_column_idx);
let next_col_x = self.column_x(self.active_column_idx + 1);
2024-05-10 16:58:53 +04:00
let mut column = self.columns.remove(self.active_column_idx);
let data = self.data.remove(self.active_column_idx);
2024-05-14 16:29:03 +04:00
cancel_resize_for_column(&mut self.interactive_resize, &mut column);
self.columns.insert(new_idx, column);
self.data.insert(new_idx, data);
2023-12-24 15:10:09 +04:00
// 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 += view_offset_delta;
if let Some(ViewOffsetAdjustment::Animation(anim)) = &mut self.view_offset_adj {
2024-06-17 09:16:28 +03:00
anim.offset(view_offset_delta);
}
2023-12-24 15:10:09 +04:00
2024-04-08 18:23:18 +04:00
// 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);
2023-12-24 15:10:09 +04:00
}
2023-12-29 08:00:40 +04:00
pub fn move_left(&mut self) {
let new_idx = self.active_column_idx.saturating_sub(1);
self.move_column_to(new_idx);
}
2023-12-24 15:10:09 +04:00
pub fn move_right(&mut self) {
if self.columns.is_empty() {
return;
}
let new_idx = min(self.active_column_idx + 1, self.columns.len() - 1);
2023-12-29 08:00:40 +04:00
self.move_column_to(new_idx);
2023-12-24 15:10:09 +04:00
}
2023-12-29 08:01:02 +04:00
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);
}
2023-12-24 15:10:09 +04:00
pub fn move_down(&mut self) {
if self.columns.is_empty() {
return;
}
self.columns[self.active_column_idx].move_down();
}
pub fn move_up(&mut self) {
if self.columns.is_empty() {
return;
}
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)
};
2024-04-16 09:58:39 +04:00
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;
2024-04-16 09:58:39 +04:00
if source_column.tiles.len() == 1 {
if source_col_idx == 0 {
return;
}
// Move into adjacent column.
2024-04-16 09:58:39 +04:00
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.
self.activate_prev_column_on_removal = Some(self.static_view_offset() + offset.x);
}
offset.x += self.columns[source_col_idx].render_offset().x;
2024-10-14 18:08:44 +03:00
let RemovedTile { tile, .. } = self.remove_tile_by_idx(
source_col_idx,
0,
2024-08-23 15:41:06 +03:00
Transaction::new(),
Some(self.options.animations.window_movement.0),
);
self.add_tile_to_column(target_column_idx, None, tile, source_tile_was_active);
2024-04-16 09:58:39 +04:00
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);
2024-04-16 09:58:39 +04:00
let new_tile = target_column.tiles.last_mut().unwrap();
new_tile.animate_move_from(offset);
} else {
// Move out of column.
2024-06-17 09:16:28 +03:00
let mut offset = Point::from((source_column.render_offset().x, 0.));
2024-10-14 18:08:44 +03:00
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),
2024-10-14 18:08:44 +03:00
removed.tile,
source_tile_was_active,
2024-10-14 18:08:44 +03:00
removed.width,
removed.is_full_width,
2024-04-17 18:03:17 +04:00
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);
}
2024-04-16 09:58:39 +04:00
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);
2024-04-16 09:58:39 +04:00
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;
2024-04-16 09:58:39 +04:00
if source_column.tiles.len() == 1 {
if source_col_idx + 1 == self.columns.len() {
return;
}
// Move into adjacent column.
2024-04-16 09:58:39 +04:00
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;
}
2024-10-14 18:08:44 +03:00
let RemovedTile { tile, .. } = self.remove_tile_by_idx(
source_col_idx,
0,
2024-08-23 15:41:06 +03:00
Transaction::new(),
Some(self.options.animations.window_movement.0),
);
self.add_tile_to_column(target_column_idx, None, tile, source_tile_was_active);
2024-04-16 09:58:39 +04:00
let target_column = &mut self.columns[target_column_idx];
offset += prev_off - target_column.tile_offset(target_column.tiles.len() - 1);
2024-04-16 09:58:39 +04:00
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;
2024-10-14 18:08:44 +03:00
let removed =
self.remove_tile_by_idx(source_col_idx, source_tile_idx, Transaction::new(), None);
let target_column_idx = source_col_idx + 1;
2024-04-17 18:03:17 +04:00
self.add_tile(
Some(target_column_idx),
2024-10-14 18:08:44 +03:00
removed.tile,
source_tile_was_active,
2024-10-14 18:08:44 +03:00
removed.width,
removed.is_full_width,
2024-04-17 18:03:17 +04:00
Some(self.options.animations.window_movement.0),
);
2024-04-16 09:58:39 +04:00
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);
}
}
2023-12-24 15:10:09 +04:00
pub fn consume_into_column(&mut self) {
if self.columns.len() < 2 {
return;
}
if self.active_column_idx == self.columns.len() - 1 {
return;
}
2024-10-11 11:02:32 +03:00
let target_column_idx = self.active_column_idx;
2023-12-24 15:10:09 +04:00
let source_column_idx = self.active_column_idx + 1;
2024-04-16 09:58:39 +04:00
let offset = self.column_x(source_column_idx)
+ self.columns[source_column_idx].render_offset().x
2024-10-11 11:02:32 +03:00
- self.column_x(target_column_idx);
2024-06-17 09:16:28 +03:00
let mut offset = Point::from((offset, 0.));
let prev_off = self.columns[source_column_idx].tile_offset(0);
2024-04-16 09:58:39 +04:00
2024-10-14 18:08:44 +03:00
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);
2024-10-11 11:02:32 +03:00
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;
2024-04-16 09:58:39 +04:00
let new_tile = target_column.tiles.last_mut().unwrap();
new_tile.animate_move_from(offset);
2023-12-24 15:10:09 +04:00
}
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);
2024-04-16 09:58:39 +04:00
2023-12-24 18:00:46 +04:00
let source_column = &self.columns[self.active_column_idx];
if source_column.tiles.len() == 1 {
2023-12-24 15:10:09 +04:00
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);
2024-04-16 09:58:39 +04:00
let removed =
self.remove_tile_by_idx(source_col_idx, source_tile_idx, Transaction::new(), None);
2023-12-24 15:10:09 +04:00
2024-04-17 18:03:17 +04:00
self.add_tile(
Some(target_col_idx),
2024-10-14 18:08:44 +03:00
removed.tile,
false,
2024-10-14 18:08:44 +03:00
removed.width,
removed.is_full_width,
2024-04-17 18:03:17 +04:00
Some(self.options.animations.window_movement.0),
);
2024-04-16 09:58:39 +04:00
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);
2023-12-24 15:10:09 +04:00
}
pub fn center_column(&mut self) {
if self.columns.is_empty() {
return;
}
2024-01-08 17:17:19 +04:00
let center_x = self.view_pos();
self.animate_view_offset_to_column_centered(
center_x,
self.active_column_idx,
self.options.animations.horizontal_view_movement.0,
);
2024-05-10 16:58:53 +04:00
let col = &mut self.columns[self.active_column_idx];
cancel_resize_for_column(&mut self.interactive_resize, col);
2023-12-24 15:10:09 +04:00
}
2024-07-15 15:51:48 +02:00
pub fn view_pos(&self) -> f64 {
2023-12-24 15:10:09 +04:00
self.column_x(self.active_column_idx) + self.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.
2024-06-17 09:16:28 +03:00
fn static_view_offset(&self) -> f64 {
match &self.view_offset_adj {
// For animations we can return the final value.
2024-06-17 09:16:28 +03:00
Some(ViewOffsetAdjustment::Animation(anim)) => anim.to(),
Some(ViewOffsetAdjustment::Gesture(gesture)) => gesture.static_view_offset,
_ => self.view_offset,
}
}
// 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!
2024-06-17 09:16:28 +03:00
fn column_xs(&self, data: impl Iterator<Item = ColumnData>) -> impl Iterator<Item = f64> {
let gaps = self.options.gaps;
2024-06-17 09:16:28 +03:00
let mut x = 0.;
2024-04-08 19:48:52 +04:00
// Chain with a dummy value to be able to get one past all columns' X.
2024-06-17 09:16:28 +03:00
let dummy = ColumnData { width: 0. };
let data = data.chain(iter::once(dummy));
2024-04-08 19:48:52 +04:00
data.map(move |data| {
let rv = x;
x += data.width + gaps;
rv
})
}
2024-06-17 09:16:28 +03:00
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>,
2024-06-17 09:16:28 +03:00
) -> 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)
}
2024-04-08 19:48:52 +04:00
2024-06-17 09:16:28 +03:00
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)
}
2024-06-17 09:16:28 +03:00
fn columns_in_render_order(&self) -> impl Iterator<Item = (&Column<W>, f64)> + '_ {
let offsets = self.column_xs_in_render_order(self.data.iter().copied());
2024-04-08 19:48:52 +04:00
let (first, rest) = self.columns.split_at(self.active_column_idx);
let (active, rest) = rest.split_at(1);
let tiles = active.iter().chain(first).chain(rest);
zip(tiles, offsets)
2023-12-24 15:10:09 +04:00
}
2024-06-17 09:16:28 +03:00
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, rest) = self.columns.split_at_mut(self.active_column_idx);
let (active, rest) = rest.split_at_mut(1);
let tiles = active.iter_mut().chain(first).chain(rest);
zip(tiles, offsets)
}
2024-07-15 15:51:48 +02:00
pub fn tiles_with_render_positions(
&self,
) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> {
2024-06-17 09:16:28 +03:00
let scale = self.scale.fractional_scale();
let view_off = Point::from((-self.view_pos(), 0.));
self.columns_in_render_order()
.flat_map(move |(col, col_x)| {
2024-06-17 09:16:28 +03:00
let col_off = Point::from((col_x, 0.));
let col_render_off = col.render_offset();
col.tiles_in_render_order().map(move |(tile, tile_off)| {
let pos = view_off + col_off + col_render_off + tile_off + tile.render_offset();
2024-06-17 09:16:28 +03:00
// Round to physical pixels.
let pos = pos.to_physical_precise_round(scale).to_logical(scale);
(tile, pos)
})
})
}
2024-07-15 15:51:48 +02:00
pub fn tiles_with_render_positions_mut(
&mut self,
2024-06-17 09:16:28 +03:00
round: bool,
) -> impl Iterator<Item = (&mut Tile<W>, Point<f64, Logical>)> {
let scale = self.scale.fractional_scale();
let view_off = Point::from((-self.view_pos(), 0.));
self.columns_in_render_order_mut()
.flat_map(move |(col, col_x)| {
2024-06-17 09:16:28 +03:00
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)| {
2024-06-17 09:16:28 +03:00
let mut pos =
view_off + col_off + col_render_off + tile_off + tile.render_offset();
2024-06-17 09:16:28 +03:00
// Round to physical pixels.
if round {
pos = pos.to_physical_precise_round(scale).to_logical(scale);
}
(tile, pos)
})
})
2024-02-26 18:47:46 +01:00
}
2024-07-15 15:51:48 +02:00
fn insert_hint_area(&self, insert_hint: &InsertHint) -> Option<Rectangle<f64, Logical>> {
let mut hint_area = match insert_hint.position {
InsertPosition::NewColumn(column_index) => {
if column_index == 0 || column_index == self.columns.len() {
let size =
Size::from((300., self.working_area.size.h - self.options.gaps * 2.));
let mut loc = Point::from((
self.column_x(column_index),
self.working_area.loc.y + self.options.gaps,
));
if column_index == 0 && !self.columns.is_empty() {
loc.x -= size.w + self.options.gaps;
}
Rectangle::from_loc_and_size(loc, size)
} else if column_index > self.columns.len() {
error!("insert hint column index is out of range");
return None;
} else {
let size =
Size::from((300., self.working_area.size.h - self.options.gaps * 2.));
let loc = Point::from((
self.column_x(column_index) - size.w / 2. - self.options.gaps / 2.,
self.working_area.loc.y + self.options.gaps,
));
Rectangle::from_loc_and_size(loc, size)
}
}
InsertPosition::InColumn(column_index, tile_index) => {
if column_index > self.columns.len() {
error!("insert hint column index is out of range");
return None;
}
if tile_index > self.columns[column_index].tiles.len() {
error!("insert hint tile index is out of range");
return None;
}
let (height, y) = if tile_index == 0 {
(150., self.columns[column_index].tile_offset(tile_index).y)
} else if tile_index == self.columns[column_index].tiles.len() {
(
150.,
self.columns[column_index].tile_offset(tile_index).y
- self.options.gaps
- 150.,
)
} else {
(
300.,
self.columns[column_index].tile_offset(tile_index).y
- self.options.gaps / 2.
- 150.,
)
};
let size = Size::from((self.data[column_index].width, height));
let loc = Point::from((self.column_x(column_index), y));
Rectangle::from_loc_and_size(loc, size)
}
};
// First window on an empty workspace will cancel out any view offset. Replicate this
// effect here.
if self.columns.is_empty() {
let view_offset = if self.is_centering_focused_column() {
self.compute_new_view_offset_centered(0., 0., hint_area.size.w, false)
} else {
self.compute_new_view_offset_fit(0., 0., hint_area.size.w, false)
};
hint_area.loc.x -= view_offset;
} else {
hint_area.loc.x -= self.view_pos();
}
let view_size = self.view_size();
// Make sure the hint is at least partially visible.
if matches!(insert_hint.position, InsertPosition::NewColumn(_)) {
hint_area.loc.x = hint_area.loc.x.max(-hint_area.size.w / 2.);
hint_area.loc.x = hint_area.loc.x.min(view_size.w - hint_area.size.w / 2.);
}
// Round to physical pixels.
hint_area = hint_area
.to_physical_precise_round(self.scale.fractional_scale())
.to_logical(self.scale.fractional_scale());
Some(hint_area)
}
2024-02-26 18:47:46 +01:00
/// Returns the geometry of the active tile relative to and clamped to the view.
///
/// During animations, assumes the final view position.
2024-06-17 09:16:28 +03:00
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
2024-02-26 18:47:46 +01:00
.view_offset_adj
.as_ref()
2024-06-17 09:16:28 +03:00
.map_or(self.view_offset, |adj| adj.target_view_offset());
let view_off = Point::from((-final_view_offset, 0.));
2024-02-26 18:47:46 +01:00
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();
2024-02-26 18:47:46 +01:00
let tile_rect = Rectangle::from_loc_and_size(tile_pos, tile_size);
2024-06-17 09:16:28 +03:00
let view = Rectangle::from_loc_and_size((0., 0.), self.view_size);
2024-02-26 18:47:46 +01:00
view.intersection(tile_rect)
}
pub fn window_under(
&self,
pos: Point<f64, Logical>,
2024-06-17 09:16:28 +03:00
) -> Option<(&W, Option<Point<f64, Logical>>)> {
if self.columns.is_empty() {
return None;
}
self.tiles_with_render_positions()
.find_map(|(tile, tile_pos)| {
2024-06-17 09:16:28 +03:00
let pos_within_tile = pos - tile_pos;
if tile.is_in_input_region(pos_within_tile) {
let pos_within_surface = tile_pos + tile.buf_loc();
return Some((tile.window(), Some(pos_within_surface)));
} else if tile.is_in_activation_region(pos_within_tile) {
return Some((tile.window(), None));
}
None
})
}
2024-05-10 16:58:53 +04:00
pub fn resize_edges_under(&self, pos: Point<f64, Logical>) -> Option<ResizeEdge> {
if self.columns.is_empty() {
return None;
}
self.tiles_with_render_positions()
.find_map(|(tile, tile_pos)| {
2024-06-17 09:16:28 +03:00
let pos_within_tile = pos - tile_pos;
2024-05-10 16:58:53 +04:00
// This logic should be consistent with window_under() in when it returns Some vs.
// None.
if tile.is_in_input_region(pos_within_tile)
|| tile.is_in_activation_region(pos_within_tile)
{
let size = tile.tile_size().to_f64();
2024-05-10 16:58:53 +04:00
let mut edges = ResizeEdge::empty();
if pos_within_tile.x < size.w / 3. {
edges |= ResizeEdge::LEFT;
} else if 2. * size.w / 3. < pos_within_tile.x {
edges |= ResizeEdge::RIGHT;
}
if pos_within_tile.y < size.h / 3. {
edges |= ResizeEdge::TOP;
} else if 2. * size.h / 3. < pos_within_tile.y {
edges |= ResizeEdge::BOTTOM;
}
return Some(edges);
2024-05-10 16:58:53 +04:00
}
None
})
2024-05-10 16:58:53 +04:00
}
2023-12-24 15:10:09 +04:00
pub fn toggle_width(&mut self) {
if self.columns.is_empty() {
return;
}
2024-05-10 16:58:53 +04:00
let col = &mut self.columns[self.active_column_idx];
col.toggle_width();
2024-05-14 16:29:03 +04:00
cancel_resize_for_column(&mut self.interactive_resize, col);
2023-12-24 15:10:09 +04:00
}
pub fn toggle_full_width(&mut self) {
if self.columns.is_empty() {
return;
}
2024-05-10 16:58:53 +04:00
let col = &mut self.columns[self.active_column_idx];
col.toggle_full_width();
2024-05-14 16:29:03 +04:00
cancel_resize_for_column(&mut self.interactive_resize, col);
2023-12-24 15:10:09 +04:00
}
pub fn set_column_width(&mut self, change: SizeChange) {
if self.columns.is_empty() {
return;
}
2024-05-10 16:58:53 +04:00
let col = &mut self.columns[self.active_column_idx];
col.set_column_width(change, None, true);
2024-05-14 16:29:03 +04:00
cancel_resize_for_column(&mut self.interactive_resize, col);
2023-12-24 15:10:09 +04:00
}
pub fn set_window_height(&mut self, window: Option<&W::Id>, change: SizeChange) {
2023-12-24 15:10:09 +04:00
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);
2024-05-10 16:58:53 +04:00
2024-05-14 16:29:03 +04:00
cancel_resize_for_column(&mut self.interactive_resize, col);
2023-12-24 15:10:09 +04:00
}
pub fn reset_window_height(&mut self, window: Option<&W::Id>) {
2024-05-11 09:33:23 +04:00
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);
2024-05-11 09:33:23 +04:00
2024-05-14 16:29:03 +04:00
cancel_resize_for_column(&mut self.interactive_resize, col);
2024-05-11 09:33:23 +04:00
}
2024-09-12 11:53:10 +03:00
pub fn toggle_window_height(&mut self, window: Option<&W::Id>) {
2024-09-05 23:37:10 +02:00
if self.columns.is_empty() {
return;
}
2024-09-12 11:53:10 +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.toggle_window_height(tile_idx, true);
2024-09-05 23:37:10 +02:00
cancel_resize_for_column(&mut self.interactive_resize, col);
}
pub fn set_fullscreen(&mut self, window: &W::Id, is_fullscreen: bool) {
let (mut col_idx, tile_idx) = self
2023-12-24 15:10:09 +04:00
.columns
.iter()
.enumerate()
.find_map(|(col_idx, col)| col.position(window).map(|tile_idx| (col_idx, tile_idx)))
2023-12-24 15:10:09 +04:00
.unwrap();
2024-06-04 19:44:33 +03:00
if is_fullscreen == self.columns[col_idx].is_fullscreen {
return;
}
2024-04-13 18:17:49 +04:00
if is_fullscreen
&& col_idx == self.active_column_idx
&& self.columns[col_idx].tiles.len() == 1
{
self.view_offset_before_fullscreen = Some(self.static_view_offset());
}
2023-12-24 15:10:09 +04:00
let mut col = &mut self.columns[col_idx];
2024-05-14 16:29:03 +04:00
cancel_resize_for_column(&mut self.interactive_resize, col);
2024-05-10 16:58:53 +04:00
if is_fullscreen && col.tiles.len() > 1 {
2023-12-24 15:10:09 +04: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;
2023-12-24 15:10:09 +04:00
2024-10-14 18:08:44 +03:00
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(
2024-10-14 18:08:44 +03:00
removed.tile,
self.view_size,
self.working_area,
2024-06-17 09:16:28 +03:00
self.scale.fractional_scale(),
self.options.clone(),
2024-10-14 18:08:44 +03:00
removed.width,
removed.is_full_width,
false,
2023-12-24 15:10:09 +04:00
);
self.add_column(Some(col_idx + 1), column, activate, None);
2024-04-13 18:17:49 +04:00
col_idx += 1;
2023-12-24 15:10:09 +04:00
col = &mut self.columns[col_idx];
}
col.set_fullscreen(is_fullscreen);
2024-04-13 18:17:49 +04: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;
}
2023-12-24 15:10:09 +04:00
}
pub fn toggle_fullscreen(&mut self, window: &W::Id) {
2023-12-24 15:10:09 +04:00
let col = self
.columns
.iter_mut()
2023-12-25 11:06:03 +04:00
.find(|col| col.contains(window))
2023-12-24 15:10:09 +04:00
.unwrap();
let value = !col.is_fullscreen;
self.set_fullscreen(window, value);
}
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;
}
2024-02-29 08:56:20 +04:00
if self.view_offset_adj.is_some() {
2023-12-24 15:10:09 +04:00
return false;
}
self.columns[self.active_column_idx].is_fullscreen
}
pub fn render_elements<R: NiriRenderer>(
2023-12-24 15:10:09 +04:00
&self,
2024-01-01 15:36:37 +04:00
renderer: &mut R,
target: RenderTarget,
) -> Vec<WorkspaceRenderElement<R>> {
let output_scale = Scale::from(self.scale.fractional_scale());
2023-12-24 15:10:09 +04:00
let mut rv = vec![];
2024-07-15 15:51:48 +02:00
// Draw the insert hint.
if let Some(insert_hint) = &self.insert_hint {
if let Some(area) = self.insert_hint_area(insert_hint) {
2024-11-02 09:33:44 +03:00
rv.extend(
self.insert_hint_element
.render(renderer, area.loc)
.map(WorkspaceRenderElement::InsertHint),
2024-07-15 15:51:48 +02:00
);
}
}
// Draw the closing windows on top of the other windows.
2024-06-17 09:16:28 +03:00
let view_rect = Rectangle::from_loc_and_size((self.view_pos(), 0.), self.view_size);
2024-05-14 14:52:13 +04:00
for closing in self.closing_windows.iter().rev() {
let elem = closing.render(renderer.as_gles_renderer(), view_rect, output_scale, target);
2024-05-12 08:46:02 +04:00
rv.push(elem.into());
2024-04-09 22:37:10 +04:00
}
if self.columns.is_empty() {
return rv;
}
let mut first = true;
for (tile, tile_pos) in self.tiles_with_render_positions() {
2024-02-06 19:47:36 +04:00
// For the active tile (which comes first), draw the focus ring.
let focus_ring = first;
first = false;
2024-02-04 22:52:11 +04:00
rv.extend(
tile.render(renderer, tile_pos, output_scale, focus_ring, target)
.map(Into::into),
2024-02-04 22:52:11 +04:00
);
}
2023-12-24 15:10:09 +04:00
rv
}
2024-02-29 08:56:20 +04:00
2024-05-11 14:01:48 +04:00
pub fn view_offset_gesture_begin(&mut self, is_touchpad: bool) {
2024-02-29 08:56:20 +04:00
if self.columns.is_empty() {
return;
}
if self.interactive_resize.is_some() {
return;
}
2024-02-29 08:56:20 +04:00
let gesture = ViewGesture {
2024-06-17 09:16:28 +03:00
current_view_offset: self.view_offset,
tracker: SwipeTracker::new(),
2024-06-17 09:16:28 +03:00
delta_from_tracker: self.view_offset,
static_view_offset: self.static_view_offset(),
2024-05-11 14:01:48 +04:00
is_touchpad,
2024-02-29 08:56:20 +04:00
};
self.view_offset_adj = Some(ViewOffsetAdjustment::Gesture(gesture));
}
pub fn view_offset_gesture_update(
&mut self,
delta_x: f64,
timestamp: Duration,
2024-05-11 14:01:48 +04:00
is_touchpad: bool,
) -> Option<bool> {
2024-02-29 08:56:20 +04:00
let Some(ViewOffsetAdjustment::Gesture(gesture)) = &mut self.view_offset_adj else {
return None;
};
2024-05-11 14:01:48 +04:00
if gesture.is_touchpad != is_touchpad {
return None;
}
gesture.tracker.push(delta_x, timestamp);
2024-05-11 14:01:48 +04:00
let norm_factor = if gesture.is_touchpad {
2024-06-17 09:16:28 +03:00
self.working_area.size.w / VIEW_GESTURE_WORKING_AREA_MOVEMENT
2024-05-11 14:01:48 +04:00
} else {
1.
};
let pos = gesture.tracker.pos() * norm_factor;
let view_offset = pos + gesture.delta_from_tracker;
gesture.current_view_offset = view_offset;
2024-02-29 08:56:20 +04:00
Some(true)
}
2024-05-11 14:01:48 +04:00
pub fn view_offset_gesture_end(&mut self, _cancelled: bool, is_touchpad: Option<bool>) -> bool {
let Some(ViewOffsetAdjustment::Gesture(gesture)) = &self.view_offset_adj else {
return false;
};
2024-05-11 14:01:48 +04:00
if is_touchpad.map_or(false, |x| gesture.is_touchpad != x) {
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.
2024-05-11 14:01:48 +04:00
let norm_factor = if gesture.is_touchpad {
2024-06-17 09:16:28 +03:00
self.working_area.size.w / VIEW_GESTURE_WORKING_AREA_MOVEMENT
2024-05-11 14:01:48 +04:00
} else {
1.
};
2024-03-05 13:32:30 +04:00
let velocity = gesture.tracker.velocity() * norm_factor;
let pos = gesture.tracker.pos() * norm_factor;
let current_view_offset = pos + gesture.delta_from_tracker;
2024-02-29 08:56:20 +04:00
if self.columns.is_empty() {
2024-06-17 09:16:28 +03:00
self.view_offset = current_view_offset;
self.view_offset_adj = None;
return true;
2024-02-29 08:56:20 +04:00
}
// 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).
2024-06-17 09:16:28 +03:00
view_pos: f64,
// Column to activate for this snapping point.
col_idx: usize,
}
let mut snapping_points = Vec::new();
2024-02-29 08:56:20 +04:00
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() {
2024-06-17 09:16:28 +03:00
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 {
2024-06-17 09:16:28 +03:00
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;
2024-02-29 08:56:20 +04:00
}
} else {
let view_width = self.view_size.w;
2024-09-04 21:45:47 +03:00
let working_area_width = self.working_area.size.w;
let gaps = self.options.gaps;
2024-09-04 21:45:47 +03:00
let snap_points = |col_x, col: &Column<W>| {
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;
2024-09-04 21:45:47 +03:00
(left, right)
} else {
// Logic from compute_new_view_offset.
2024-09-04 21:45:47 +03:00
let padding = ((working_area_width - col_w) / 2.).clamp(0., gaps);
let left = col_x - padding - left_strut;
let right = col_x + col_w + padding + right_strut;
2024-09-04 21:45:47 +03:00
(left, right)
}
2024-09-04 21:45:47 +03:00
};
2024-02-29 08:56:20 +04: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).
let leftmost_snap = snap_points(0., &self.columns[0]).0;
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 rightmost_snap =
snap_points(last_col_x, &self.columns[last_col_idx]).1 - view_width;
snapping_points.push(Snap {
view_pos: leftmost_snap,
col_idx: 0,
});
snapping_points.push(Snap {
view_pos: rightmost_snap,
col_idx: last_col_idx,
});
2024-09-04 21:45:47 +03:00
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,
});
}
2024-09-04 21:45:47 +03:00
};
let mut col_x = 0.;
for (col_idx, col) in self.columns.iter().enumerate() {
let (left, right) = snap_points(col_x, col);
push(col_idx, left, right);
col_x += col.width() + gaps;
}
2024-02-29 08:56:20 +04:00
}
// Find the closest snapping point.
2024-06-17 09:16:28 +03:00
snapping_points.sort_by_key(|snap| NotNan::new(snap.view_pos).unwrap());
let active_col_x = self.column_x(self.active_column_idx);
2024-06-17 09:16:28 +03:00
let target_view_pos = active_col_x + target_view_offset;
let target_snap = snapping_points
.iter()
2024-06-17 09:16:28 +03:00
.min_by_key(|snap| NotNan::new((snap.view_pos - target_view_pos).abs()).unwrap())
.unwrap();
2024-02-29 08:56:20 +04:00
let mut new_col_idx = target_snap.col_idx;
2024-02-29 08:56:20 +04:00
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 =
2024-06-17 09:16:28 +03:00
((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 =
2024-06-17 09:16:28 +03:00
((self.working_area.size.w - col_w) / 2.).clamp(0., self.options.gaps);
if col_x - padding < target_snap.view_pos + left_strut {
break;
}
}
2024-02-29 08:56:20 +04:00
new_col_idx = col_idx;
}
}
2024-02-29 08:56:20 +04:00
}
let new_col_x = self.column_x(new_col_idx);
2024-06-17 09:16:28 +03:00
let delta = active_col_x - new_col_x;
self.view_offset = current_view_offset + delta;
2024-04-13 18:17:49 +04:00
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_adj = Some(ViewOffsetAdjustment::Animation(Animation::new(
current_view_offset + delta,
2024-06-17 09:16:28 +03:00
target_view_offset,
2024-03-05 13:32:30 +04:00
velocity,
2024-04-17 14:06:32 +04:00
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(self.view_pos(), new_col_idx, None);
2024-02-29 08:56:20 +04:00
true
}
2023-12-24 15:10:09 +04:00
2024-05-10 16:58:53 +04:00
pub fn interactive_resize_begin(&mut self, window: W::Id, edges: ResizeEdge) -> bool {
2024-10-20 09:13:00 +03:00
if self.interactive_resize.is_some() {
return false;
}
2024-05-10 16:58:53 +04:00
let col = self
.columns
.iter_mut()
.find(|col| col.contains(&window))
.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 },
2024-05-10 16:58:53 +04:00
};
self.interactive_resize = Some(resize);
// Stop ongoing animation.
self.view_offset_adj = None;
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();
2024-05-10 16:58:53 +04:00
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 {
2024-05-10 16:58:53 +04:00
dx *= 2.;
}
2024-06-17 09:16:28 +03:00
let window_width = (resize.original_window_size.w + dx).round() as i32;
2024-05-10 16:58:53 +04:00
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.
2024-06-17 09:16:28 +03:00
let window_height = (resize.original_window_size.h + dy).round() as i32;
2024-05-10 16:58:53 +04:00
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(self.view_pos(), self.active_column_idx, None);
}
2024-05-10 16:58:53 +04:00
}
self.interactive_resize = None;
}
2024-03-23 14:38:07 +04:00
pub fn refresh(&mut self, is_active: bool) {
for (col_idx, col) in self.columns.iter_mut().enumerate() {
2024-05-10 16:58:53 +04:00
let mut col_resize_data = None;
if let Some(resize) = &self.interactive_resize {
if col.contains(&resize.window) {
col_resize_data = Some(resize.data);
}
}
2024-08-22 14:44:11 +03:00
let intent = if self.options.disable_resize_throttling {
ConfigureIntent::CanSend
} else if self.options.disable_transactions {
// When transactions are disabled, we don't use combined throttling, but rather
// compute throttling individually below.
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,
}
})
};
2024-03-23 14:38:07 +04:00
for (tile_idx, tile) in col.tiles.iter_mut().enumerate() {
let win = tile.window_mut();
2024-04-22 22:51:52 +02:00
let active_in_column = col.active_tile_idx == tile_idx;
win.set_active_in_column(active_in_column);
let active = is_active && self.active_column_idx == col_idx && active_in_column;
2024-02-04 21:23:00 +04:00
win.set_activated(active);
2024-05-10 16:58:53 +04:00
win.set_interactive_resize(col_resize_data);
2024-04-24 21:49:07 +04:00
let border_config = win.rules().border.resolve_against(self.options.border);
let bounds = compute_toplevel_bounds(
border_config,
self.working_area.size,
self.options.gaps,
);
2024-03-19 13:52:08 +04:00
win.set_bounds(bounds);
2024-04-24 21:49:07 +04:00
2024-08-22 14:44:11 +03:00
// If transactions are disabled, also disable combined throttling, for more
// intuitive behavior.
let intent = if self.options.disable_transactions {
win.configure_intent()
} else {
intent
};
2024-08-22 14:36:47 +03:00
if matches!(
intent,
ConfigureIntent::CanSend | ConfigureIntent::ShouldSend
) {
win.send_pending_configure();
}
2024-02-04 21:23:00 +04:00
win.refresh();
}
}
}
}
2023-12-24 15:10:09 +04:00
impl<W: LayoutElement> Column<W> {
2024-06-17 09:16:28 +03:00
#[allow(clippy::too_many_arguments)]
fn new_with_tile(
tile: Tile<W>,
2024-06-17 09:16:28 +03:00
view_size: Size<f64, Logical>,
working_area: Rectangle<f64, Logical>,
scale: f64,
options: Rc<Options>,
width: ColumnWidth,
is_full_width: bool,
animate_resize: bool,
2023-12-24 15:10:09 +04:00
) -> Self {
let mut rv = Self {
tiles: vec![],
data: vec![],
active_tile_idx: 0,
2023-12-24 15:10:09 +04:00
width,
is_full_width,
is_fullscreen: false,
2024-04-08 18:23:18 +04:00
move_animation: None,
2023-12-24 15:10:09 +04:00
view_size,
working_area,
2024-06-17 09:16:28 +03:00
scale,
2023-12-24 15:10:09 +04:00
options,
};
let is_pending_fullscreen = tile.window().is_pending_fullscreen();
2024-02-03 09:45:26 +04:00
2024-10-11 11:02:32 +03:00
rv.add_tile_at(0, tile, animate_resize);
2023-12-24 15:10:09 +04:00
2024-02-03 09:45:26 +04:00
if is_pending_fullscreen {
rv.set_fullscreen(true);
}
2023-12-24 15:10:09 +04:00
rv
}
2024-06-17 09:16:28 +03:00
fn set_view_size(&mut self, size: Size<f64, Logical>, working_area: Rectangle<f64, Logical>) {
2023-12-24 15:10:09 +04:00
if self.view_size == size && self.working_area == working_area {
return;
}
self.view_size = size;
self.working_area = working_area;
2024-04-13 11:07:23 +04:00
self.update_tile_sizes(false);
2023-12-24 15:10:09 +04:00
}
2024-06-17 09:16:28 +03:00
fn update_config(&mut self, scale: f64, options: Rc<Options>) {
2023-12-24 15:10:09 +04:00
let mut update_sizes = false;
// If preset widths changed, make our width non-preset.
2024-09-05 23:37:10 +02:00
if self.options.preset_column_widths != options.preset_column_widths {
2023-12-24 15:10:09 +04:00
if let ColumnWidth::Preset(idx) = self.width {
2024-09-05 23:37:10 +02:00
self.width = self.options.preset_column_widths[idx];
2023-12-24 15:10:09 +04:00
}
}
2024-09-05 23:37:10 +02: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;
}
2023-12-24 15:10:09 +04:00
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;
}
for (tile, data) in zip(&mut self.tiles, &mut self.data) {
2024-06-17 09:16:28 +03:00
tile.update_config(scale, options.clone());
data.update(tile);
}
2024-06-17 09:16:28 +03:00
self.scale = scale;
2023-12-24 15:10:09 +04:00
self.options = options;
if update_sizes {
2024-04-13 11:07:23 +04:00
self.update_tile_sizes(false);
2023-12-24 15:10:09 +04:00
}
}
2024-05-10 16:58:53 +04:00
fn set_width(&mut self, width: ColumnWidth, animate: bool) {
2023-12-24 15:10:09 +04:00
self.width = width;
self.is_full_width = false;
2024-05-10 16:58:53 +04:00
self.update_tile_sizes(animate);
2023-12-24 15:10:09 +04:00
}
pub fn advance_animations(&mut self, current_time: Duration) {
if let Some(anim) = &mut self.move_animation {
anim.set_current_time(current_time);
if anim.is_done() {
self.move_animation = None;
2024-04-08 18:23:18 +04:00
}
}
for tile in &mut self.tiles {
tile.advance_animations(current_time);
2024-02-06 19:52:47 +04:00
}
}
2024-02-07 11:32:02 +04:00
pub fn are_animations_ongoing(&self) -> bool {
2024-04-08 18:23:18 +04:00
self.move_animation.is_some() || self.tiles.iter().any(Tile::are_animations_ongoing)
}
2024-06-17 09:16:28 +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();
tile.update(is_active, tile_view_rect);
}
}
2024-06-17 09:16:28 +03:00
pub fn render_offset(&self) -> Point<f64, Logical> {
2024-04-08 18:23:18 +04:00
let mut offset = Point::from((0., 0.));
if let Some(anim) = &self.move_animation {
offset.x += anim.value();
}
2024-06-17 09:16:28 +03:00
offset
2024-04-08 18:23:18 +04:00
}
2024-06-17 09:16:28 +03:00
pub fn animate_move_from(&mut self, from_x_offset: f64) {
2024-04-13 11:07:23 +04:00
self.animate_move_from_with_config(
from_x_offset,
2024-04-17 14:06:32 +04:00
self.options.animations.window_movement.0,
2024-04-13 11:07:23 +04:00
);
}
pub fn animate_move_from_with_config(
&mut self,
2024-06-17 09:16:28 +03:00
from_x_offset: f64,
2024-04-13 11:07:23 +04:00
config: niri_config::Animation,
) {
2024-04-08 18:23:18 +04:00
let current_offset = self.move_animation.as_ref().map_or(0., Animation::value);
self.move_animation = Some(Animation::new(
2024-06-17 09:16:28 +03:00
from_x_offset + current_offset,
2024-04-08 18:23:18 +04:00
0.,
0.,
2024-04-13 11:07:23 +04:00
config,
2024-04-08 18:23:18 +04:00
));
2024-02-07 11:32:02 +04:00
}
pub fn contains(&self, window: &W::Id) -> bool {
self.tiles
.iter()
.map(Tile::window)
.any(|win| win.id() == window)
2023-12-24 15:10:09 +04:00
}
pub fn position(&self, window: &W::Id) -> Option<usize> {
self.tiles
.iter()
.map(Tile::window)
.position(|win| win.id() == window)
2023-12-26 10:29:00 +04:00
}
fn activate_window(&mut self, window: &W::Id) {
2023-12-26 10:29:00 +04:00
let idx = self.position(window).unwrap();
self.active_tile_idx = idx;
2023-12-24 15:10:09 +04:00
}
fn add_tile_at(&mut self, idx: usize, mut tile: Tile<W>, animate: bool) {
tile.update_config(self.scale, self.options.clone());
2024-10-11 11:02:32 +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()));
2023-12-24 15:10:09 +04:00
self.is_fullscreen = false;
2024-10-11 11:02:32 +03:00
self.data
.insert(idx, TileData::new(&tile, WindowHeight::auto_1()));
self.tiles.insert(idx, tile);
self.update_tile_sizes(animate);
2024-10-11 11:02:32 +03:00
// 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);
}
2023-12-24 15:10:09 +04:00
}
fn update_window(&mut self, window: &W::Id) {
2024-04-13 11:07:23 +04:00
let (tile_idx, tile) = self
.tiles
.iter_mut()
2024-04-13 11:07:23 +04:00
.enumerate()
.find(|(_, tile)| tile.window().id() == window)
.unwrap();
2024-04-13 11:07:23 +04:00
2024-06-17 09:16:28 +03:00
let height = f64::from(tile.window().size().h);
2024-04-13 11:07:23 +04:00
let offset = tile
.window()
.animation_snapshot()
2024-06-17 09:16:28 +03:00
.map_or(0., |from| from.size.h - height);
2024-04-13 11:07:23 +04:00
tile.update_window();
self.data[tile_idx].update(tile);
2024-04-13 11:07:23 +04:00
// Move windows below in tandem with resizing.
2024-10-11 11:02:32 +03:00
//
// 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.
2024-06-17 09:16:28 +03:00
if tile.resize_animation().is_some() && offset != 0. {
2024-04-13 11:07:23 +04:00
for tile in &mut self.tiles[tile_idx + 1..] {
2024-04-19 12:44:24 +04:00
tile.animate_move_y_from_with_config(
offset,
2024-04-21 20:10:35 +04:00
self.options.animations.window_resize.anim,
2024-04-13 11:07:23 +04:00
);
}
}
}
2024-04-13 11:07:23 +04:00
fn update_tile_sizes(&mut self, animate: bool) {
2024-08-23 15:41:06 +03:00
self.update_tile_sizes_with_transaction(animate, Transaction::new());
}
fn update_tile_sizes_with_transaction(&mut self, animate: bool, transaction: Transaction) {
2023-12-24 15:10:09 +04:00
if self.is_fullscreen {
self.tiles[0].request_fullscreen(self.view_size);
2023-12-24 15:10:09 +04:00
return;
}
2024-08-25 10:16:37 +03:00
let min_size: Vec<_> = self
.tiles
.iter()
.map(Tile::min_size)
.map(|mut size| {
size.w = size.w.max(1.);
size.h = size.h.max(1.);
size
})
.collect();
let max_size: Vec<_> = self.tiles.iter().map(Tile::max_size).collect();
2023-12-24 15:10:09 +04:00
// Compute the column width.
let min_width = min_size
.iter()
2024-08-25 10:16:37 +03:00
.map(|size| NotNan::new(size.w).unwrap())
2023-12-24 15:10:09 +04:00
.max()
2024-06-17 09:16:28 +03:00
.map(NotNan::into_inner)
2024-08-25 10:16:37 +03:00
.unwrap();
2023-12-24 15:10:09 +04:00
let max_width = max_size
.iter()
.filter_map(|size| {
let w = size.w;
2024-06-17 09:16:28 +03:00
if w == 0. {
2023-12-24 15:10:09 +04:00
None
} else {
2024-06-17 09:16:28 +03:00
Some(NotNan::new(w).unwrap())
2023-12-24 15:10:09 +04:00
}
})
.min()
2024-06-17 09:16:28 +03:00
.map(NotNan::into_inner)
.unwrap_or(f64::from(i32::MAX));
let max_width = f64::max(max_width, min_width);
2023-12-24 15:10:09 +04:00
let width = if self.is_full_width {
ColumnWidth::Proportion(1.)
} else {
self.width
};
let width = width.resolve(&self.options, self.working_area.size.w);
2024-06-17 09:16:28 +03:00
let width = f64::max(f64::min(width, max_width), min_width);
2024-09-05 23:37:10 +02:00
let height = self.working_area.size.h;
2023-12-24 15:10:09 +04: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;
if self.tiles.len() > 1 {
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];
let height_left = self.working_area.size.h
- self.options.gaps
- min_height_taken
- self.options.gaps;
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);
}
WindowHeight::Fixed(tile.tile_height_for_window_height(window_height))
}
2024-09-05 23:37:10 +02:00
WindowHeight::Preset(idx) => {
let preset = self.options.preset_window_heights[idx];
let window_height = match resolve_preset_size(preset, &self.options, height) {
ResolvedSize::Tile(h) => tile.window_height_for_tile_height(h),
ResolvedSize::Window(h) => h,
};
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);
2024-09-05 23:37:10 +02:00
WindowHeight::Fixed(tile_height)
}
})
.collect::<Vec<_>>();
2024-09-05 23:37:10 +02:00
let gaps_left = self.options.gaps * (self.tiles.len() + 1) as f64;
let mut height_left = self.working_area.size.h - gaps_left;
let mut auto_tiles_left = self.tiles.len();
2023-12-24 15:10:09 +04:00
// Subtract all fixed-height tiles.
2023-12-24 15:10:09 +04:00
for (h, (min_size, max_size)) in zip(&mut heights, zip(&min_size, &max_size)) {
// Check if the tile has an exact height constraint.
2024-08-25 10:16:37 +03:00
if min_size.h == max_size.h {
2023-12-24 15:10:09 +04:00
*h = WindowHeight::Fixed(min_size.h);
}
if let WindowHeight::Fixed(h) = h {
2024-06-17 09:16:28 +03:00
if max_size.h > 0. {
*h = f64::min(*h, max_size.h);
2023-12-24 15:10:09 +04:00
}
2024-08-25 10:16:37 +03:00
*h = f64::max(*h, min_size.h);
2023-12-24 15:10:09 +04:00
height_left -= *h;
auto_tiles_left -= 1;
2023-12-24 15:10:09 +04:00
}
}
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.
2023-12-24 15:10:09 +04:00
// 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
2023-12-24 15:10:09 +04:00
// 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.
2023-12-24 15:10:09 +04:00
// 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.
2024-08-25 11:46:04 +03:00
'outer: while auto_tiles_left > 0 {
2024-06-19 08:46:01 +03:00
// 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;
2024-06-19 08:46:01 +03:00
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,
2024-09-05 23:37:10 +02:00
WindowHeight::Preset(_) => unreachable!(),
};
let factor = weight / total_weight_2;
2023-12-24 15:10:09 +04:00
2024-06-19 08:46:01 +03:00
// Compute the current auto height.
2024-08-25 10:16:37 +03:00
let mut auto = height_left_2 * factor;
2023-12-24 15:10:09 +04:00
// Check if the auto height satisfies the min height.
2024-08-25 10:16:37 +03:00
if min_size.h > auto {
2024-06-19 08:46:01 +03:00
auto = min_size.h;
*h = WindowHeight::Fixed(auto);
height_left -= auto;
total_weight -= weight;
auto_tiles_left -= 1;
2024-08-25 11:46:04 +03:00
// 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;
2023-12-24 15:10:09 +04:00
}
2024-06-19 08:46:01 +03:00
2024-08-25 11:46:04 +03:00
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;
2023-12-24 15:10:09 +04:00
}
// All min heights were satisfied, fill them in.
2024-06-19 08:46:01 +03:00
for (h, tile) in zip(&mut heights, &self.tiles) {
let weight = match *h {
WindowHeight::Auto { weight } => weight,
WindowHeight::Fixed(_) => continue,
2024-09-05 23:37:10 +02:00
WindowHeight::Preset(_) => unreachable!(),
};
let factor = weight / total_weight;
2023-12-24 15:10:09 +04:00
2024-06-19 08:46:01 +03:00
// Compute the current auto height.
let auto = height_left * factor;
2024-06-19 08:46:01 +03:00
let auto = tile.tile_height_for_window_height(
tile.window_height_for_tile_height(auto).round().max(1.),
);
2023-12-24 15:10:09 +04:00
*h = WindowHeight::Fixed(auto);
height_left -= auto;
total_weight -= weight;
auto_tiles_left -= 1;
2023-12-24 15:10:09 +04:00
}
assert_eq!(auto_tiles_left, 0);
2023-12-24 15:10:09 +04:00
}
for (tile, h) in zip(&mut self.tiles, heights) {
2023-12-24 15:10:09 +04:00
let WindowHeight::Fixed(height) = h else {
unreachable!()
};
let size = Size::from((width, height));
2024-08-22 14:44:11 +03:00
tile.request_tile_size(size, animate, Some(transaction.clone()));
2023-12-24 15:10:09 +04:00
}
}
2024-06-17 09:16:28 +03:00
fn width(&self) -> f64 {
self.data
.iter()
.map(|data| NotNan::new(data.size.w).unwrap())
.max()
.map(NotNan::into_inner)
.unwrap()
2023-12-24 15:10:09 +04:00
}
fn focus_up(&mut self) {
self.active_tile_idx = self.active_tile_idx.saturating_sub(1);
2023-12-24 15:10:09 +04:00
}
fn focus_down(&mut self) {
self.active_tile_idx = min(self.active_tile_idx + 1, self.tiles.len() - 1);
2023-12-24 15:10:09 +04:00
}
fn move_up(&mut self) {
let new_idx = self.active_tile_idx.saturating_sub(1);
if self.active_tile_idx == new_idx {
2023-12-24 15:10:09 +04:00
return;
}
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;
2024-04-15 21:19:09 +04:00
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;
2024-04-15 21:19:09 +04:00
// Animate the movement.
let new_active_y = self.tile_offset(new_idx).y;
2024-04-19 12:44:24 +04:00
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);
2023-12-24 15:10:09 +04:00
}
fn move_down(&mut self) {
let new_idx = min(self.active_tile_idx + 1, self.tiles.len() - 1);
if self.active_tile_idx == new_idx {
2023-12-24 15:10:09 +04:00
return;
}
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;
2024-04-15 21:19:09 +04:00
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;
2024-04-15 21:19:09 +04:00
// Animate the movement.
let new_active_y = self.tile_offset(new_idx).y;
2024-04-19 12:44:24 +04:00
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);
2023-12-24 15:10:09 +04:00
}
#[cfg(test)]
fn verify_invariants(&self) {
2024-06-17 09:16:28 +03:00
use approx::assert_abs_diff_eq;
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());
2023-12-24 15:10:09 +04:00
if self.is_fullscreen {
assert_eq!(self.tiles.len(), 1);
2023-12-24 15:10:09 +04: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"
);
}
}
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));
2024-06-17 09:16:28 +03:00
assert_eq!(self.scale, tile.scale());
assert_eq!(self.is_fullscreen, tile.window().is_pending_fullscreen());
let mut data2 = *data;
data2.update(tile);
assert_eq!(data, &data2, "tile data must be up to date");
2024-06-17 09:16:28 +03:00
let scale = tile.scale();
let size = tile.tile_size();
let rounded = size.to_physical_precise_round(scale).to_logical(scale);
assert_abs_diff_eq!(size.w, rounded.w, epsilon = 1e-5);
assert_abs_diff_eq!(size.h, rounded.h, epsilon = 1e-5);
if matches!(data.height, WindowHeight::Fixed(_)) {
assert!(
!found_fixed,
"there can only be one fixed-height window in a column"
);
found_fixed = true;
}
2024-09-05 23:37:10 +02:00
if let WindowHeight::Preset(idx) = data.height {
assert!(self.options.preset_window_heights.len() > idx);
}
let requested_size = tile.window().requested_size().unwrap();
total_height += tile.tile_height_for_window_height(f64::from(requested_size.h));
total_min_height += f64::max(1., tile.min_size().h);
}
if tile_count > 1
&& self.scale.round() == self.scale
&& self.working_area.size.h.round() == self.working_area.size.h
&& self.options.gaps.round() == self.options.gaps
{
total_height += self.options.gaps * (tile_count + 1) as f64;
total_min_height += self.options.gaps * (tile_count + 1) as f64;
let max_height = f64::max(total_min_height, self.working_area.size.h);
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})"
);
}
2023-12-24 15:10:09 +04:00
}
fn toggle_width(&mut self) {
let width = if self.is_full_width {
ColumnWidth::Proportion(1.)
} else {
self.width
};
let idx = match width {
2024-09-05 23:37:10 +02:00
ColumnWidth::Preset(idx) => (idx + 1) % self.options.preset_column_widths.len(),
2023-12-24 15:10:09 +04:00
_ => {
let current = self.width();
self.options
2024-09-05 23:37:10 +02:00
.preset_column_widths
2023-12-24 15:10:09 +04:00
.iter()
.position(|prop| {
2024-06-17 09:16:28 +03:00
let resolved = prop.resolve(&self.options, self.working_area.size.w);
// Some allowance for fractional scaling purposes.
current + 1. < resolved
2023-12-24 15:10:09 +04:00
})
.unwrap_or(0)
}
};
let width = ColumnWidth::Preset(idx);
2024-05-10 16:58:53 +04:00
self.set_width(width, true);
2023-12-24 15:10:09 +04:00
}
fn toggle_full_width(&mut self) {
self.is_full_width = !self.is_full_width;
2024-04-13 11:07:23 +04:00
self.update_tile_sizes(true);
2023-12-24 15:10:09 +04:00
}
2024-05-10 16:58:53 +04:00
fn set_column_width(&mut self, change: SizeChange, tile_idx: Option<usize>, animate: bool) {
2023-12-24 15:10:09 +04:00
let width = if self.is_full_width {
ColumnWidth::Proportion(1.)
} else {
self.width
};
let current_px = width.resolve(&self.options, self.working_area.size.w);
let current = match width {
2024-09-05 23:37:10 +02:00
ColumnWidth::Preset(idx) => self.options.preset_column_widths[idx],
2023-12-24 15:10:09 +04:00
current => current,
};
// FIXME: fix overflows then remove limits.
2024-06-17 09:16:28 +03:00
const MAX_PX: f64 = 100000.;
2023-12-24 15:10:09 +04:00
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
2024-05-10 16:58:53 +04:00
// 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];
2024-06-17 09:16:28 +03:00
ColumnWidth::Fixed(
tile.tile_width_for_window_width(f64::from(fixed))
.clamp(1., MAX_PX),
)
}
2023-12-24 15:10:09 +04:00
(_, SizeChange::SetProportion(proportion)) => {
ColumnWidth::Proportion((proportion / 100.).clamp(0., MAX_F))
}
(_, SizeChange::AdjustFixed(delta)) => {
2024-06-17 09:16:28 +03:00
let width = (current_px + f64::from(delta)).clamp(1., MAX_PX);
2023-12-24 15:10:09 +04:00
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)) => {
2024-06-17 09:16:28 +03:00
let full = self.working_area.size.w - self.options.gaps;
let current = if full == 0. {
1.
} else {
(current_px + self.options.gaps) / full
};
2023-12-24 15:10:09 +04:00
let proportion = (current + delta / 100.).clamp(0., MAX_F);
ColumnWidth::Proportion(proportion)
}
(ColumnWidth::Preset(_), _) => unreachable!(),
};
2024-05-10 16:58:53 +04:00
self.set_width(width, animate);
2023-12-24 15:10:09 +04:00
}
2024-05-10 16:58:53 +04: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
2024-09-05 23:37:10 +02:00
// 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;
2024-05-10 16:58:53 +04:00
let tile = &self.tiles[tile_idx];
let current_window_px = match current {
2024-09-05 23:37:10 +02:00
WindowHeight::Auto { .. } | WindowHeight::Preset(_) => tile.window_size().h,
2023-12-24 15:10:09 +04:00
WindowHeight::Fixed(height) => height,
};
let current_tile_px = tile.tile_height_for_window_height(current_window_px);
2024-06-17 09:16:28 +03:00
let full = self.working_area.size.h - self.options.gaps;
let current_prop = if full == 0. {
1.
} else {
(current_tile_px + self.options.gaps) / (full)
};
2023-12-24 15:10:09 +04:00
// FIXME: fix overflows then remove limits.
2024-06-17 09:16:28 +03:00
const MAX_PX: f64 = 100000.;
2023-12-24 15:10:09 +04:00
let mut window_height = match change {
2024-06-17 09:16:28 +03:00
SizeChange::SetFixed(fixed) => f64::from(fixed),
2023-12-24 15:10:09 +04:00
SizeChange::SetProportion(proportion) => {
2024-09-06 18:25:54 +03:00
let tile_height = (self.working_area.size.h - self.options.gaps)
* (proportion / 100.)
- self.options.gaps;
tile.window_height_for_tile_height(tile_height)
2023-12-24 15:10:09 +04:00
}
2024-06-17 09:16:28 +03:00
SizeChange::AdjustFixed(delta) => current_window_px + f64::from(delta),
2023-12-24 15:10:09 +04:00
SizeChange::AdjustProportion(delta) => {
let proportion = current_prop + delta / 100.;
2024-06-17 09:16:28 +03:00
let tile_height =
(self.working_area.size.h - self.options.gaps) * proportion - self.options.gaps;
tile.window_height_for_tile_height(tile_height)
2023-12-24 15:10:09 +04:00
}
};
// If there are multiple windows in a column, clamp the height according to other windows'
// min sizes.
let min_height_taken = self
.tiles
.iter()
.enumerate()
.filter(|(idx, _)| *idx != tile_idx)
.map(|(_, tile)| f64::max(1., tile.min_size().h) + self.options.gaps)
.sum::<f64>();
if min_height_taken > 0. {
let height_left =
self.working_area.size.h - self.options.gaps - min_height_taken - self.options.gaps;
let height_left = f64::max(1., tile.window_height_for_tile_height(height_left));
window_height = f64::min(height_left, window_height);
}
2023-12-24 15:10:09 +04:00
// Clamp it against the window height constraints.
2024-05-10 16:58:53 +04:00
let win = &self.tiles[tile_idx].window();
2023-12-24 15:10:09 +04:00
let min_h = win.min_size().h;
let max_h = win.max_size().h;
if max_h > 0 {
2024-06-17 09:16:28 +03:00
window_height = f64::min(window_height, f64::from(max_h));
2023-12-24 15:10:09 +04:00
}
if min_h > 0 {
2024-06-17 09:16:28 +03:00
window_height = f64::max(window_height, f64::from(min_h));
2023-12-24 15:10:09 +04:00
}
2024-06-17 09:16:28 +03:00
self.data[tile_idx].height = WindowHeight::Fixed(window_height.clamp(1., MAX_PX));
2024-05-10 16:58:53 +04:00
self.update_tile_sizes(animate);
2023-12-24 15:10:09 +04:00
}
2024-05-11 09:33:23 +04:00
fn reset_window_height(&mut self, tile_idx: Option<usize>, animate: bool) {
let tile_idx = tile_idx.unwrap_or(self.active_tile_idx);
self.data[tile_idx].height = WindowHeight::auto_1();
2024-05-11 09:33:23 +04:00
self.update_tile_sizes(animate);
}
2024-09-05 23:37:10 +02:00
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| {
let resolved =
resolve_preset_size(preset, &self.options, self.working_area.size.h);
let window_height = match resolved {
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 };
}
}
2023-12-24 15:10:09 +04:00
fn set_fullscreen(&mut self, is_fullscreen: bool) {
2024-06-04 19:44:33 +03:00
if self.is_fullscreen == is_fullscreen {
return;
}
assert_eq!(self.tiles.len(), 1);
2023-12-24 15:10:09 +04:00
self.is_fullscreen = is_fullscreen;
2024-04-13 11:07:23 +04:00
self.update_tile_sizes(false);
}
/// Returns the static window location, not taking the render offset into account.
2024-06-17 09:16:28 +03:00
pub fn window_loc(&self, tile_idx: usize) -> Point<f64, Logical> {
let (tile, pos) = self.tiles().nth(tile_idx).unwrap();
pos + tile.window_loc()
2023-12-24 15:10:09 +04: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>,
2024-06-17 09:16:28 +03:00
) -> 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;
2024-10-11 11:00:50 +03:00
let col_width = if self.tiles.is_empty() {
0.
} else {
self.width()
};
2024-06-17 09:16:28 +03:00
let mut y = 0.;
2023-12-24 15:10:09 +04:00
if !self.is_fullscreen {
y = self.working_area.loc.y + self.options.gaps;
}
// 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| {
2024-06-17 09:16:28 +03:00
let mut pos = Point::from((0., y));
2024-04-15 21:19:09 +04:00
if center {
2024-06-17 09:16:28 +03:00
pos.x = (col_width - data.size.w) / 2.;
} else if data.interactively_resizing_by_left_edge {
pos.x = col_width - data.size.w;
}
y += data.size.h + gaps;
2023-12-24 15:10:09 +04:00
pos
})
}
2024-02-26 18:47:46 +01:00
2024-06-17 09:16:28 +03:00
fn tile_offsets(&self) -> impl Iterator<Item = Point<f64, Logical>> + '_ {
self.tile_offsets_iter(self.data.iter().copied())
}
2024-06-17 09:16:28 +03:00
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>,
2024-06-17 09:16:28 +03:00
) -> 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)
}
2024-06-17 09:16:28 +03:00
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)
}
2024-06-17 09:16:28 +03:00
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)
}
2024-06-17 09:16:28 +03:00
fn tiles_in_render_order(&self) -> impl Iterator<Item = (&Tile<W>, Point<f64, Logical>)> + '_ {
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);
let tiles = active.iter().chain(first).chain(rest);
zip(tiles, offsets)
}
fn tiles_in_render_order_mut(
&mut self,
2024-06-17 09:16:28 +03:00
) -> 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)
2024-02-26 18:47:46 +01:00
}
2023-12-24 15:10:09 +04:00
}
fn compute_new_view_offset(
2024-06-17 09:16:28 +03:00
cur_x: f64,
view_width: f64,
new_col_x: f64,
new_col_width: f64,
gaps: f64,
) -> f64 {
2023-12-24 15:10:09 +04:00
// If the column is wider than the view, always left-align it.
if view_width <= new_col_width {
2024-06-17 09:16:28 +03:00
return 0.;
2023-12-24 15:10:09 +04:00
}
// Compute the padding in case it needs to be smaller due to large tile width.
2024-06-17 09:16:28 +03:00
let padding = ((view_width - new_col_width) / 2.).clamp(0., gaps);
2023-12-24 15:10:09 +04:00
// 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);
}
2024-06-09 10:50:22 +00:00
// Otherwise, prefer the alignment that results in less motion from the current position.
2024-06-17 09:16:28 +03:00
let dist_to_left = (cur_x - new_x).abs();
let dist_to_right = ((cur_x + view_width) - new_right_x).abs();
2023-12-24 15:10:09 +04:00
if dist_to_left <= dist_to_right {
-padding
} else {
-(view_width - padding - new_col_width)
}
}
2024-06-17 09:16:28 +03:00
pub fn compute_working_area(output: &Output, struts: Struts) -> Rectangle<f64, Logical> {
2023-12-24 15:10:09 +04:00
// Start with the layer-shell non-exclusive zone.
2024-06-17 09:16:28 +03:00
let mut working_area = layer_map_for_output(output).non_exclusive_zone().to_f64();
2023-12-24 15:10:09 +04:00
// Add struts.
2024-06-17 09:16:28 +03:00
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 scale = output.current_scale().fractional_scale();
let loc = working_area
.loc
.to_physical_precise_ceil(scale)
.to_logical(scale);
2023-12-24 15:10:09 +04:00
2024-06-17 09:16:28 +03:00
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);
2023-12-24 15:10:09 +04:00
2024-06-17 09:16:28 +03:00
working_area.size -= size_diff;
working_area.loc = loc;
2023-12-24 15:10:09 +04:00
working_area
}
2024-04-24 21:49:07 +04:00
fn compute_toplevel_bounds(
border_config: niri_config::Border,
2024-06-17 09:16:28 +03:00
working_area_size: Size<f64, Logical>,
gaps: f64,
2024-04-24 21:49:07 +04:00
) -> Size<i32, Logical> {
2024-06-17 09:16:28 +03:00
let mut border = 0.;
2024-04-24 21:49:07 +04:00
if !border_config.off {
2024-06-17 09:16:28 +03:00
border = border_config.width.0 * 2.;
2024-04-24 21:49:07 +04:00
}
Size::from((
2024-06-17 09:16:28 +03:00
f64::max(working_area_size.w - gaps * 2. - border, 1.),
f64::max(working_area_size.h - gaps * 2. - border, 1.),
2024-04-24 21:49:07 +04:00
))
2024-06-17 09:16:28 +03:00
.to_i32_floor()
2024-04-24 21:49:07 +04:00
}
2024-05-10 16:58:53 +04:00
2024-05-14 16:29:03 +04:00
fn cancel_resize_for_column<W: LayoutElement>(
2024-05-10 16:58:53 +04:00
interactive_resize: &mut Option<InteractiveResize<W>>,
column: &mut Column<W>,
) {
if let Some(resize) = interactive_resize {
if column.contains(&resize.window) {
*interactive_resize = None;
}
}
2024-05-14 16:29:03 +04:00
for tile in &mut column.tiles {
tile.window_mut().cancel_interactive_resize();
}
2024-05-10 16:58:53 +04:00
}