diff options
| author | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-07 11:32:02 +0400 |
|---|---|---|
| committer | Ivan Molodetskikh <yalterz@gmail.com> | 2024-02-07 13:16:54 +0400 |
| commit | f9085db5648bc6bad7fb0abf45e2a11f2e03d1af (patch) | |
| tree | 56b32abe55f9857649f0296a0e36c30cd20c38e5 | |
| parent | 49ce791d13031ba5396b2ee8dbbffe128d64ff0f (diff) | |
| download | niri-f9085db5648bc6bad7fb0abf45e2a11f2e03d1af.tar.gz niri-f9085db5648bc6bad7fb0abf45e2a11f2e03d1af.tar.bz2 niri-f9085db5648bc6bad7fb0abf45e2a11f2e03d1af.zip | |
Implement window open animations
| -rw-r--r-- | niri-visual-tests/src/cases/layout.rs | 228 | ||||
| -rw-r--r-- | niri-visual-tests/src/cases/mod.rs | 1 | ||||
| -rw-r--r-- | niri-visual-tests/src/cases/tile.rs | 25 | ||||
| -rw-r--r-- | niri-visual-tests/src/main.rs | 22 | ||||
| -rw-r--r-- | niri-visual-tests/src/test_window.rs | 4 | ||||
| -rw-r--r-- | src/animation.rs | 24 | ||||
| -rw-r--r-- | src/handlers/compositor.rs | 1 | ||||
| -rw-r--r-- | src/layout/mod.rs | 44 | ||||
| -rw-r--r-- | src/layout/tile.rs | 94 | ||||
| -rw-r--r-- | src/layout/workspace.rs | 30 |
10 files changed, 462 insertions, 11 deletions
diff --git a/niri-visual-tests/src/cases/layout.rs b/niri-visual-tests/src/cases/layout.rs new file mode 100644 index 00000000..b12b6745 --- /dev/null +++ b/niri-visual-tests/src/cases/layout.rs @@ -0,0 +1,228 @@ +use std::collections::HashMap; +use std::time::Duration; + +use niri::layout::workspace::ColumnWidth; +use niri::layout::Options; +use niri::utils::get_monotonic_time; +use niri_config::Color; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::desktop::layer_map_for_output; +use smithay::output::{Mode, Output, PhysicalProperties, Subpixel}; +use smithay::utils::{Logical, Physical, Size}; + +use super::TestCase; +use crate::test_window::TestWindow; + +type DynStepFn = Box<dyn FnOnce(&mut Layout)>; + +pub struct Layout { + output: Output, + windows: Vec<TestWindow>, + layout: niri::layout::Layout<TestWindow>, + start_time: Duration, + steps: HashMap<Duration, DynStepFn>, +} + +impl Layout { + pub fn new(size: Size<i32, Logical>) -> Self { + let output = Output::new( + String::new(), + PhysicalProperties { + size: Size::from((size.w, size.h)), + subpixel: Subpixel::Unknown, + make: String::new(), + model: String::new(), + }, + ); + let mode = Some(Mode { + size: size.to_physical(1), + refresh: 60000, + }); + output.change_current_state(mode, None, None, None); + + let options = Options { + focus_ring: niri_config::FocusRing { + off: true, + ..Default::default() + }, + border: niri_config::FocusRing { + off: false, + width: 4, + active_color: Color::new(255, 163, 72, 255), + inactive_color: Color::new(50, 50, 50, 255), + }, + ..Default::default() + }; + let mut layout = niri::layout::Layout::with_options(options); + layout.add_output(output.clone()); + + Self { + output, + windows: Vec::new(), + layout, + start_time: get_monotonic_time(), + steps: HashMap::new(), + } + } + + pub fn open_in_between(size: Size<i32, Logical>) -> Self { + let mut rv = Self::new(size); + + rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3))); + rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3))); + rv.layout.activate_window(&rv.windows[0]); + + rv.add_step(500, |l| { + let win = TestWindow::freeform(2); + l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3))); + l.layout.start_open_animation_for_window(&win); + }); + + rv + } + + pub fn open_multiple_quickly(size: Size<i32, Logical>) -> Self { + let mut rv = Self::new(size); + + for delay in [100, 200, 300] { + rv.add_step(delay, move |l| { + let win = TestWindow::freeform(delay as usize); + l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.3))); + l.layout.start_open_animation_for_window(&win); + }); + } + + rv + } + + pub fn open_multiple_quickly_big(size: Size<i32, Logical>) -> Self { + let mut rv = Self::new(size); + + for delay in [100, 200, 300] { + rv.add_step(delay, move |l| { + let win = TestWindow::freeform(delay as usize); + l.add_window(win.clone(), Some(ColumnWidth::Proportion(0.5))); + l.layout.start_open_animation_for_window(&win); + }); + } + + rv + } + + pub fn open_to_the_left(size: Size<i32, Logical>) -> Self { + let mut rv = Self::new(size); + + rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3))); + rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.3))); + + rv.add_step(500, |l| { + let win = TestWindow::freeform(2); + let right_of = l.windows[0].clone(); + l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.3))); + l.layout.start_open_animation_for_window(&win); + }); + + rv + } + + pub fn open_to_the_left_big(size: Size<i32, Logical>) -> Self { + let mut rv = Self::new(size); + + rv.add_window(TestWindow::freeform(0), Some(ColumnWidth::Proportion(0.3))); + rv.add_window(TestWindow::freeform(1), Some(ColumnWidth::Proportion(0.8))); + + rv.add_step(500, |l| { + let win = TestWindow::freeform(2); + let right_of = l.windows[0].clone(); + l.add_window_right_of(&right_of, win.clone(), Some(ColumnWidth::Proportion(0.5))); + l.layout.start_open_animation_for_window(&win); + }); + + rv + } + + fn add_window(&mut self, window: TestWindow, width: Option<ColumnWidth>) { + self.layout.add_window(window.clone(), width, false); + if window.communicate() { + self.layout.update_window(&window); + } + self.windows.push(window); + } + + fn add_window_right_of( + &mut self, + right_of: &TestWindow, + window: TestWindow, + width: Option<ColumnWidth>, + ) { + self.layout + .add_window_right_of(right_of, window.clone(), width, false); + if window.communicate() { + self.layout.update_window(&window); + } + self.windows.push(window); + } + + fn add_step(&mut self, delay_ms: u64, f: impl FnOnce(&mut Self) + 'static) { + self.steps + .insert(Duration::from_millis(delay_ms), Box::new(f) as _); + } +} + +impl TestCase for Layout { + fn resize(&mut self, width: i32, height: i32) { + let mode = Some(Mode { + size: Size::from((width, height)), + refresh: 60000, + }); + self.output.change_current_state(mode, None, None, None); + layer_map_for_output(&self.output).arrange(); + self.layout.update_output_size(&self.output); + for win in &self.windows { + if win.communicate() { + self.layout.update_window(win); + } + } + } + + fn are_animations_ongoing(&self) -> bool { + self.layout + .monitor_for_output(&self.output) + .unwrap() + .are_animations_ongoing() + || !self.steps.is_empty() + } + + fn advance_animations(&mut self, mut current_time: Duration) { + let run = self + .steps + .keys() + .copied() + .filter(|delay| self.start_time + *delay <= current_time) + .collect::<Vec<_>>(); + for key in &run { + let f = self.steps.remove(key).unwrap(); + f(self); + } + if !run.is_empty() { + current_time = get_monotonic_time(); + } + + self.layout.advance_animations(current_time); + } + + fn render( + &mut self, + renderer: &mut GlesRenderer, + _size: Size<i32, Physical>, + ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> { + self.layout + .monitor_for_output(&self.output) + .unwrap() + .render_elements(renderer) + .into_iter() + .map(|elem| Box::new(elem) as _) + .collect() + } +} diff --git a/niri-visual-tests/src/cases/mod.rs b/niri-visual-tests/src/cases/mod.rs index b7d71cd0..6f732f6f 100644 --- a/niri-visual-tests/src/cases/mod.rs +++ b/niri-visual-tests/src/cases/mod.rs @@ -4,6 +4,7 @@ use smithay::backend::renderer::element::RenderElement; use smithay::backend::renderer::gles::GlesRenderer; use smithay::utils::{Physical, Size}; +pub mod layout; pub mod tile; pub mod window; diff --git a/niri-visual-tests/src/cases/tile.rs b/niri-visual-tests/src/cases/tile.rs index 824661bb..e3fa8fb1 100644 --- a/niri-visual-tests/src/cases/tile.rs +++ b/niri-visual-tests/src/cases/tile.rs @@ -41,6 +41,27 @@ impl Tile { rv } + pub fn freeform_open(size: Size<i32, Logical>) -> Self { + let mut rv = Self::freeform(size); + rv.window.set_color([0.1, 0.1, 0.1, 1.]); + rv.tile.start_open_animation(); + rv + } + + pub fn fixed_size_open(size: Size<i32, Logical>) -> Self { + let mut rv = Self::fixed_size(size); + rv.window.set_color([0.1, 0.1, 0.1, 1.]); + rv.tile.start_open_animation(); + rv + } + + pub fn fixed_size_with_csd_shadow_open(size: Size<i32, Logical>) -> Self { + let mut rv = Self::fixed_size_with_csd_shadow(size); + rv.window.set_color([0.1, 0.1, 0.1, 1.]); + rv.tile.start_open_animation(); + rv + } + pub fn with_window(window: TestWindow) -> Self { let options = Options { focus_ring: niri_config::FocusRing { @@ -66,6 +87,10 @@ impl TestCase for Tile { self.window.communicate(); } + fn are_animations_ongoing(&self) -> bool { + self.tile.are_animations_ongoing() + } + fn advance_animations(&mut self, current_time: Duration) { self.tile.advance_animations(current_time, true); } diff --git a/niri-visual-tests/src/main.rs b/niri-visual-tests/src/main.rs index ad208b19..32911b55 100644 --- a/niri-visual-tests/src/main.rs +++ b/niri-visual-tests/src/main.rs @@ -16,6 +16,7 @@ use smithay::utils::{Logical, Size}; use smithay_view::SmithayView; use tracing_subscriber::EnvFilter; +use crate::cases::layout::Layout; use crate::cases::TestCase; mod cases; @@ -85,6 +86,27 @@ fn build_ui(app: &adw::Application) { Tile::fixed_size_with_csd_shadow, "Fixed Size Tile - CSD Shadow", ); + s.add(Tile::freeform_open, "Freeform Tile - Open"); + s.add(Tile::fixed_size_open, "Fixed Size Tile - Open"); + s.add( + Tile::fixed_size_with_csd_shadow_open, + "Fixed Size Tile - CSD Shadow - Open", + ); + + s.add(Layout::open_in_between, "Layout - Open In-Between"); + s.add( + Layout::open_multiple_quickly, + "Layout - Open Multiple Quickly", + ); + s.add( + Layout::open_multiple_quickly_big, + "Layout - Open Multiple Quickly - Big", + ); + s.add(Layout::open_to_the_left, "Layout - Open To The Left"); + s.add( + Layout::open_to_the_left_big, + "Layout - Open To The Left - Big", + ); let content_headerbar = adw::HeaderBar::new(); diff --git a/niri-visual-tests/src/test_window.rs b/niri-visual-tests/src/test_window.rs index 291a391d..7e50023a 100644 --- a/niri-visual-tests/src/test_window.rs +++ b/niri-visual-tests/src/test_window.rs @@ -5,7 +5,7 @@ use std::rc::Rc; use niri::layout::{LayoutElement, LayoutElementRenderElement}; use niri::render_helpers::renderer::NiriRenderer; use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; -use smithay::backend::renderer::element::Kind; +use smithay::backend::renderer::element::{Id, Kind}; use smithay::output::Output; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Logical, Point, Scale, Size, Transform}; @@ -196,6 +196,8 @@ impl LayoutElement for TestWindow { fn output_leave(&self, _output: &Output) {} + fn set_offscreen_element_id(&self, _id: Option<Id>) {} + fn is_fullscreen(&self) -> bool { false } diff --git a/src/animation.rs b/src/animation.rs index addfdfca..bf0536e2 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -15,6 +15,13 @@ pub struct Animation { duration: Duration, start_time: Duration, current_time: Duration, + curve: Curve, +} + +#[derive(Debug, Clone, Copy)] +pub enum Curve { + EaseOutCubic, + EaseOutExpo, } impl Animation { @@ -29,9 +36,15 @@ impl Animation { duration: over.mul_f64(ANIMATION_SLOWDOWN.load(Ordering::Relaxed)), start_time: now, current_time: now, + curve: Curve::EaseOutCubic, } } + pub fn with_curve(mut self, curve: Curve) -> Self { + self.curve = curve; + self + } + pub fn set_current_time(&mut self, time: Duration) { self.current_time = time; } @@ -44,7 +57,7 @@ impl Animation { let passed = (self.current_time - self.start_time).as_secs_f64(); let total = self.duration.as_secs_f64(); let x = (passed / total).clamp(0., 1.); - EaseOutCubic.y(x) * (self.to - self.from) + self.from + self.curve.y(x) * (self.to - self.from) + self.from } pub fn to(&self) -> f64 { @@ -56,3 +69,12 @@ impl Animation { self.from } } + +impl Curve { + pub fn y(self, x: f64) -> f64 { + match self { + Curve::EaseOutCubic => EaseOutCubic.y(x), + Curve::EaseOutExpo => 1. - 2f64.powf(-10. * x), + } + } +} diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index 79ad5d12..5522e48d 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -120,6 +120,7 @@ impl CompositorHandler for State { }; if let Some(output) = output.cloned() { + self.niri.layout.start_open_animation_for_window(&window); self.niri.queue_redraw(output); } return; diff --git a/src/layout/mod.rs b/src/layout/mod.rs index b9e298cf..36f60d12 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -37,7 +37,7 @@ use std::time::Duration; use niri_config::{self, CenterFocusedColumn, Config, SizeChange, Struts}; use smithay::backend::renderer::element::solid::SolidColorRenderElement; use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; -use smithay::backend::renderer::element::AsRenderElements; +use smithay::backend::renderer::element::{AsRenderElements, Id}; use smithay::desktop::space::SpaceElement; use smithay::desktop::Window; use smithay::output::Output; @@ -52,6 +52,7 @@ pub use self::monitor::MonitorRenderElement; use self::monitor::{Monitor, WorkspaceSwitch, WorkspaceSwitchGesture}; use self::workspace::{compute_working_area, Column, ColumnWidth, OutputId, Workspace}; use crate::animation::Animation; +use crate::niri::WindowOffscreenId; use crate::niri_render_elements; use crate::render_helpers::renderer::NiriRenderer; use crate::utils::output_size; @@ -105,6 +106,7 @@ pub trait LayoutElement: PartialEq { fn set_preferred_scale_transform(&self, scale: i32, transform: Transform); fn output_enter(&self, output: &Output); fn output_leave(&self, output: &Output); + fn set_offscreen_element_id(&self, id: Option<Id>); /// Whether the element is currently fullscreen. /// @@ -292,6 +294,11 @@ impl LayoutElement for Window { SpaceElement::output_leave(self, output) } + fn set_offscreen_element_id(&self, id: Option<Id>) { + let data = self.user_data().get_or_insert(WindowOffscreenId::default); + data.0.replace(id); + } + fn is_fullscreen(&self) -> bool { self.toplevel() .current_state() @@ -1593,6 +1600,39 @@ impl<W: LayoutElement> Layout<W> { }; monitor.move_workspace_up(); } + + pub fn start_open_animation_for_window(&mut self, window: &W) { + match &mut self.monitor_set { + MonitorSet::Normal { monitors, .. } => { + for mon in monitors { + for ws in &mut mon.workspaces { + for col in &mut ws.columns { + for tile in &mut col.tiles { + if tile.window() == window { + tile.start_open_animation(); + return; + } + } + } + } + } + } + MonitorSet::NoOutputs { workspaces, .. } => { + for ws in workspaces { + if ws.has_window(window) { + for col in &mut ws.columns { + for tile in &mut col.tiles { + if tile.window() == window { + tile.start_open_animation(); + return; + } + } + } + } + } + } + } + } } impl Layout<Window> { @@ -1757,6 +1797,8 @@ mod tests { fn output_leave(&self, _output: &Output) {} + fn set_offscreen_element_id(&self, _id: Option<Id>) {} + fn is_fullscreen(&self) -> bool { false } diff --git a/src/layout/tile.rs b/src/layout/tile.rs index 80797130..dfda6fbb 100644 --- a/src/layout/tile.rs +++ b/src/layout/tile.rs @@ -3,13 +3,17 @@ use std::rc::Rc; use std::time::Duration; use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; -use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement}; -use smithay::backend::renderer::element::Kind; +use smithay::backend::renderer::element::utils::{ + Relocate, RelocateRenderElement, RescaleRenderElement, +}; +use smithay::backend::renderer::element::{Element, Kind}; use smithay::utils::{Logical, Point, Rectangle, Scale, Size}; use super::focus_ring::FocusRing; use super::{LayoutElement, LayoutElementRenderElement, Options}; +use crate::animation::{Animation, Curve}; use crate::niri_render_elements; +use crate::render_helpers::offscreen::OffscreenRenderElement; use crate::render_helpers::renderer::NiriRenderer; /// Toplevel window with decorations. @@ -39,6 +43,9 @@ pub struct Tile<W: LayoutElement> { /// The size we were requested to fullscreen into. fullscreen_size: Size<i32, Logical>, + /// The animation upon opening a window. + open_animation: Option<Animation>, + /// Configurable properties of the layout. options: Rc<Options>, } @@ -47,6 +54,7 @@ niri_render_elements! { TileRenderElement => { LayoutElement = LayoutElementRenderElement<R>, SolidColor = RelocateRenderElement<SolidColorRenderElement>, + Offscreen = RescaleRenderElement<OffscreenRenderElement>, } } @@ -59,6 +67,7 @@ impl<W: LayoutElement> Tile<W> { is_fullscreen: false, // FIXME: up-to-date fullscreen right away, but we need size. fullscreen_backdrop: SolidColorBuffer::new((0, 0), [0., 0., 0., 1.]), fullscreen_size: Default::default(), + open_animation: None, options, } } @@ -76,7 +85,7 @@ impl<W: LayoutElement> Tile<W> { } } - pub fn advance_animations(&mut self, _current_time: Duration, is_active: bool) { + pub fn advance_animations(&mut self, current_time: Duration, is_active: bool) { let width = self.border.width(); self.border.update( (width, width).into(), @@ -88,6 +97,25 @@ impl<W: LayoutElement> Tile<W> { self.focus_ring .update((0, 0).into(), self.tile_size(), self.has_ssd()); self.focus_ring.set_active(is_active); + + match &mut self.open_animation { + Some(anim) => { + anim.set_current_time(current_time); + if anim.is_done() { + self.open_animation = None; + } + } + None => (), + } + } + + pub fn are_animations_ongoing(&self) -> bool { + self.open_animation.is_some() + } + + pub fn start_open_animation(&mut self) { + self.open_animation = + Some(Animation::new(0., 1., Duration::from_millis(150)).with_curve(Curve::EaseOutExpo)); } pub fn window(&self) -> &W { @@ -160,6 +188,22 @@ impl<W: LayoutElement> Tile<W> { self.window.size() } + /// Returns an animated size of the tile for rendering and input. + /// + /// During the window opening animation, windows to the right should gradually slide further to + /// the right. This is what this visual size is used for. Other things like window resizes or + /// transactions or new view position calculation always use the real size, instead of this + /// visual size. + pub fn visual_tile_size(&self) -> Size<i32, Logical> { + let size = self.tile_size(); + let v = self + .open_animation + .as_ref() + .map(|anim| anim.value()) + .unwrap_or(1.); + Size::from(((f64::from(size.w) * v).round() as i32, size.h)) + } + pub fn buf_loc(&self) -> Point<i32, Logical> { let mut loc = Point::from((0, 0)); loc += self.window_loc(); @@ -251,7 +295,7 @@ impl<W: LayoutElement> Tile<W> { self.effective_border_width().is_some() || self.window.has_ssd() } - pub fn render<R: NiriRenderer>( + fn render_inner<R: NiriRenderer>( &self, renderer: &mut R, location: Point<i32, Logical>, @@ -300,4 +344,46 @@ impl<W: LayoutElement> Tile<W> { }); rv.chain(elem) } + + pub fn render<R: NiriRenderer>( + &self, + renderer: &mut R, + location: Point<i32, Logical>, + scale: Scale<f64>, + focus_ring: bool, + ) -> impl Iterator<Item = TileRenderElement<R>> { + if let Some(anim) = &self.open_animation { + let renderer = renderer.as_gles_renderer(); + let elements = self.render_inner(renderer, location, scale, focus_ring); + let elements = elements.collect::<Vec<TileRenderElement<_>>>(); + + let elem = OffscreenRenderElement::new( + renderer, + scale.x as i32, + &elements, + anim.value() as f32, + ); + self.window() + .set_offscreen_element_id(Some(elem.id().clone())); + + let mut center = location; + center.x += self.tile_size().w / 2; + center.y += self.tile_size().h / 2; + + Some(TileRenderElement::Offscreen( + RescaleRenderElement::from_element( + elem, + center.to_physical_precise_round(scale), + (anim.value() / 2. + 0.5).min(1.), + ), + )) + .into_iter() + .chain(None.into_iter().flatten()) + } else { + self.window().set_offscreen_element_id(None); + + let elements = self.render_inner(renderer, location, scale, focus_ring); + None.into_iter().chain(Some(elements).into_iter().flatten()) + } + } } diff --git a/src/layout/workspace.rs b/src/layout/workspace.rs index 07a94321..24372074 100644 --- a/src/layout/workspace.rs +++ b/src/layout/workspace.rs @@ -230,7 +230,7 @@ impl<W: LayoutElement> Workspace<W> { } pub fn are_animations_ongoing(&self) -> bool { - self.view_offset_anim.is_some() + self.view_offset_anim.is_some() || self.columns.iter().any(Column::are_animations_ongoing) } pub fn update_config(&mut self, options: Rc<Options>) { @@ -507,6 +507,16 @@ impl<W: LayoutElement> Workspace<W> { x } + fn visual_column_x(&self, column_idx: usize) -> i32 { + let mut x = 0; + + for column in self.columns.iter().take(column_idx) { + x += column.visual_width() + self.options.gaps; + } + + x + } + pub fn add_window( &mut self, window: W, @@ -978,13 +988,13 @@ impl<W: LayoutElement> Workspace<W> { } fn tiles_in_render_order(&self) -> impl Iterator<Item = (&'_ Tile<W>, Point<i32, Logical>)> { - let view_pos = self.view_pos(); + let view_pos = self.visual_column_x(self.active_column_idx) + self.view_offset; // Start with the active window since it's drawn on top. let col = &self.columns[self.active_column_idx]; let tile = &col.tiles[col.active_tile_idx]; let tile_pos = Point::from(( - self.column_x(self.active_column_idx) - view_pos, + self.visual_column_x(self.active_column_idx) - view_pos, col.tile_y(col.active_tile_idx), )); let first = iter::once((tile, tile_pos)); @@ -997,7 +1007,7 @@ impl<W: LayoutElement> Workspace<W> { // Keep track of column X position. .map(move |(col_idx, col)| { let rv = (col_idx, col, x); - x += col.width() + self.options.gaps; + x += col.visual_width() + self.options.gaps; rv }) .flat_map(move |(col_idx, col, x)| { @@ -1279,6 +1289,10 @@ impl<W: LayoutElement> Column<W> { } } + pub fn are_animations_ongoing(&self) -> bool { + self.tiles.iter().any(Tile::are_animations_ongoing) + } + pub fn contains(&self, window: &W) -> bool { self.tiles.iter().map(Tile::window).any(|win| win == window) } @@ -1477,6 +1491,14 @@ impl<W: LayoutElement> Column<W> { .unwrap() } + fn visual_width(&self) -> i32 { + self.tiles + .iter() + .map(|tile| tile.visual_tile_size().w) + .max() + .unwrap() + } + fn focus_up(&mut self) { self.active_tile_idx = self.active_tile_idx.saturating_sub(1); } |
