diff --git a/Cargo.lock b/Cargo.lock index 22504908..38e0eefd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -586,12 +586,14 @@ dependencies = [ "renderdoc", "ron", "rust-embed", + "sanitize-filename", "sendfd", "serde", "serde_json", "smithay", "smithay-egui", "thiserror", + "time", "tiny-skia 0.10.0", "tracing", "tracing-journald", @@ -600,6 +602,7 @@ dependencies = [ "wayland-scanner", "xcursor", "xdg", + "xdg-user", "xkbcommon", ] @@ -614,7 +617,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "atomicwrites", "calloop", @@ -629,7 +632,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "quote", "syn 1.0.109", @@ -670,7 +673,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "almost", "cosmic-config", @@ -1806,6 +1809,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "i18n-config" version = "0.4.6" @@ -1879,7 +1891,7 @@ dependencies = [ [[package]] name = "iced" version = "0.10.0" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "iced_core", "iced_futures", @@ -1892,7 +1904,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.10.0" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "bitflags 1.3.2", "instant", @@ -1906,7 +1918,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.7.0" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "futures", "iced_core", @@ -1918,7 +1930,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.9.0" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -1936,7 +1948,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -1949,7 +1961,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.1.1" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "iced_core", "iced_futures", @@ -1959,7 +1971,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.9.0" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "iced_core", "once_cell", @@ -1969,7 +1981,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "bytemuck", "cosmic-text", @@ -1987,7 +1999,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.11.1" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2009,7 +2021,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.1.3" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "iced_renderer", "iced_runtime", @@ -2345,7 +2357,7 @@ checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic/#57f4abb8a000c0e554c77807100025301599cf3f" +source = "git+https://github.com/pop-os/libcosmic/#bb7c7ac52a0103a65dc33f77694037c10978926e" dependencies = [ "apply", "cosmic-config", @@ -2976,6 +2988,15 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "objc" version = "0.2.7" @@ -3818,6 +3839,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sanitize-filename" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "scan_fmt" version = "0.2.6" @@ -4217,7 +4248,7 @@ dependencies = [ [[package]] name = "taffy" version = "0.3.11" -source = "git+https://github.com/DioxusLabs/taffy#1876f72bee5e376023eaa518aa7b8a34c769bd1b" +source = "git+https://github.com/DioxusLabs/taffy?rev=7781c70#7781c70241f7f572130c13106f2a869a9cf80885" dependencies = [ "arrayvec", "grid", @@ -4296,6 +4327,8 @@ checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", + "libc", + "num_threads", "powerfmt", "serde", "time-core", @@ -5496,6 +5529,16 @@ version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" +[[package]] +name = "xdg-user" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d5cd803f28ce5a488c8b129858222998c0a06bbec81f9d1b71faed1f9f9f0e" +dependencies = [ + "home", + "libc", +] + [[package]] name = "xkbcommon" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index b240d2dd..d54ca85f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,8 +52,11 @@ keyframe = "1.1.1" once_cell = "1.18.0" i18n-embed = { version = "0.14", features = ["fluent-system", "desktop-requester"] } i18n-embed-fl = "0.7" -rust-embed = "8.0" +rust-embed = { version = "8.0", features = ["debug-embed"] } libc = "0.2.149" +xdg-user = "0.2.1" +time = { version = "0.3.30", features = ["macros", "formatting", "local-offset"] } +sanitize-filename = "0.5.0" [dependencies.id_tree] git = "https://github.com/Drakulix/id-tree.git" diff --git a/resources/i18n/en/cosmic_comp.ftl b/resources/i18n/en/cosmic_comp.ftl index 71e72984..31906a9b 100644 --- a/resources/i18n/en/cosmic_comp.ftl +++ b/resources/i18n/en/cosmic_comp.ftl @@ -2,4 +2,22 @@ grow-window = Grow shrink-window = Shrink swap-windows = Swap Windows stack-windows = Stack Windows -unknown-keybinding = \ No newline at end of file +unknown-keybinding = +window-menu-minimize = Minimize +window-menu-maximize = Maximize +window-menu-tiled = Float window +window-menu-screenshot = Take screenshot +window-menu-move = Move +window-menu-resize = Resize +window-menu-move-prev-workspace = Move to previous workspace +window-menu-move-next-workspace = Move to next workspace +window-menu-stack = Create window stack +window-menu-unstack = Unstack windows +window-menu-always-on-top = Always on top +window-menu-always-on-visible-ws = Always on visible workspace +window-menu-close = Close +window-menu-close-all = Close all windows +window-menu-resize-edge-top = Top +window-menu-resize-edge-left = Left +window-menu-resize-edge-right = Right +window-menu-resize-edge-bottom = Bottom \ No newline at end of file diff --git a/src/backend/render/mod.rs b/src/backend/render/mod.rs index 5c999d31..acb00949 100644 --- a/src/backend/render/mod.rs +++ b/src/backend/render/mod.rs @@ -12,7 +12,9 @@ use std::{ use crate::debug::{fps_ui, profiler_ui}; use crate::{ shell::{ - focus::target::WindowGroup, grabs::SeatMoveGrabState, layout::tiling::ANIMATION_DURATION, + focus::target::WindowGroup, + grabs::{SeatMenuGrabState, SeatMoveGrabState}, + layout::tiling::ANIMATION_DURATION, CosmicMapped, CosmicMappedRenderElement, OverviewMode, SessionLock, Trigger, WorkspaceRenderElement, }, @@ -422,6 +424,17 @@ where { elements.extend(grab_elements); } + + if let Some(grab_elements) = seat + .user_data() + .get::() + .unwrap() + .borrow() + .as_ref() + .map(|state| state.render::, R>(renderer, output)) + { + elements.extend(grab_elements.into_iter().map(Into::into)); + } } elements diff --git a/src/input/mod.rs b/src/input/mod.rs index 843d6734..86031e18 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -5,7 +5,7 @@ use crate::{ config::{xkb_config_to_wl, Action, Config, KeyPattern}, shell::{ focus::{target::PointerFocusTarget, FocusDirection}, - grabs::{ResizeEdge, SeatMoveGrabState}, + grabs::{ResizeEdge, SeatMenuGrabState, SeatMoveGrabState}, layout::{ floating::ResizeGrabMarker, tiling::{SwapWindowGrab, TilingLayout}, @@ -185,6 +185,7 @@ pub fn add_seat( userdata.insert_if_missing(SupressedKeys::default); userdata.insert_if_missing(ModifiersShortcutQueue::default); userdata.insert_if_missing(SeatMoveGrabState::default); + userdata.insert_if_missing(SeatMenuGrabState::default); userdata.insert_if_missing(CursorState::default); userdata.insert_if_missing(|| ActiveOutput(RefCell::new(output.clone()))); userdata.insert_if_missing(|| RefCell::new(CursorImageStatus::default_named())); diff --git a/src/shell/element/stack.rs b/src/shell/element/stack.rs index 11557e2a..a79ea64f 100644 --- a/src/shell/element/stack.rs +++ b/src/shell/element/stack.rs @@ -590,6 +590,7 @@ impl CosmicStack { #[derive(Debug, Clone, Copy)] pub enum Message { DragStart, + Menu, PotentialTabDragStart(usize), Activate(usize), Close(usize), @@ -684,6 +685,41 @@ impl Program for CosmicStackInternal { Message::Scrolled => { self.scroll_to_focus.store(false, Ordering::SeqCst); } + Message::Menu => { + if let Some((seat, serial)) = self.last_seat.lock().unwrap().clone() { + if let Some(surface) = self.windows.lock().unwrap() + [self.active.load(Ordering::SeqCst)] + .wl_surface() + { + loop_handle.insert_idle(move |state| { + if let Some(mapped) = + state.common.shell.element_for_wl_surface(&surface).cloned() + { + if let Some(workspace) = state.common.shell.space_for_mut(&mapped) { + let position = workspace + .element_geometry(&mapped) + .unwrap() + .loc + .to_global(&workspace.output); + let mut cursor = seat + .get_pointer() + .unwrap() + .current_location() + .to_i32_round(); + cursor.y -= TAB_HEIGHT; + Shell::menu_request( + state, + &surface, + &seat, + serial, + cursor - position.as_logical(), + ); + } + } + }); + } + } + } _ => unreachable!(), } Command::none() @@ -718,6 +754,7 @@ impl Program for CosmicStackInternal { .center_y() .apply(iced_widget::mouse_area) .on_press(Message::DragStart) + .on_right_press(Message::Menu) .into(), CosmicElement::new( Tabs::new( @@ -750,6 +787,7 @@ impl Program for CosmicStackInternal { .padding([64, 24]) .apply(iced_widget::mouse_area) .on_press(Message::DragStart) + .on_right_press(Message::Menu) .into(), ]; diff --git a/src/shell/element/window.rs b/src/shell/element/window.rs index 20d47cce..8e4639c9 100644 --- a/src/shell/element/window.rs +++ b/src/shell/element/window.rs @@ -8,7 +8,7 @@ use crate::{ wayland::handlers::screencopy::ScreencopySessions, }; use calloop::LoopHandle; -use cosmic::iced::Command; +use cosmic::{iced::Command, widget::mouse_area, Apply}; use cosmic_protocols::screencopy::v1::server::zcosmic_screencopy_session_v1::InputType; use smithay::{ backend::{ @@ -240,6 +240,7 @@ pub enum Message { DragStart, Maximize, Close, + Menu, } impl Program for CosmicWindowInternal { @@ -284,6 +285,38 @@ impl Program for CosmicWindowInternal { } } Message::Close => self.window.close(), + Message::Menu => { + if let Some((seat, serial)) = self.last_seat.lock().unwrap().clone() { + if let Some(surface) = self.window.wl_surface() { + loop_handle.insert_idle(move |state| { + if let Some(mapped) = + state.common.shell.element_for_wl_surface(&surface).cloned() + { + if let Some(workspace) = state.common.shell.space_for_mut(&mapped) { + let position = workspace + .element_geometry(&mapped) + .unwrap() + .loc + .to_global(&workspace.output); + let mut cursor = seat + .get_pointer() + .unwrap() + .current_location() + .to_i32_round(); + cursor.y -= SSD_HEIGHT; + Shell::menu_request( + state, + &surface, + &seat, + serial, + cursor - position.as_logical(), + ); + } + } + }); + } + } + } } Command::none() } @@ -355,7 +388,9 @@ impl Program for CosmicWindowInternal { .on_drag(Message::DragStart) .on_maximize(Message::Maximize) .on_close(Message::Close) - .into_element() + .apply(mouse_area) + .on_right_press(Message::Menu) + .into() } } diff --git a/src/shell/grabs/menu/item.rs b/src/shell/grabs/menu/item.rs new file mode 100644 index 00000000..97ea987e --- /dev/null +++ b/src/shell/grabs/menu/item.rs @@ -0,0 +1,225 @@ +use cosmic::{ + iced::Element, + iced_core::{ + event, layout, mouse, overlay, + renderer::{Quad, Style}, + widget::{tree, Id, OperationOutputWrapper, Tree, Widget}, + Background, Clipboard, Color, Event, Layout, Length, Rectangle, Renderer as IcedRenderer, + Shell, + }, + widget::button::StyleSheet, +}; + +pub struct SubmenuItem<'a, Message, Renderer> +where + Renderer: IcedRenderer, + Renderer::Theme: StyleSheet, +{ + elem: Element<'a, Message, Renderer>, + idx: usize, + styling: ::Style, +} + +impl<'a, Message, Renderer> SubmenuItem<'a, Message, Renderer> +where + Renderer: IcedRenderer, + Renderer::Theme: StyleSheet, +{ + pub fn new(elem: impl Into>, idx: usize) -> Self { + Self { + elem: elem.into(), + idx, + styling: Default::default(), + } + } + + pub fn style(mut self, style: ::Style) -> Self { + self.styling = style; + self + } +} + +pub trait CursorEvents { + fn cursor_entered(idx: usize, bounds: Rectangle) -> Self; + fn cursor_left(idx: usize, bounds: Rectangle) -> Self; +} + +struct State { + cursor_over: bool, +} + +impl<'a, Message, Renderer> Widget for SubmenuItem<'a, Message, Renderer> +where + Renderer: IcedRenderer, + Renderer::Theme: StyleSheet, + Message: CursorEvents, +{ + fn id(&self) -> Option { + None + } + + fn width(&self) -> Length { + self.elem.as_widget().width() + } + + fn height(&self) -> Length { + self.elem.as_widget().height() + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + let node = self.elem.as_widget().layout(renderer, limits); + layout::Node::with_children(node.size(), vec![node]) + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &::Theme, + style: &Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let widget_state = state.state.downcast_ref::(); + let styling = if widget_state.cursor_over { + theme.hovered(true, &self.styling) + } else { + theme.active(true, &self.styling) + }; + + renderer.fill_quad( + Quad { + bounds: layout.bounds(), + border_radius: styling.border_radius, + border_width: styling.border_width, + border_color: styling.border_color, + }, + styling + .background + .unwrap_or(Background::Color(Color::TRANSPARENT)), + ); + + let state = &state.children[0]; + let layout = layout.children().next().unwrap(); + self.elem.as_widget().draw( + state, + renderer, + theme, + &Style { + text_color: styling.text_color.unwrap_or(style.text_color), + icon_color: styling.icon_color.unwrap_or(style.text_color), + ..*style + }, + layout, + cursor, + viewport, + ) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State { cursor_over: false }) + } + + fn children(&self) -> Vec { + vec![Tree::new(&self.elem)] + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(std::slice::from_mut(&mut self.elem)) + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn cosmic::widget::Operation>, + ) { + let state = &mut state.children[0]; + let layout = layout.children().next().unwrap(); + self.elem + .as_widget() + .operate(state, layout, renderer, operation) + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) -> event::Status { + let is_over = cursor.is_over(layout.bounds()); + let widget_state = state.state.downcast_mut::(); + match event { + Event::Mouse(mouse::Event::CursorEntered) + | Event::Mouse(mouse::Event::CursorMoved { .. }) + if is_over && !widget_state.cursor_over => + { + shell.publish(Message::cursor_entered(self.idx, layout.bounds())); + widget_state.cursor_over = true; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Mouse(mouse::Event::CursorLeft) + if !is_over && widget_state.cursor_over => + { + shell.publish(Message::cursor_left(self.idx, layout.bounds())); + widget_state.cursor_over = false; + } + _ => {} + }; + + let state = &mut state.children[0]; + let layout = layout.children().next().unwrap(); + self.elem.as_widget_mut().on_event( + state, event, layout, cursor, renderer, clipboard, shell, viewport, + ) + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let state = &state.children[0]; + let layout = layout.children().next().unwrap(); + self.elem + .as_widget() + .mouse_interaction(state, layout, cursor, viewport, renderer) + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + ) -> Option> { + let state = &mut state.children[0]; + let layout = layout.children().next().unwrap(); + self.elem.as_widget_mut().overlay(state, layout, renderer) + } +} + +impl<'a, Message, Renderer> Into> + for SubmenuItem<'a, Message, Renderer> +where + Renderer: IcedRenderer + 'a, + Renderer::Theme: StyleSheet, + Message: CursorEvents + 'a, +{ + fn into(self) -> Element<'a, Message, Renderer> { + Element::new(self) + } +} diff --git a/src/shell/grabs/menu/mod.rs b/src/shell/grabs/menu/mod.rs new file mode 100644 index 00000000..10202f7a --- /dev/null +++ b/src/shell/grabs/menu/mod.rs @@ -0,0 +1,949 @@ +use std::{ + cell::RefCell, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, +}; + +use anyhow::Context; +use calloop::LoopHandle; +use cosmic::{ + iced::Background, + iced_core::{alignment::Horizontal, Length, Rectangle as IcedRectangle}, + iced_widget::{self, horizontal_rule, text::Appearance as TextAppearance, Column, Row}, + theme, + widget::{button, horizontal_space, icon::from_name, text}, + Apply as _, Command, +}; +use smithay::{ + backend::{ + allocator::Fourcc, + input::ButtonState, + renderer::{ + damage::OutputDamageTracker, + element::{ + memory::MemoryRenderBufferRenderElement, surface::WaylandSurfaceRenderElement, + AsRenderElements, + }, + gles::GlesRenderbuffer, + ExportMem, ImportAll, ImportMem, Offscreen, Renderer, + }, + }, + desktop::{space::SpaceElement, utils::bbox_from_surface_tree}, + input::{ + pointer::{ + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, + GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, + GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, + GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle, + PointerTarget, RelativeMotionEvent, + }, + Seat, + }, + output::Output, + utils::{Logical, Point, Scale, Size, Transform}, + wayland::seat::WaylandFocus, +}; +use tracing::warn; + +use crate::{ + backend::kms::source_node_for_surface, + config::{Action, StaticConfig}, + fl, + shell::{ + element::{CosmicMapped, CosmicSurface}, + focus::target::PointerFocusTarget, + grabs::ReleaseMode, + Shell, + }, + state::{BackendData, Common, State}, + utils::{ + iced::{IcedElement, Program}, + prelude::{Global, PointGlobalExt, PointLocalExt, SeatExt}, + }, +}; + +use super::ResizeEdge; + +mod item; + +pub struct MenuGrabState { + elements: Arc>>, +} +pub type SeatMenuGrabState = RefCell>; + +impl MenuGrabState { + pub fn render(&self, renderer: &mut R, output: &Output) -> Vec + where + R: Renderer + ImportMem, + ::TextureId: 'static, + I: From>, + { + let scale = output.current_scale().fractional_scale(); + self.elements + .lock() + .unwrap() + .iter() + .flat_map(|elem| { + elem.iced.render_elements( + renderer, + elem.position + .to_local(output) + .as_logical() + .to_physical_precise_round(scale), + scale.into(), + 1.0, + ) + }) + .collect() + } + + pub fn set_theme(&self, theme: cosmic::Theme) { + for element in &*self.elements.lock().unwrap() { + element.iced.set_theme(theme.clone()) + } + } +} + +#[derive(Clone)] +pub enum Item { + Separator, + Submenu { + title: String, + items: Vec, + }, + Entry { + title: String, + shortcut: Option, + on_press: Arc) + Send + Sync>>, + toggled: bool, + submenu: bool, + disabled: bool, + }, +} + +impl Item { + pub fn new, F: Fn(&LoopHandle<'_, State>) + Send + Sync + 'static>( + title: S, + on_press: F, + ) -> Item { + Item::Entry { + title: title.into(), + shortcut: None, + on_press: Arc::new(Box::new(on_press)), + toggled: false, + submenu: false, + disabled: false, + } + } + + pub fn new_submenu>(title: S, items: Vec) -> Item { + Item::Submenu { + title: title.into(), + items, + } + } + + pub fn shortcut(mut self, shortcut: impl Into>) -> Self { + if let Item::Entry { + shortcut: ref mut s, + .. + } = self + { + *s = shortcut.into(); + } + self + } + + pub fn toggled(mut self, toggled: bool) -> Self { + if let Item::Entry { + toggled: ref mut t, .. + } = self + { + *t = toggled; + } + self + } + + pub fn disabled(mut self, disabled: bool) -> Self { + if let Item::Entry { + disabled: ref mut d, + .. + } = self + { + *d = disabled; + } + self + } +} + +pub struct ContextMenu { + items: Vec, + selected: AtomicBool, + row_width: Mutex>, +} + +impl ContextMenu { + pub fn new(items: Vec) -> ContextMenu { + ContextMenu { + items, + selected: AtomicBool::new(false), + row_width: Mutex::new(None), + } + } +} + +#[derive(Debug, Clone)] +pub enum Message { + ItemEntered(usize, IcedRectangle), + ItemPressed(usize), + ItemLeft(usize, IcedRectangle), +} + +impl item::CursorEvents for Message { + fn cursor_entered(idx: usize, bounds: IcedRectangle) -> Self { + Message::ItemEntered(idx, bounds) + } + + fn cursor_left(idx: usize, bounds: IcedRectangle) -> Self { + Message::ItemLeft(idx, bounds) + } +} + +impl Program for ContextMenu { + type Message = Message; + + fn update( + &mut self, + message: Self::Message, + loop_handle: &LoopHandle<'static, crate::state::State>, + ) -> Command { + match message { + Message::ItemPressed(idx) => { + if let Some(Item::Entry { on_press, .. }) = self.items.get_mut(idx) { + (on_press)(loop_handle); + self.selected.store(true, Ordering::SeqCst); + } + } + Message::ItemEntered(idx, bounds) => { + if let Some(Item::Submenu { items, .. }) = self.items.get_mut(idx) { + let items = items.clone(); + let _ = loop_handle.insert_idle(move |state| { + let seat = state.common.last_active_seat(); + let grab_state = seat + .user_data() + .get::() + .unwrap() + .borrow_mut(); + + if let Some(grab_state) = &*grab_state { + let mut elements = grab_state.elements.lock().unwrap(); + let mut position = elements.last().unwrap().position; + position.x += bounds.width.ceil() as i32; + position.y += bounds.y.ceil() as i32; + + let element = IcedElement::new( + ContextMenu::new(items), + Size::default(), + state.common.event_loop_handle.clone(), + state.common.theme.clone(), + ); + let min_size = element.minimum_size(); + element.with_program(|p| { + *p.row_width.lock().unwrap() = Some(min_size.w as f32); + }); + element.resize(min_size); + element.output_enter(&seat.active_output(), element.bbox()); + + elements.push(Element { + iced: element, + position, + pointer_entered: false, + }) + } + }); + } + } + Message::ItemLeft(idx, _) => { + if let Some(Item::Submenu { .. }) = self.items.get_mut(idx) { + let _ = loop_handle.insert_idle(|state| { + let seat = state.common.last_active_seat(); + let grab_state = seat + .user_data() + .get::() + .unwrap() + .borrow_mut(); + + if let Some(grab_state) = &*grab_state { + let mut elements = grab_state.elements.lock().unwrap(); + elements.pop(); + } + }); + } + } + }; + + Command::none() + } + + fn view(&self) -> crate::utils::iced::Element<'_, Self::Message> { + let width = self + .row_width + .lock() + .unwrap() + .map(|size| Length::Fixed(size)) + .unwrap_or(Length::Shrink); + let mode = match width { + Length::Shrink => Length::Shrink, + _ => Length::Fill, + }; + + Column::with_children( + self.items + .iter() + .enumerate() + .map(|(idx, item)| match item { + Item::Separator => horizontal_rule(1) + .style(theme::Rule::LightDivider) + .width(Length::Shrink) + .into(), + Item::Submenu { title, .. } => Row::with_children(vec![ + horizontal_space(16).into(), + text(title).width(mode).into(), + from_name("go-next-symbolic") + .size(16) + .prefer_svg(true) + .icon() + .into(), + ]) + .spacing(8) + .width(width) + .padding([8, 24]) + .apply(|row| item::SubmenuItem::new(row, idx)) + .style(theme::Button::MenuItem) + .into(), + Item::Entry { + title, + shortcut, + toggled, + disabled, + .. + } => { + let mut components = vec![ + if *toggled { + from_name("object-select-symbolic") + .size(16) + .prefer_svg(true) + .icon() + .style(theme::Svg::custom(|theme| { + iced_widget::svg::Appearance { + color: Some(theme.cosmic().accent.base.into()), + } + })) + .into() + } else { + horizontal_space(16).into() + }, + text(title).width(mode).into(), + ]; + if let Some(shortcut) = shortcut.as_ref() { + components.push( + text(shortcut) + .line_height(20.) + .size(14) + .horizontal_alignment(Horizontal::Right) + .width(Length::Shrink) + .style(theme::Text::Custom(|theme| { + let mut color = theme.cosmic().background.component.on; + color.alpha *= 0.75; + TextAppearance { + color: Some(color.into()), + } + })) + .into(), + ); + } + components.push(horizontal_space(16).into()); + + Row::with_children(components) + .spacing(8) + .width(mode) + .apply(button) + .width(width) + .padding([8, 24]) + .on_press_maybe((!disabled).then_some(Message::ItemPressed(idx))) + .style(theme::Button::MenuItem) + .into() + } + }) + .collect(), + ) + .width(Length::Shrink) + .apply(iced_widget::container) + .padding(1) + .style(theme::Container::custom(|theme| { + let cosmic = theme.cosmic(); + let component = &cosmic.background.component; + iced_widget::container::Appearance { + icon_color: Some(cosmic.accent.base.into()), + text_color: Some(component.on.into()), + background: Some(Background::Color(component.base.into())), + border_radius: 8.0.into(), + border_width: 1.0, + border_color: component.divider.into(), + } + })) + .width(Length::Shrink) + .into() + } +} + +pub struct Element { + iced: IcedElement, + position: Point, + pointer_entered: bool, +} + +pub struct MenuGrab { + elements: Arc>>, + start_data: PointerGrabStartData, + seat: Seat, +} + +impl PointerGrab for MenuGrab { + fn motion( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &MotionEvent, + ) { + { + let mut guard = self.elements.lock().unwrap(); + let elements = &mut *guard; + if let Some(i) = elements.iter().position(|elem| { + let mut bbox = elem.iced.bbox(); + bbox.loc = elem.position.as_logical(); + + bbox.contains(event.location.to_i32_round()) + }) { + let element = &mut elements[i]; + + let new_event = MotionEvent { + location: event.location - element.position.as_logical().to_f64(), + serial: event.serial, + time: event.time, + }; + if !element.pointer_entered { + PointerTarget::enter(&element.iced, &self.seat, state, &new_event); + element.pointer_entered = true; + } else { + element.iced.motion(&self.seat, state, &new_event); + } + } else { + elements.iter_mut().for_each(|element| { + if element.pointer_entered { + PointerTarget::leave( + &element.iced, + &self.seat, + state, + event.serial, + event.time, + ); + element.pointer_entered = false; + } + }) + } + } + handle.motion(state, None, event); + } + + fn relative_motion( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &RelativeMotionEvent, + ) { + // While the grab is active, no client has pointer focus + handle.relative_motion(state, None, event); + } + + fn button( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &ButtonEvent, + ) { + let any_entered = self + .elements + .lock() + .unwrap() + .iter() + .any(|elem| elem.pointer_entered); + if !any_entered { + if event.state == ButtonState::Pressed { + handle.unset_grab(state, event.serial, event.time, true); + } + } else { + let selected = { + let elements = self.elements.lock().unwrap(); + let mut selected = false; + for element in elements.iter().filter(|elem| elem.pointer_entered) { + element.iced.button(&self.seat, state, event); + selected = true; + } + selected + }; + if selected && event.state == ButtonState::Released { + handle.unset_grab(state, event.serial, event.time, true); + } else { + handle.button(state, event); + } + } + } + + fn axis( + &mut self, + state: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + details: AxisFrame, + ) { + handle.axis(state, details); + } + + fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { + handle.frame(data) + } + + fn gesture_swipe_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeBeginEvent, + ) { + handle.gesture_swipe_begin(data, event) + } + + fn gesture_swipe_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeUpdateEvent, + ) { + handle.gesture_swipe_update(data, event) + } + + fn gesture_swipe_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureSwipeEndEvent, + ) { + handle.gesture_swipe_end(data, event) + } + + fn gesture_pinch_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchBeginEvent, + ) { + handle.gesture_pinch_begin(data, event) + } + + fn gesture_pinch_update( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchUpdateEvent, + ) { + handle.gesture_pinch_update(data, event) + } + + fn gesture_pinch_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GesturePinchEndEvent, + ) { + handle.gesture_pinch_end(data, event) + } + + fn gesture_hold_begin( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldBeginEvent, + ) { + handle.gesture_hold_begin(data, event) + } + + fn gesture_hold_end( + &mut self, + data: &mut State, + handle: &mut PointerInnerHandle<'_, State>, + event: &GestureHoldEndEvent, + ) { + handle.gesture_hold_end(data, event) + } + + fn start_data(&self) -> &PointerGrabStartData { + &self.start_data + } +} + +impl MenuGrab { + pub fn new( + start_data: PointerGrabStartData, + seat: &Seat, + items: impl Iterator, + position: Point, + handle: LoopHandle<'static, crate::state::State>, + theme: cosmic::Theme, + ) -> MenuGrab { + let items = items.collect::>(); + let element = IcedElement::new(ContextMenu::new(items), Size::default(), handle, theme); + let min_size = element.minimum_size(); + element.with_program(|p| { + *p.row_width.lock().unwrap() = Some(min_size.w as f32); + }); + element.resize(min_size); + + element.output_enter(&seat.active_output(), element.bbox()); + + let elements = Arc::new(Mutex::new(vec![Element { + iced: element, + position, + pointer_entered: false, + }])); + + let grab_state = MenuGrabState { + elements: elements.clone(), + }; + + *seat + .user_data() + .get::() + .unwrap() + .borrow_mut() = Some(grab_state); + + MenuGrab { + elements, + start_data, + seat: seat.clone(), + } + } +} + +impl Drop for MenuGrab { + fn drop(&mut self) { + self.seat + .user_data() + .get::() + .unwrap() + .borrow_mut() + .take(); + } +} + +pub fn window_items( + window: &CosmicMapped, + is_tiled: bool, + is_stacked: bool, + tiling_enabled: bool, + possible_resizes: ResizeEdge, + config: &StaticConfig, +) -> impl Iterator { + //let is_always_on_top = false; // TODO check window (potentially shell?) + //let is_always_on_visible_ws = false; // TODO check window (potentially shell?) + + let maximize_clone = window.clone(); + let tile_clone = window.clone(); + let move_prev_clone = window.clone(); + let move_next_clone = window.clone(); + let move_clone = window.clone(); + let resize_top_clone = window.clone(); + let resize_left_clone = window.clone(); + let resize_right_clone = window.clone(); + let resize_bottom_clone = window.clone(); + let unstack_clone = window.clone(); + let screenshot_clone = window.clone(); + let stack_clone = window.clone(); + let close_clone = window.clone(); + + vec![ + is_stacked.then_some(Item::new(fl!("window-menu-unstack"), move |handle| { + let unstack_clone = unstack_clone.clone(); + let _ = handle.insert_idle(move |state| { + let seat = state.common.last_active_seat().clone(); + let Some(ws) = state.common.shell.space_for_mut(&unstack_clone) else { return }; + if let Some(new_focus) = ws.toggle_stacking(&unstack_clone) { + Common::set_focus(state, Some(&new_focus), &seat, None); + } + }); + }) + .shortcut(config.get_shortcut_for_action(&Action::ToggleStacking))), + is_stacked.then_some(Item::Separator), + //Some(Item::new(fl!("window-menu-minimize"), |handle| {})), + Some(Item::new(fl!("window-menu-maximize"), move |handle| { + let maximize_clone = maximize_clone.clone(); + let _ = handle.insert_idle(move |state| { + if let Some(space) = state.common.shell.space_for_mut(&maximize_clone) { + if maximize_clone.is_maximized(false) { + space.unmaximize_request(&maximize_clone.active_window()); + } else { + space.maximize_request(&maximize_clone.active_window()); + } + } + }); + }) + .shortcut(config.get_shortcut_for_action(&Action::Maximize)) + .toggled(window.is_maximized(false))), + tiling_enabled.then_some(Item::new(fl!("window-menu-tiled"), move |handle| { + let tile_clone = tile_clone.clone(); + let _ = handle.insert_idle(move |state| { + let seat = state.common.last_active_seat().clone(); + if let Some(ws) = state.common.shell.space_for_mut(&tile_clone) { + ws.toggle_floating_window(&seat, &tile_clone); + } + }); + }) + .shortcut(config.get_shortcut_for_action(&Action::ToggleWindowFloating)) + .toggled(!is_tiled)), + Some(Item::Separator), + // TODO: Where to save? + Some(Item::new(fl!("window-menu-screenshot"), move |handle| { + let screenshot_clone = screenshot_clone.clone(); + let _ = handle.insert_idle(move |state| { + fn render_window( + renderer: &mut R, + window: &CosmicSurface, + offset: &time::UtcOffset, + ) -> anyhow::Result<()> + where + R: Renderer + + ImportAll + + Offscreen + + ExportMem, + ::TextureId: 'static, + ::Error: Send + Sync + 'static, + { + let bbox = bbox_from_surface_tree(&window.wl_surface().unwrap(), (0, 0)); + let elements = AsRenderElements::::render_elements::>( + window, + renderer, + (-bbox.loc.x, -bbox.loc.y).into(), + Scale::from(1.0), + 1.0, + ); + + // TODO: 10-bit + let format = Fourcc::Abgr8888; + let render_buffer = + Offscreen::::create_buffer(renderer, format, bbox.size.to_buffer(1, Transform::Normal))?; + renderer.bind(render_buffer)?; + let mut output_damage_tracker = OutputDamageTracker::new(bbox.size.to_physical(1), 1.0, Transform::Normal); + output_damage_tracker.render_output(renderer, 0, &elements, [0.0, 0.0, 0.0, 0.0]).map_err(|err| match err { + smithay::backend::renderer::damage::Error::Rendering(err) => err, + smithay::backend::renderer::damage::Error::OutputNoMode(_) => unreachable!(), + })?; + let mapping = renderer.copy_framebuffer(bbox.to_buffer(1, Transform::Normal, &bbox.size), format)?; + let gl_data = renderer.map_texture(&mapping)?; + + if let Ok(Some(path)) = xdg_user::pictures() { + let local_timestamp = time::OffsetDateTime::now_utc().to_offset(*offset); + let mut title = window.title(); + title.truncate(227); // 255 - time - png + let name = sanitize_filename::sanitize(format!("{}_{}.png", + title, + local_timestamp.format(time::macros::format_description!("[year]-[month]-[day]_[hour]:[minute]:[second]_[subsecond digits:4]")).unwrap(), + )); + let file = std::fs::File::create(path.join(name))?; + + let ref mut writer = std::io::BufWriter::new(file); + let mut encoder = png::Encoder::new(writer, bbox.size.w as u32, bbox.size.h as u32); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + encoder.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2)); // 1.0 / 2.2, unscaled, but rounded + let source_chromaticities = png::SourceChromaticities::new( // Using unscaled instantiation here + (0.31270, 0.32900), + (0.64000, 0.33000), + (0.30000, 0.60000), + (0.15000, 0.06000) + ); + encoder.set_source_chromaticities(source_chromaticities); + let mut writer = encoder.write_header()?; + writer.write_image_data(&gl_data)?; + } + + Ok(()) + } + + if let Some(surface) = screenshot_clone.active_window().wl_surface() { + let res = match &mut state.backend { + BackendData::Kms(kms) => { + let node = source_node_for_surface(&surface, &state.common.display_handle) + .unwrap_or(kms.primary); + kms + .api + .single_renderer(&node) + .with_context(|| "Failed to get renderer for screenshot") + .and_then(|mut multirenderer| render_window(&mut multirenderer, &screenshot_clone.active_window(), &state.common.local_offset)) + }, + BackendData::Winit(winit) => { + render_window(winit.backend.renderer(), &screenshot_clone.active_window(), &state.common.local_offset) + }, + BackendData::X11(x11) => { + render_window(&mut x11.renderer, &screenshot_clone.active_window(), &state.common.local_offset) + }, + BackendData::Unset => unreachable!(), + }; + if let Err(err) = res { + warn!(?err, "Failed to take screenshot") + } + } + }); + })), + Some(Item::Separator), + Some(Item::new(fl!("window-menu-move"), move |handle| { + let move_clone = move_clone.clone(); + let _ = handle.insert_idle(move |state| { + if let Some(surface) = move_clone.wl_surface() { + let seat = state.common.last_active_seat().clone(); + Shell::move_request(state, &surface, &seat, None, ReleaseMode::Click); + } + }); + })), + Some(Item::new_submenu(fl!("window-menu-resize"), vec![ + Item::new(fl!("window-menu-resize-edge-top"), move |handle| { + let resize_clone = resize_top_clone.clone(); + let _ = handle.insert_idle(move |state| { + let seat = state.common.last_active_seat().clone(); + Shell::menu_resize_request(state, &resize_clone, &seat, ResizeEdge::TOP); + }); + }).disabled(!possible_resizes.contains(ResizeEdge::TOP)), + Item::new(fl!("window-menu-resize-edge-left"), move |handle| { + let resize_clone = resize_left_clone.clone(); + let _ = handle.insert_idle(move |state| { + let seat = state.common.last_active_seat().clone(); + Shell::menu_resize_request(state, &resize_clone, &seat, ResizeEdge::LEFT); + }); + }).disabled(!possible_resizes.contains(ResizeEdge::LEFT)), + Item::new(fl!("window-menu-resize-edge-right"), move |handle| { + let resize_clone = resize_right_clone.clone(); + let _ = handle.insert_idle(move |state| { + let seat = state.common.last_active_seat().clone(); + Shell::menu_resize_request(state, &resize_clone, &seat, ResizeEdge::RIGHT); + }); + }).disabled(!possible_resizes.contains(ResizeEdge::RIGHT)), + Item::new(fl!("window-menu-resize-edge-bottom"), move |handle| { + let resize_clone = resize_bottom_clone.clone(); + let _ = handle.insert_idle(move |state| { + let seat = state.common.last_active_seat().clone(); + Shell::menu_resize_request(state, &resize_clone, &seat, ResizeEdge::BOTTOM); + }); + }).disabled(!possible_resizes.contains(ResizeEdge::BOTTOM)), + ])), + Some(Item::new(fl!("window-menu-move-prev-workspace"), move |handle| { + let move_prev_clone = move_prev_clone.clone(); + let _ = handle.insert_idle(move |state| { + let seat = state.common.last_active_seat().clone(); + let (current_handle, output) = { + let Some(ws) = state.common.shell.space_for(&move_prev_clone) else { return }; + (ws.handle, ws.output.clone()) + }; + let maybe_handle = state + .common + .shell + .workspaces + .spaces_for_output(&output) + .enumerate() + .find_map(|(i, space)| (space.handle == current_handle).then_some(i)) + .and_then(|i| i.checked_sub(1)) + .and_then(|i| { + state + .common + .shell + .workspaces + .get(i, &output) + .map(|s| s.handle) + }); + if let Some(prev_handle) = maybe_handle + { + Shell::move_window( + state, + &seat, + &move_prev_clone, + ¤t_handle, + &prev_handle, + true, + None, + ); + } + }); + }) + .shortcut(config.get_shortcut_for_action(&Action::MoveToPreviousWorkspace))), + Some(Item::new(fl!("window-menu-move-next-workspace"), move |handle| { + let move_next_clone = move_next_clone.clone(); + let _ = handle.insert_idle(move |state| { + let seat = state.common.last_active_seat().clone(); + let (current_handle, output) = { + let Some(ws) = state.common.shell.space_for(&move_next_clone) else { return }; + (ws.handle, ws.output.clone()) + }; + let maybe_handle = state + .common + .shell + .workspaces + .spaces_for_output(&output) + .skip_while(|space| space.handle != current_handle) + .skip(1) + .next() + .map(|space| space.handle); + if let Some(next_handle) = maybe_handle + { + Shell::move_window( + state, + &seat, + &move_next_clone, + ¤t_handle, + &next_handle, + true, + None, + ); + } + }); + }) + .shortcut(config.get_shortcut_for_action(&Action::MoveToNextWorkspace))), + (!is_stacked).then_some(Item::new(fl!("window-menu-stack"), move |handle| { + let stack_clone = stack_clone.clone(); + let _ = handle.insert_idle(move |state| { + let seat = state.common.last_active_seat().clone(); + let Some(ws) = state.common.shell.space_for_mut(&stack_clone) else { return }; + if let Some(new_focus) = ws.toggle_stacking(&stack_clone) { + Common::set_focus(state, Some(&new_focus), &seat, None); + } + }); + }) + .shortcut(config.get_shortcut_for_action(&Action::ToggleStacking))), + Some(Item::Separator), + //Some(Item::new(fl!("window-menu-always-on-top"), |handle| {}).toggled(is_always_on_top)), + //Some(Item::new(fl!("window-menu-always-on-visible-ws"), |handle| {}) + // .toggled(is_always_on_visible_ws)), + //Some(Item::Separator), + if is_stacked { + Some(Item::new(fl!("window-menu-close-all"), move |_handle| { + for (window, _) in close_clone.windows() { + window.close(); + } + })) + } else { + Some(Item::new(fl!("window-menu-close"), move |_handle| { + close_clone.send_close(); + }) + .shortcut(config.get_shortcut_for_action(&Action::Close))) + }, + ].into_iter().flatten() +} diff --git a/src/shell/grabs/mod.rs b/src/shell/grabs/mod.rs index 582b98c5..c78f8b7a 100644 --- a/src/shell/grabs/mod.rs +++ b/src/shell/grabs/mod.rs @@ -22,6 +22,9 @@ pub enum ReleaseMode { Click, NoMouseButtons, } + +mod menu; +pub use self::menu::*; mod moving; pub use self::moving::*; diff --git a/src/shell/layout/tiling/mod.rs b/src/shell/layout/tiling/mod.rs index 66edaa08..c717d61b 100644 --- a/src/shell/layout/tiling/mod.rs +++ b/src/shell/layout/tiling/mod.rs @@ -2209,6 +2209,43 @@ impl TilingLayout { edges } + pub fn menu_resize( + &self, + mut node_id: NodeId, + edge: ResizeEdge, + ) -> Option<(NodeId, usize, Orientation)> { + let tree = self.tree(); + + while let Some(group_id) = tree.get(&node_id).unwrap().parent().cloned() { + let orientation = tree.get(&group_id).unwrap().data().orientation(); + let node_idx = tree + .children_ids(&group_id) + .unwrap() + .position(|id| id == &node_id) + .unwrap(); + let total = tree.children_ids(&group_id).unwrap().count(); + if orientation == Orientation::Vertical { + if node_idx > 0 && edge.contains(ResizeEdge::LEFT) { + return Some((group_id, node_idx - 1, orientation)); + } + if node_idx < total - 1 && edge.contains(ResizeEdge::RIGHT) { + return Some((group_id, node_idx, orientation)); + } + } else { + if node_idx > 0 && edge.contains(ResizeEdge::TOP) { + return Some((group_id, node_idx - 1, orientation)); + } + if node_idx < total - 1 && edge.contains(ResizeEdge::BOTTOM) { + return Some((group_id, node_idx, orientation)); + } + } + + node_id = group_id; + } + + None + } + pub fn resize( &mut self, focused: &KeyboardFocusTarget, diff --git a/src/shell/mod.rs b/src/shell/mod.rs index aa1806b7..0a35a4ff 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -15,7 +15,7 @@ use smithay::{ layer_map_for_output, space::SpaceElement, LayerSurface, PopupManager, WindowSurfaceType, }, input::{ - pointer::{Focus, GrabStartData as PointerGrabStartData}, + pointer::{Focus, GrabStartData as PointerGrabStartData, MotionEvent}, Seat, }, output::Output, @@ -26,7 +26,7 @@ use smithay::{ }, wayland_server::{protocol::wl_surface::WlSurface, Client, DisplayHandle}, }, - utils::{Point, Rectangle, Serial, SERIAL_COUNTER}, + utils::{Logical, Point, Rectangle, Serial, SERIAL_COUNTER}, wayland::{ compositor::with_states, seat::WaylandFocus, @@ -74,8 +74,11 @@ use self::{ CosmicWindow, }, focus::target::KeyboardFocusTarget, - grabs::ResizeEdge, - layout::{floating::ResizeState, tiling::NodeDesc}, + grabs::{window_items, MenuGrab, ReleaseMode, ResizeEdge, ResizeGrab}, + layout::{ + floating::ResizeState, + tiling::{NodeDesc, ResizeForkGrab, TilingLayout}, + }, }; const ANIMATION_DURATION: Duration = Duration::from_millis(200); @@ -1008,6 +1011,7 @@ impl Workspaces { } } +#[derive(Debug)] pub struct InvalidWorkspaceIndex; impl Shell { @@ -1018,7 +1022,11 @@ impl Shell { ); let xdg_shell_state = XdgShellState::new_with_capabilities::( dh, - [WmCapabilities::Fullscreen, WmCapabilities::Maximize], + [ + WmCapabilities::Fullscreen, + WmCapabilities::Maximize, + WmCapabilities::WindowMenu, + ], ); let xdg_activation_state = XdgActivationState::new::(dh); let toplevel_info_state = @@ -1708,10 +1716,27 @@ impl Shell { follow: bool, direction: Option, ) -> Option> { - let from_output = state.common.shell.space_for_handle(from)?.output.clone(); - let to_output = state.common.shell.space_for_handle(to)?.output.clone(); + let from_output = state + .common + .shell + .workspaces + .space_for_handle(from)? + .output + .clone(); + let to_output = state + .common + .shell + .workspaces + .space_for_handle(to)? + .output + .clone(); - let from_workspace = state.common.shell.space_for_handle_mut(from).unwrap(); // checked above + let from_workspace = state + .common + .shell + .workspaces + .space_for_handle_mut(from) + .unwrap(); // checked above let window_state = from_workspace.unmap(mapped)?; let elements = from_workspace.mapped().cloned().collect::>(); @@ -1744,7 +1769,12 @@ impl Shell { None }; - let mut to_workspace = state.common.shell.space_for_handle_mut(to).unwrap(); // checked above + let mut to_workspace = state + .common + .shell + .workspaces + .space_for_handle_mut(to) + .unwrap(); // checked above let focus_stack = to_workspace.focus_stack.get(&seat); if window_state.layer == ManagedLayer::Floating { to_workspace.floating_layer.map(mapped.clone(), None); @@ -1774,7 +1804,12 @@ impl Shell { &new_workspace_handle, layer, ); - to_workspace = state.common.shell.space_for_handle_mut(to).unwrap(); + to_workspace = state + .common + .shell + .workspaces + .space_for_handle_mut(to) + .unwrap(); // checked above } } @@ -1878,6 +1913,75 @@ impl Shell { } } + pub fn menu_request( + state: &mut State, + surface: &WlSurface, + seat: &Seat, + serial: impl Into>, + location: Point, + ) { + let serial = serial.into(); + if let Some(start_data) = + check_grab_preconditions(&seat, surface, serial, ReleaseMode::NoMouseButtons) + { + if let Some(mapped) = state.common.shell.element_for_wl_surface(surface).cloned() { + if let Some(workspace) = state.common.shell.space_for_mut(&mapped) { + let output = seat.active_output(); + let (_, relative_loc) = mapped + .windows() + .find(|(w, _)| w.wl_surface().as_ref() == Some(surface)) + .unwrap(); + + let global_position = (workspace.element_geometry(&mapped).unwrap().loc + + relative_loc.as_local() + + location.as_local()) + .to_global(&output); + let is_tiled = workspace.is_tiled(&mapped); + let edge = if is_tiled { + mapped + .tiling_node_id + .lock() + .unwrap() + .clone() + .map(|node_id| { + TilingLayout::possible_resizes( + workspace.tiling_layer.tree(), + node_id, + ) + }) + .unwrap_or(ResizeEdge::empty()) + } else { + ResizeEdge::all() + }; + let is_stacked = mapped.is_stack(); + let tiling_enabled = workspace.tiling_enabled; + let grab = MenuGrab::new( + start_data, + seat, + window_items( + &mapped, + is_tiled, + is_stacked, + tiling_enabled, + edge, + &state.common.config.static_conf, + ) + .into_iter(), + global_position, + state.common.event_loop_handle.clone(), + state.common.theme.clone(), + ); + seat.get_pointer().unwrap().set_grab( + state, + grab, + serial.unwrap_or_else(|| SERIAL_COUNTER.next_serial()), + Focus::Keep, + ); + } + } + } + } + pub fn move_request( state: &mut State, surface: &WlSurface, @@ -1933,6 +2037,81 @@ impl Shell { } } + pub fn menu_resize_request( + state: &mut State, + mapped: &CosmicMapped, + seat: &Seat, + edge: ResizeEdge, + ) { + let Some(surface) = mapped.active_window().wl_surface() else { return }; + if let Some(start_data) = + check_grab_preconditions(&seat, &surface, None, ReleaseMode::Click) + { + if let Some(ws) = state.common.shell.space_for_mut(mapped) { + let geometry = ws.element_geometry(mapped).unwrap().to_global(ws.output()); + + let new_loc = if edge.contains(ResizeEdge::LEFT) { + Point::::from(( + geometry.loc.x, + geometry.loc.y + (geometry.size.h / 2), + )) + } else if edge.contains(ResizeEdge::RIGHT) { + Point::::from(( + geometry.loc.x + geometry.size.w, + geometry.loc.y + (geometry.size.h / 2), + )) + } else if edge.contains(ResizeEdge::TOP) { + Point::::from(( + geometry.loc.x + (geometry.size.w / 2), + geometry.loc.y, + )) + } else if edge.contains(ResizeEdge::BOTTOM) { + Point::::from(( + geometry.loc.x + (geometry.size.w / 2), + geometry.loc.y + geometry.size.h, + )) + } else { + return; + }; + + let grab: ResizeGrab = if ws.is_floating(mapped) { + let Some(grab) = ws.floating_layer.resize_request(mapped, seat, start_data, edge, ReleaseMode::Click) else { + return + }; + grab.into() + } else { + let Some(node_id) = mapped.tiling_node_id.lock().unwrap().clone() else { return }; + let Some((node, left_up_idx, orientation)) = + ws.tiling_layer.menu_resize(node_id, edge) else { return }; + ResizeForkGrab::new( + start_data, + new_loc.to_f64(), + node, + left_up_idx, + orientation, + ws.output.downgrade(), + ReleaseMode::Click, + ) + .into() + }; + + let ptr = seat.get_pointer().unwrap(); + let serial = SERIAL_COUNTER.next_serial(); + ptr.motion( + state, + Some((mapped.clone().into(), (new_loc - geometry.loc).as_logical())), + &MotionEvent { + location: new_loc.as_logical().to_f64(), + serial, + time: 0, + }, + ); + ptr.frame(state); + ptr.set_grab(state, grab, serial, Focus::Keep); + } + } + } + pub fn resize_request( state: &mut State, surface: &WlSurface, diff --git a/src/state.rs b/src/state.rs index 48da8300..2da7c2e2 100644 --- a/src/state.rs +++ b/src/state.rs @@ -84,6 +84,7 @@ use smithay::{ xwayland_keyboard_grab::XWaylandKeyboardGrabState, }, }; +use time::UtcOffset; use tracing::error; use std::{cell::RefCell, ffi::OsString, time::Duration}; @@ -144,6 +145,7 @@ pub struct Common { pub clock: Clock, pub should_stop: bool, + pub local_offset: time::UtcOffset, pub theme: cosmic::Theme, @@ -332,6 +334,7 @@ impl State { .with_context(|| "Failed to load languages") .unwrap(); + let local_offset = UtcOffset::current_local_offset().expect("No yet multithreaded"); let clock = Clock::new(); let config = Config::load(&handle); let compositor_state = CompositorState::new::(dh); @@ -384,6 +387,7 @@ impl State { seats: Vec::new(), last_active_seat: None, + local_offset, clock, should_stop: false, diff --git a/src/wayland/handlers/xdg_shell/mod.rs b/src/wayland/handlers/xdg_shell/mod.rs index d72fa567..cb891476 100644 --- a/src/wayland/handlers/xdg_shell/mod.rs +++ b/src/wayland/handlers/xdg_shell/mod.rs @@ -17,7 +17,7 @@ use smithay::{ wayland_protocols::xdg::shell::server::xdg_toplevel, wayland_server::protocol::{wl_output::WlOutput, wl_seat::WlSeat}, }, - utils::Serial, + utils::{Logical, Point, Serial}, wayland::{ seat::WaylandFocus, shell::xdg::{ @@ -359,6 +359,17 @@ impl XdgShellHandler for State { ); } } + + fn show_window_menu( + &mut self, + surface: ToplevelSurface, + seat: WlSeat, + serial: Serial, + location: Point, + ) { + let seat = Seat::from_resource(&seat).unwrap(); + Shell::menu_request(self, surface.wl_surface(), &seat, serial, location) + } } delegate_xdg_shell!(State);