diff --git a/src/backend/render/mod.rs b/src/backend/render/mod.rs index 6e9d5e5d..25fe4d1b 100644 --- a/src/backend/render/mod.rs +++ b/src/backend/render/mod.rs @@ -476,19 +476,10 @@ where } } - state - .shell - .space_for_handle_mut(¤t.0) - .ok_or(OutputNoMode)? - .update_animations(&state.event_loop_handle); - if let Some((previous, _, _)) = previous.as_ref() { - state - .shell - .space_for_handle_mut(&previous) - .ok_or(OutputNoMode)? - .update_animations(&state.event_loop_handle); - } let overview = state.shell.overview_mode(); + let (resize_mode, resize_indicator) = state.shell.resize_mode(); + let resize_indicator = resize_indicator.map(|indicator| (resize_mode, indicator)); + let last_active_seat = state.last_active_seat().clone(); let move_active = last_active_seat .user_data() @@ -552,6 +543,7 @@ where state.xwayland_state.as_mut(), (!move_active && is_active_space).then_some(&last_active_seat), overview.clone(), + resize_indicator.clone(), state.config.static_conf.active_hint, ) .map_err(|_| OutputNoMode)? @@ -598,6 +590,7 @@ where state.xwayland_state.as_mut(), (!move_active && is_active_space).then_some(&last_active_seat), overview, + resize_indicator, state.config.static_conf.active_hint, ) .map_err(|_| OutputNoMode)? diff --git a/src/shell/element/mod.rs b/src/shell/element/mod.rs index b230d67f..2f4652d5 100644 --- a/src/shell/element/mod.rs +++ b/src/shell/element/mod.rs @@ -54,6 +54,7 @@ pub mod stack; pub use self::stack::CosmicStack; pub mod window; pub use self::window::CosmicWindow; +pub mod resize_indicator; #[cfg(feature = "debug")] use egui::plot::{Corner, Legend, Plot, PlotPoints, Polygon}; diff --git a/src/shell/element/resize_indicator.rs b/src/shell/element/resize_indicator.rs new file mode 100644 index 00000000..17e74ac9 --- /dev/null +++ b/src/shell/element/resize_indicator.rs @@ -0,0 +1,223 @@ +use std::sync::Mutex; + +use crate::{ + config::{Action, Config}, + fl, + shell::{grabs::ResizeEdge, ResizeDirection}, + utils::iced::{IcedElement, Program}, +}; + +use apply::Apply; +use calloop::LoopHandle; +use cosmic::{ + iced::widget::{column, container, horizontal_space, row, vertical_space}, + iced_core::{Background, Color, Length}, + theme, + widget::{icon, text}, +}; +use smithay::utils::Size; + +pub type ResizeIndicator = IcedElement; + +pub fn resize_indicator( + direction: ResizeDirection, + config: &Config, + evlh: LoopHandle<'static, crate::state::Data>, +) -> ResizeIndicator { + ResizeIndicator::new( + ResizeIndicatorInternal { + edges: Mutex::new(ResizeEdge::all()), + direction, + shortcut1: config + .static_conf + .key_bindings + .iter() + .find_map(|(pattern, action)| { + (*action == Action::Resizing(ResizeDirection::Outwards)).then_some(pattern) + }) + .map(|pattern| format!("{}: ", pattern.to_string())) + .unwrap_or_else(|| crate::fl!("unknown-keybinding")), + shortcut2: config + .static_conf + .key_bindings + .iter() + .find_map(|(pattern, action)| { + (*action == Action::Resizing(ResizeDirection::Inwards)).then_some(pattern) + }) + .map(|pattern| format!("{}: ", pattern.to_string())) + .unwrap_or_else(|| crate::fl!("unknown-keybinding")), + }, + Size::from((1, 1)), + evlh, + ) +} + +pub struct ResizeIndicatorInternal { + pub edges: Mutex, + pub direction: ResizeDirection, + pub shortcut1: String, + pub shortcut2: String, +} + +impl Program for ResizeIndicatorInternal { + type Message = (); + + fn view(&self) -> crate::utils::iced::Element<'_, Self::Message> { + let edges = self.edges.lock().unwrap(); + column(vec![ + if edges.contains(ResizeEdge::TOP) { + icon( + if self.direction == ResizeDirection::Outwards { + "go-up-symbolic" + } else { + "go-down-symbolic" + }, + 32, + ) + .force_svg(true) + .apply(container) + .padding(2) + .style(theme::Container::custom(|theme| container::Appearance { + text_color: Some(Color::from(theme.cosmic().accent.on)), + background: Some(Background::Color(theme.cosmic().accent_color().into())), + border_radius: 18.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + })) + .width(Length::Shrink) + .apply(container) + .center_x() + .width(Length::Fill) + .into() + } else { + vertical_space(36).into() + }, + row(vec![ + if edges.contains(ResizeEdge::LEFT) { + icon( + if self.direction == ResizeDirection::Outwards { + "go-previous-symbolic" + } else { + "go-next-symbolic" + }, + 32, + ) + .force_svg(true) + .apply(container) + .padding(4) + .style(theme::Container::custom(|theme| container::Appearance { + text_color: Some(Color::from(theme.cosmic().accent.on)), + background: Some(Background::Color(theme.cosmic().accent_color().into())), + border_radius: 18.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + })) + .width(Length::Shrink) + .apply(container) + .center_y() + .height(Length::Fill) + .into() + } else { + horizontal_space(36).into() + }, + row(vec![ + text(&self.shortcut1) + .font(cosmic::font::FONT_SEMIBOLD) + .size(14) + .into(), + text(fl!("grow-window")) + .font(cosmic::font::FONT) + .size(14) + .into(), + horizontal_space(40).into(), + text(&self.shortcut2) + .font(cosmic::font::FONT_SEMIBOLD) + .size(14) + .into(), + text(fl!("shrink-window")) + .font(cosmic::font::FONT) + .size(14) + .into(), + ]) + .apply(container) + .center_x() + .center_y() + .padding(16) + .apply(container) + .style(theme::Container::custom(|theme| container::Appearance { + text_color: Some(Color::from(theme.cosmic().accent.on)), + background: Some(Background::Color(theme.cosmic().accent_color().into())), + border_radius: 18.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + })) + .width(Length::Shrink) + .height(Length::Shrink) + .apply(container) + .height(Length::Fill) + .width(Length::Fill) + .center_x() + .center_y() + .into(), + if edges.contains(ResizeEdge::RIGHT) { + icon( + if self.direction == ResizeDirection::Outwards { + "go-next-symbolic" + } else { + "go-previous-symbolic" + }, + 32, + ) + .force_svg(true) + .apply(container) + .padding(4) + .style(theme::Container::custom(|theme| container::Appearance { + text_color: Some(Color::from(theme.cosmic().accent.on)), + background: Some(Background::Color(theme.cosmic().accent_color().into())), + border_radius: 18.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + })) + .height(Length::Shrink) + .apply(container) + .center_y() + .height(Length::Fill) + .into() + } else { + horizontal_space(36).into() + }, + ]) + .width(Length::Fill) + .height(Length::Fill) + .into(), + if edges.contains(ResizeEdge::BOTTOM) { + icon( + if self.direction == ResizeDirection::Outwards { + "go-down-symbolic" + } else { + "go-up-symbolic" + }, + 32, + ) + .force_svg(true) + .apply(container) + .padding(4) + .style(theme::Container::custom(|theme| container::Appearance { + text_color: Some(Color::from(theme.cosmic().accent.on)), + background: Some(Background::Color(theme.cosmic().accent_color().into())), + border_radius: 18.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + })) + .width(Length::Shrink) + .apply(container) + .center_x() + .width(Length::Fill) + .into() + } else { + vertical_space(36).into() + }, + ]) + .into() + } +} diff --git a/src/shell/layout/floating/mod.rs b/src/shell/layout/floating/mod.rs index a42a5eb4..48caab09 100644 --- a/src/shell/layout/floating/mod.rs +++ b/src/shell/layout/floating/mod.rs @@ -16,8 +16,8 @@ use crate::{ backend::render::{element::AsGlowRenderer, IndicatorShader}, shell::{ element::{ - stack::CosmicStackRenderElement, window::CosmicWindowRenderElement, CosmicMapped, - CosmicMappedRenderElement, + resize_indicator::ResizeIndicator, stack::CosmicStackRenderElement, + window::CosmicWindowRenderElement, CosmicMapped, CosmicMappedRenderElement, }, focus::target::KeyboardFocusTarget, grabs::ResizeEdge, @@ -421,6 +421,7 @@ impl FloatingLayout { renderer: &mut R, output: &Output, focused: Option<&CosmicMapped>, + mut resize_indicator: Option<(ResizeMode, ResizeIndicator)>, indicator_thickness: u8, alpha: f32, ) -> Vec> @@ -435,14 +436,15 @@ impl FloatingLayout { puffin::profile_function!(); let output_scale = output.current_scale().fractional_scale(); - let output_loc = self.space.output_geometry(output).unwrap().loc; + let output_geo = self.space.output_geometry(output).unwrap(); self.space .elements_for_output(output) .rev() .flat_map(|elem| { - let render_location = - self.space.element_location(elem).unwrap() - output_loc - elem.geometry().loc; + let render_location = self.space.element_location(elem).unwrap() + - output_geo.loc + - elem.geometry().loc; let mut elements = elem.render_elements( renderer, render_location.to_physical_precise_round(output_scale), @@ -450,19 +452,41 @@ impl FloatingLayout { alpha, ); if focused == Some(elem) { + let mut indicator_geometry = Rectangle::from_loc_and_size( + self.space.element_location(elem).unwrap() - output_geo.loc, + elem.geometry().size, + ); + if indicator_thickness > 0 { let element = IndicatorShader::focus_element( renderer, elem.clone(), - Rectangle::from_loc_and_size( - self.space.element_location(elem).unwrap() - output_loc, - elem.geometry().size, - ), + indicator_geometry, indicator_thickness, alpha, ); elements.insert(0, element.into()); } + + if let Some((mode, resize)) = resize_indicator.as_mut() { + indicator_geometry.loc -= (18, 18).into(); + indicator_geometry.size += (36, 36).into(); + resize.resize(indicator_geometry.size); + resize.output_enter(output, output_geo); + elements = resize + .render_elements::>( + renderer, + indicator_geometry + .loc + .to_physical_precise_round(output_scale), + output_scale.into(), + alpha * mode.alpha().unwrap_or(1.0), + ) + .into_iter() + .map(CosmicMappedRenderElement::Window) + .chain(elements.into_iter()) + .collect(); + } } elements }) diff --git a/src/shell/layout/tiling/mod.rs b/src/shell/layout/tiling/mod.rs index 412e5b85..307e6553 100644 --- a/src/shell/layout/tiling/mod.rs +++ b/src/shell/layout/tiling/mod.rs @@ -4,6 +4,7 @@ use crate::{ backend::render::{element::AsGlowRenderer, BackdropShader, IndicatorShader, Key, GROUP_COLOR}, shell::{ element::{ + resize_indicator::ResizeIndicator, stack::{CosmicStackRenderElement, MoveResult as StackMoveResult}, window::CosmicWindowRenderElement, CosmicMapped, CosmicMappedRenderElement, CosmicStack, CosmicWindow, @@ -1915,6 +1916,7 @@ impl TilingLayout { seat: Option<&Seat>, non_exclusive_zone: Rectangle, overview: OverviewMode, + resize_indicator: Option<(ResizeMode, ResizeIndicator)>, indicator_thickness: u8, ) -> Result>, OutputNotMapped> where @@ -1955,26 +1957,7 @@ impl TilingLayout { } else { 1.0 }; - let draw_groups = match overview { - OverviewMode::Started(_, start) => { - let percentage = (Instant::now().duration_since(start).as_millis() as f32 - / ANIMATION_DURATION.as_millis() as f32) - .min(1.0); - Some(Ease::Cubic(Cubic::Out).tween(percentage)) - } - OverviewMode::Ended(end) => { - let percentage = (1.0 - - Instant::now().duration_since(end).as_millis() as f32 - / ANIMATION_DURATION.as_millis() as f32) - .max(0.0); - if percentage > 0.0 { - Some(Ease::Cubic(Cubic::Out).tween(percentage)) - } else { - None - } - } - OverviewMode::None => None, - }; + let draw_groups = overview.alpha(); let mut elements = Vec::new(); @@ -2032,7 +2015,7 @@ impl TilingLayout { geometries, old_geometries, seat, - output_scale, + output, percentage, if let Some(transition) = draw_groups { let diff = (4u8.abs_diff(indicator_thickness) as f32 * transition).round() as u8; @@ -2044,6 +2027,7 @@ impl TilingLayout { } else { indicator_thickness }, + resize_indicator, )); // tiling hints @@ -2425,9 +2409,10 @@ fn render_new_tree( geometries: Option>>, old_geometries: Option>>, seat: Option<&Seat>, - output_scale: f64, + output: &Output, percentage: f32, indicator_thickness: u8, + mut resize_indicator: Option<(ResizeMode, ResizeIndicator)>, ) -> Vec> where R: Renderer + ImportAll + ImportMem + AsGlowRenderer, @@ -2452,11 +2437,16 @@ where .map(|(id, _)| id); let mut group_backdrop = None; + let mut indicator = None; + let mut resize_elements = None; + + let output_geo = output.geometry(); + let output_scale = output.current_scale().fractional_scale(); if let Some(root) = target_tree.root_node_id() { let old_geometries = old_geometries.unwrap_or_default(); let geometries = geometries.unwrap_or_default(); - let mut elements: Vec> = target_tree + let elements: Vec> = target_tree .traverse_pre_order_ids(root) .unwrap() .flat_map(|node_id| { @@ -2546,7 +2536,7 @@ where let mut elements = Vec::new(); - if focused == Some(node_id) { + if focused.as_ref() == Some(&node_id) { if indicator_thickness > 0 || data.is_group() { let mut geo = geo.clone(); if data.is_group() { @@ -2554,25 +2544,56 @@ where geo.loc += (outer_gap, outer_gap).into(); geo.size -= (outer_gap * 2, outer_gap * 2).into(); - group_backdrop = Some( - BackdropShader::element( - renderer, - match data { - Data::Group { alive, .. } => { - Key::Group(Arc::downgrade(alive)) - } - _ => unreachable!(), - }, - geo, - 8., - 0.4, - GROUP_COLOR, - ) - .into(), - ); + group_backdrop = Some(BackdropShader::element( + renderer, + match data { + Data::Group { alive, .. } => Key::Group(Arc::downgrade(alive)), + _ => unreachable!(), + }, + geo, + 8., + 0.4, + GROUP_COLOR, + )); } - let element = IndicatorShader::focus_element( + if let Some((mode, resize)) = resize_indicator.as_mut() { + let mut geo = geo.clone(); + geo.loc -= (18, 18).into(); + geo.size += (36, 36).into(); + + resize.resize(geo.size); + resize.output_enter(output, output_geo); + let possible_edges = + TilingLayout::possible_resizes(target_tree, node_id); + if !possible_edges.is_empty() { + if resize.with_program(|internal| { + let mut edges = internal.edges.lock().unwrap(); + if *edges != possible_edges { + *edges = possible_edges; + true + } else { + false + } + }) { + resize.force_update(); + } + resize_elements = Some( + resize + .render_elements::>( + renderer, + geo.loc.to_physical_precise_round(output_scale), + output_scale.into(), + alpha * mode.alpha().unwrap_or(1.0), + ) + .into_iter() + .map(CosmicMappedRenderElement::from) + .collect::>(), + ); + } + } + + indicator = Some(IndicatorShader::focus_element( renderer, match data { Data::Mapped { mapped, .. } => mapped.clone().into(), @@ -2585,8 +2606,7 @@ where indicator_thickness }, 1.0, - ); - elements.push(element.into()); + )); } } @@ -2654,8 +2674,14 @@ where elements }) .collect(); - elements.extend(group_backdrop); - elements + + resize_elements + .into_iter() + .flatten() + .chain(indicator.into_iter().map(Into::into)) + .chain(elements) + .chain(group_backdrop.into_iter().map(Into::into)) + .collect() } else { Vec::new() } diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 2288b082..17aa4aca 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -4,12 +4,13 @@ use std::{ cell::RefCell, collections::HashMap, sync::atomic::{AtomicBool, Ordering}, - time::Instant, + time::{Duration, Instant}, }; use tracing::warn; use wayland_backend::server::ClientId; use cosmic_protocols::workspace::v1::server::zcosmic_workspace_handle_v1::State as WState; +use cosmic_time::{Cubic, Ease, Tween}; use smithay::{ desktop::{ layer_map_for_output, space::SpaceElement, LayerSurface, PopupManager, WindowSurfaceType, @@ -63,10 +64,12 @@ use self::{ grabs::ResizeEdge, layout::{ floating::{FloatingLayout, ResizeState}, - tiling::{Direction, TilingLayout, ANIMATION_DURATION}, + tiling::{Direction, TilingLayout}, }, }; +const ANIMATION_DURATION: Duration = Duration::from_millis(200); + #[derive(Debug, Clone)] pub enum OverviewMode { None, @@ -74,6 +77,31 @@ pub enum OverviewMode { Ended(Instant), } +impl OverviewMode { + pub fn alpha(&self) -> Option { + match self { + OverviewMode::Started(_, start) => { + let percentage = (Instant::now().duration_since(*start).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32) + .min(1.0); + Some(Ease::Cubic(Cubic::Out).tween(percentage)) + } + OverviewMode::Ended(end) => { + let percentage = (1.0 + - Instant::now().duration_since(*end).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32) + .max(0.0); + if percentage > 0.0 { + Some(Ease::Cubic(Cubic::Out).tween(percentage)) + } else { + None + } + } + OverviewMode::None => None, + } + } +} + #[derive(Debug, Clone, Copy, serde::Deserialize, PartialEq, Eq, Hash)] pub enum ResizeDirection { Inwards, @@ -87,6 +115,31 @@ pub enum ResizeMode { Ended(Instant, ResizeDirection), } +impl ResizeMode { + pub fn alpha(&self) -> Option { + match self { + ResizeMode::Started(_, start, _) => { + let percentage = (Instant::now().duration_since(*start).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32) + .min(1.0); + Some(Ease::Cubic(Cubic::Out).tween(percentage)) + } + ResizeMode::Ended(end, _) => { + let percentage = (1.0 + - Instant::now().duration_since(*end).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32) + .max(0.0); + if percentage > 0.0 { + Some(Ease::Cubic(Cubic::Out).tween(percentage)) + } else { + None + } + } + ResizeMode::None => None, + } + } +} + pub struct Shell { pub popups: PopupManager, pub outputs: Vec, @@ -114,6 +167,7 @@ pub struct Shell { usize, Output, )>, + resize_indicator: Option, } #[derive(Debug)] @@ -574,6 +628,7 @@ impl Shell { overview_mode: OverviewMode::None, resize_mode: ResizeMode::None, resize_state: None, + resize_indicator: None, } } @@ -1177,7 +1232,12 @@ impl Shell { self.overview_mode.clone() } - pub fn set_resize_mode(&mut self, enabled: Option<(KeyPattern, ResizeDirection)>) { + pub fn set_resize_mode( + &mut self, + enabled: Option<(KeyPattern, ResizeDirection)>, + config: &Config, + evlh: LoopHandle<'static, crate::state::Data>, + ) { if let Some((pattern, direction)) = enabled { if let ResizeMode::Started(old_pattern, _, old_direction) = &mut self.resize_mode { *old_pattern = pattern; @@ -1185,6 +1245,7 @@ impl Shell { } else { self.resize_mode = ResizeMode::Started(pattern, Instant::now(), direction); } + self.resize_indicator = Some(resize_indicator(direction, config, evlh)); } else { if let ResizeMode::Started(_, _, direction) = &self.resize_mode { self.resize_mode = ResizeMode::Ended(Instant::now(), *direction); @@ -1192,25 +1253,15 @@ impl Shell { } } - pub fn resize_mode(&mut self) -> ResizeMode { + pub fn resize_mode(&mut self) -> (ResizeMode, Option) { if let ResizeMode::Ended(timestamp, _) = self.resize_mode { if Instant::now().duration_since(timestamp) > ANIMATION_DURATION { self.resize_mode = ResizeMode::None; + self.resize_indicator = None; } } - self.resize_mode.clone() - } - - pub fn resize_active_window( - &mut self, - seat: &Seat, - direction: ResizeDirection, - edge: ResizeEdge, - ) { - self.workspaces - .active_mut(&seat.active_output()) - .resize(seat, direction, edge); + (self.resize_mode.clone(), self.resize_indicator.clone()) } pub fn refresh(&mut self) { diff --git a/src/shell/workspace.rs b/src/shell/workspace.rs index e4ba3258..00a436f6 100644 --- a/src/shell/workspace.rs +++ b/src/shell/workspace.rs @@ -47,7 +47,10 @@ use tracing::warn; use wayland_backend::server::ClientId; use super::{ - element::{stack::CosmicStackRenderElement, window::CosmicWindowRenderElement, CosmicMapped}, + element::{ + resize_indicator::ResizeIndicator, stack::CosmicStackRenderElement, + window::CosmicWindowRenderElement, CosmicMapped, + }, focus::{target::KeyboardFocusTarget, FocusStack, FocusStackMut}, grabs::{ResizeEdge, ResizeGrab}, CosmicMappedRenderElement, CosmicSurface, ResizeDirection, ResizeMode, @@ -500,6 +503,7 @@ impl Workspace { xwm_state: Option<&'a mut XWaylandState>, draw_focus_indicator: Option<&Seat>, overview: OverviewMode, + resize_indicator: Option<(ResizeMode, ResizeIndicator)>, indicator_thickness: u8, ) -> Result>, OutputNotMapped> where @@ -606,6 +610,7 @@ impl Workspace { renderer, output, focused.as_ref(), + resize_indicator.clone(), indicator_thickness, alpha, ) @@ -622,6 +627,7 @@ impl Workspace { draw_focus_indicator, layer_map.non_exclusive_zone(), overview.clone(), + resize_indicator, indicator_thickness, )? .into_iter()