diff --git a/src/shell/grabs/menu/mod.rs b/src/shell/grabs/menu/mod.rs index c1ca0c2b..4b7a516a 100644 --- a/src/shell/grabs/menu/mod.rs +++ b/src/shell/grabs/menu/mod.rs @@ -1,6 +1,9 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, Mutex, +use std::{ + fmt, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, }; use calloop::LoopHandle; @@ -14,7 +17,7 @@ use cosmic::{ }; use smithay::{ backend::{ - input::ButtonState, + input::{ButtonState, TouchSlot}, renderer::{ element::{memory::MemoryRenderBufferRenderElement, AsRenderElements}, ImportMem, Renderer, @@ -26,26 +29,29 @@ use smithay::{ AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, - GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab, PointerInnerHandle, - PointerTarget, RelativeMotionEvent, + GrabStartData as PointerGrabStartData, MotionEvent as PointerMotionEvent, PointerGrab, + PointerInnerHandle, PointerTarget, RelativeMotionEvent, + }, + touch::{ + DownEvent, GrabStartData as TouchGrabStartData, MotionEvent as TouchMotionEvent, + TouchGrab, TouchInnerHandle, TouchTarget, UpEvent, }, Seat, }, output::Output, - utils::{Logical, Point, Rectangle, Size}, + utils::{Logical, Point, Rectangle, Serial, Size}, }; use crate::{ - shell::focus::target::PointerFocusTarget, - shell::SeatExt, + shell::{focus::target::PointerFocusTarget, SeatExt}, state::State, utils::{ iced::{IcedElement, Program}, - prelude::{Global, OutputExt, PointGlobalExt, PointLocalExt, SizeExt}, + prelude::*, }, }; -use super::ResizeEdge; +use super::{GrabStartData, ResizeEdge}; mod default; mod item; @@ -53,6 +59,7 @@ pub use self::default::*; pub struct MenuGrabState { elements: Arc>>, + screen_space_relative: Option, } pub type SeatMenuGrabState = Mutex>; @@ -82,6 +89,10 @@ impl MenuGrabState { .collect() } + pub fn is_in_screen_space(&self) -> bool { + self.screen_space_relative.is_some() + } + pub fn set_theme(&self, theme: cosmic::Theme) { for element in &*self.elements.lock().unwrap() { element.iced.set_theme(theme.clone()) @@ -106,6 +117,35 @@ pub enum Item { }, } +impl fmt::Debug for Item { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Separator => write!(f, "Separator"), + Self::Submenu { title, items } => f + .debug_struct("Submenu") + .field("title", title) + .field("items", items) + .finish(), + Self::Entry { + title, + shortcut, + on_press: _, + toggled, + submenu, + disabled, + } => f + .debug_struct("Entry") + .field("title", title) + .field("shortcut", shortcut) + .field("on_press", &"...") + .field("toggled", toggled) + .field("submenu", submenu) + .field("disabled", disabled) + .finish(), + } + } +} + impl Item { pub fn new, F: Fn(&LoopHandle<'_, State>) + Send + Sync + 'static>( title: S, @@ -162,6 +202,7 @@ impl Item { } /// Menu that comes up when right-clicking an application header bar +#[derive(Debug)] pub struct ContextMenu { items: Vec, selected: AtomicBool, @@ -176,6 +217,10 @@ impl ContextMenu { row_width: Mutex::new(None), } } + + pub fn set_row_width(&self, width: f32) { + *self.row_width.lock().unwrap() = Some(width); + } } #[derive(Debug, Clone)] @@ -202,6 +247,7 @@ impl Program for ContextMenu { &mut self, message: Self::Message, loop_handle: &LoopHandle<'static, crate::state::State>, + last_seat: Option<&(Seat, Serial)>, ) -> Task { match message { Message::ItemPressed(idx) => { @@ -209,124 +255,118 @@ impl Program for ContextMenu { (on_press)(loop_handle); self.selected.store(true, Ordering::SeqCst); } + // TODO: If Submenu, then also expand on "Pressed" for touch events. + // But right now we don't have any touch responsive menus with submenus } 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 - .shell - .read() - .unwrap() - .seats - .last_active() - .clone(); - let grab_state = seat - .user_data() - .get::() - .unwrap() - .lock() - .unwrap(); + if let Some((seat, _)) = last_seat.cloned() { + let items = items.clone(); + let _ = loop_handle.insert_idle(move |state| { + let grab_state = seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); - if let Some(grab_state) = &*grab_state { - let mut elements = grab_state.elements.lock().unwrap(); + if let Some(grab_state) = &*grab_state { + let mut elements = grab_state.elements.lock().unwrap(); - let position = elements.last().unwrap().position; - let element = IcedElement::new( - ContextMenu::new(items), - Size::default(), - state.common.event_loop_handle.clone(), - state.common.theme.clone(), - ); + let position = elements.last().unwrap().position; + 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); + 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); - let output = seat.active_output(); - let position = [ - // to the right -> down - Rectangle::new( - position - + Point::from(( - bounds.width.ceil() as i32, - bounds.y.ceil() as i32, - )), - min_size.as_global(), - ), - // to the right -> up - Rectangle::new( - position - + Point::from(( - bounds.width.ceil() as i32, - bounds.y.ceil() as i32 + bounds.height.ceil() as i32 - - min_size.h, - )), - min_size.as_global(), - ), - // to the left -> down - Rectangle::new( - position + Point::from((-min_size.w, bounds.y.ceil() as i32)), - min_size.as_global(), - ), - // to the left -> up - Rectangle::new( - position - + Point::from(( - -min_size.w, - bounds.y.ceil() as i32 + bounds.height.ceil() as i32 - - min_size.h, - )), - min_size.as_global(), - ), - ] - .iter() - .rev() // preference of max_by_key is backwards - .max_by_key(|rect| { - output - .geometry() - .intersection(**rect) - .map(|rect| rect.size.w * rect.size.h) - }) - .unwrap() - .loc; - element.output_enter(&output, element.bbox()); + let output = seat.active_output(); + let position = [ + // to the right -> down + Rectangle::new( + position + + Point::from(( + bounds.width.ceil() as i32, + bounds.y.ceil() as i32, + )), + min_size.as_global(), + ), + // to the right -> up + Rectangle::new( + position + + Point::from(( + bounds.width.ceil() as i32, + bounds.y.ceil() as i32 + + bounds.height.ceil() as i32 + - min_size.h, + )), + min_size.as_global(), + ), + // to the left -> down + Rectangle::new( + position + + Point::from((-min_size.w, bounds.y.ceil() as i32)), + min_size.as_global(), + ), + // to the left -> up + Rectangle::new( + position + + Point::from(( + -min_size.w, + bounds.y.ceil() as i32 + + bounds.height.ceil() as i32 + - min_size.h, + )), + min_size.as_global(), + ), + ] + .iter() + .rev() // preference of max_by_key is backwards + .max_by_key(|rect| { + output + .geometry() + .intersection(**rect) + .map(|rect| rect.size.w * rect.size.h) + }) + .unwrap() + .loc; + element.output_enter(&output, element.bbox()); - elements.push(Element { - iced: element, - position, - pointer_entered: false, - }) - } - }); + elements.push(Element { + iced: element, + position, + pointer_entered: false, + touch_entered: None, + }) + } + }); + } } } Message::ItemLeft(idx, _) => { if let Some(Item::Submenu { .. }) = self.items.get_mut(idx) { - let _ = loop_handle.insert_idle(|state| { - let seat = state - .common - .shell - .read() - .unwrap() - .seats - .last_active() - .clone(); - let grab_state = seat - .user_data() - .get::() - .unwrap() - .lock() - .unwrap(); + if let Some((seat, _)) = last_seat.cloned() { + let _ = loop_handle.insert_idle(move |_| { + let grab_state = seat + .user_data() + .get::() + .unwrap() + .lock() + .unwrap(); - if let Some(grab_state) = &*grab_state { - let mut elements = grab_state.elements.lock().unwrap(); - elements.pop(); - } - }); + if let Some(grab_state) = &*grab_state { + let mut elements = grab_state.elements.lock().unwrap(); + elements.pop(); + } + }); + } } } }; @@ -457,12 +497,14 @@ pub struct Element { iced: IcedElement, position: Point, pointer_entered: bool, + touch_entered: Option, } pub struct MenuGrab { elements: Arc>>, - start_data: PointerGrabStartData, + start_data: GrabStartData, seat: Seat, + screen_space_relative: Option, } impl PointerGrab for MenuGrab { @@ -471,21 +513,36 @@ impl PointerGrab for MenuGrab { state: &mut State, handle: &mut PointerInnerHandle<'_, State>, _focus: Option<(PointerFocusTarget, Point)>, - event: &MotionEvent, + event: &PointerMotionEvent, ) { { let mut guard = self.elements.lock().unwrap(); let elements = &mut *guard; + let event_location = if let Some(output) = self.screen_space_relative.as_ref() { + if let Some(zoom_state) = state.common.shell.read().unwrap().zoom_state() { + event + .location + .as_global() + .to_zoomed(output, zoom_state.level) + .to_global(output) + .as_logical() + } else { + event.location + } + } else { + event.location + }; + 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()) + 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(), + let new_event = PointerMotionEvent { + location: event_location - element.position.as_logical().to_f64(), serial: event.serial, time: event.time, }; @@ -493,7 +550,7 @@ impl PointerGrab for MenuGrab { PointerTarget::enter(&element.iced, &self.seat, state, &new_event); element.pointer_entered = true; } else { - element.iced.motion(&self.seat, state, &new_event); + PointerTarget::motion(&element.iced, &self.seat, state, &new_event); } } else { elements.iter_mut().for_each(|element| { @@ -545,7 +602,7 @@ impl PointerGrab for MenuGrab { 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); + PointerTarget::button(&element.iced, &self.seat, state, event); selected = true; } selected @@ -644,18 +701,224 @@ impl PointerGrab for MenuGrab { } fn start_data(&self) -> &PointerGrabStartData { - &self.start_data + match &self.start_data { + GrabStartData::Pointer(start_data) => start_data, + _ => unreachable!(), + } } fn unset(&mut self, _data: &mut State) {} } +impl TouchGrab for MenuGrab { + fn down( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &DownEvent, + seq: Serial, + ) { + { + let mut guard = self.elements.lock().unwrap(); + let elements = &mut *guard; + let event_location = if let Some(output) = self.screen_space_relative.as_ref() { + if let Some(zoom_state) = data.common.shell.read().unwrap().zoom_state() { + event + .location + .as_global() + .to_zoomed(output, zoom_state.level) + .to_global(output) + .as_logical() + } else { + event.location + } + } else { + event.location + }; + + 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 = DownEvent { + slot: event.slot, + location: event_location - element.position.as_logical().to_f64(), + serial: event.serial, + time: event.time, + }; + if element.touch_entered.is_none() { + TouchTarget::down(&element.iced, &self.seat, data, &new_event, seq); + element.touch_entered = Some(event.slot); + } + } + } + handle.down(data, None, event, seq); + } + + fn up( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &UpEvent, + seq: Serial, + ) { + { + let elements = self.elements.lock().unwrap(); + for element in elements.iter().filter(|elem| { + elem.touch_entered + .as_ref() + .is_some_and(|slot| *slot == event.slot) + }) { + TouchTarget::up(&element.iced, &self.seat, data, event, seq); + } + } + handle.unset_grab(self, data); + } + + fn motion( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + _focus: Option<(PointerFocusTarget, Point)>, + event: &TouchMotionEvent, + seq: Serial, + ) { + { + let elements = self.elements.lock().unwrap(); + for element in elements.iter().filter(|elem| { + elem.touch_entered + .as_ref() + .is_some_and(|slot| *slot == event.slot) + }) { + TouchTarget::motion(&element.iced, &self.seat, data, event, seq); + } + } + handle.motion(data, None, event, seq); + } + + fn frame(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) { + handle.frame(data, seq); + } + + fn cancel(&mut self, data: &mut State, handle: &mut TouchInnerHandle<'_, State>, seq: Serial) { + { + let mut elements = self.elements.lock().unwrap(); + for element in elements.iter_mut() { + let _ = element.touch_entered.take(); + } + } + handle.cancel(data, seq); + } + + fn shape( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &smithay::input::touch::ShapeEvent, + seq: Serial, + ) { + handle.shape(data, event, seq); + } + + fn orientation( + &mut self, + data: &mut State, + handle: &mut TouchInnerHandle<'_, State>, + event: &smithay::input::touch::OrientationEvent, + seq: Serial, + ) { + handle.orientation(data, event, seq); + } + + fn start_data(&self) -> &TouchGrabStartData { + match &self.start_data { + GrabStartData::Touch(start_data) => start_data, + _ => unreachable!(), + } + } + + fn unset(&mut self, _data: &mut State) {} +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MenuAlignment { + pub x: AxisAlignment, + pub y: AxisAlignment, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AxisAlignment { + Corner, + Centered, +} + +impl MenuAlignment { + pub const CORNER: Self = MenuAlignment { + x: AxisAlignment::Corner, + y: AxisAlignment::Corner, + }; + pub const CENTERED: Self = MenuAlignment { + x: AxisAlignment::Centered, + y: AxisAlignment::Centered, + }; + pub const HORIZONTALLY_CENTERED: Self = MenuAlignment { + x: AxisAlignment::Centered, + y: AxisAlignment::Corner, + }; + pub const VERTICALLY_CENTERED: Self = MenuAlignment { + x: AxisAlignment::Corner, + y: AxisAlignment::Centered, + }; + + fn rectangles( + &self, + position: Point, + size: Size, + ) -> Vec> { + match (self.x, self.y) { + (AxisAlignment::Corner, AxisAlignment::Corner) => vec![ + Rectangle::new(position, size), // normal + Rectangle::new(position - Point::from((size.w, 0)), size), // flipped left + Rectangle::new(position - Point::from((0, size.h)), size), // flipped up + Rectangle::new(position - size.to_point(), size), // flipped left & up + ], + (AxisAlignment::Centered, AxisAlignment::Corner) => { + let x = position.x - ((size.w as f64 / 2.).round() as i32); + vec![ + Rectangle::new(Point::from((x, position.y)), size), // below + Rectangle::new(Point::from((x, position.y - size.h)), size), // above + ] + } + (AxisAlignment::Corner, AxisAlignment::Centered) => { + let y = position.y - ((size.h as f64 / 2.).round() as i32); + vec![ + Rectangle::new(Point::from((position.x, y)), size), // left + Rectangle::new(Point::from((position.x - size.w, y)), size), // right + ] + } + (AxisAlignment::Centered, AxisAlignment::Centered) => { + vec![Rectangle::new( + position - size.to_f64().downscale(2.).to_i32_round().to_point(), + size, + )] + } + } + } +} + impl MenuGrab { pub fn new( - start_data: PointerGrabStartData, + start_data: GrabStartData, seat: &Seat, items: impl Iterator, position: Point, + alignment: MenuAlignment, + screen_space_relative: bool, handle: LoopHandle<'static, crate::state::State>, theme: cosmic::Theme, ) -> MenuGrab { @@ -668,31 +931,18 @@ impl MenuGrab { element.resize(min_size); let output = seat.active_output(); - let position = [ - Rectangle::new(position, min_size.as_global()), // normal - Rectangle::new( - position - Point::from((min_size.w, 0)), - min_size.as_global(), - ), // flipped left - Rectangle::new( - position - Point::from((0, min_size.h)), - min_size.as_global(), - ), // flipped up - Rectangle::new( - position - Point::from((min_size.w, min_size.h)), - min_size.as_global(), - ), // flipped left & up - ] - .iter() - .rev() // preference of max_by_key is backwards - .max_by_key(|rect| { - output - .geometry() - .intersection(**rect) - .map(|rect| rect.size.w * rect.size.h) - }) - .unwrap() - .loc; + let position = alignment + .rectangles(position, min_size.as_global()) + .iter() + .rev() // preference of max_by_key is backwards + .max_by_key(|rect| { + output + .geometry() + .intersection(**rect) + .map(|rect| rect.size.w * rect.size.h) + }) + .unwrap() + .loc; element.output_enter(&output, element.bbox()); @@ -700,10 +950,14 @@ impl MenuGrab { iced: element, position, pointer_entered: false, + touch_entered: None, }])); + let screen_space_relative = screen_space_relative.then_some(output); + let grab_state = MenuGrabState { elements: elements.clone(), + screen_space_relative: screen_space_relative.clone(), }; *seat @@ -717,6 +971,14 @@ impl MenuGrab { elements, start_data, seat: seat.clone(), + screen_space_relative, + } + } + + pub fn is_touch_grab(&self) -> bool { + match self.start_data { + GrabStartData::Touch(_) => true, + GrabStartData::Pointer(_) => false, } } } diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 8277bfe6..09d39477 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -1,6 +1,6 @@ use calloop::LoopHandle; use focus::target::WindowGroup; -use grabs::SeatMoveGrabState; +use grabs::{MenuAlignment, SeatMoveGrabState}; use indexmap::IndexMap; use layout::TilingExceptions; use std::{ @@ -2900,9 +2900,9 @@ impl Shell { ) -> Option<(MenuGrab, Focus)> { let serial = serial.into(); let Some(GrabStartData::Pointer(start_data)) = - check_grab_preconditions(&seat, surface, serial, true) + check_grab_preconditions(&seat, serial, Some(surface)) else { - return None; + return None; // TODO: an application can send a menu request for a touch event }; let mapped = self.element_for_surface(surface).cloned()?; @@ -2966,7 +2966,7 @@ impl Shell { }; let grab = MenuGrab::new( - start_data, + GrabStartData::Pointer(start_data), seat, if target_stack || !is_stacked { Box::new(window_items( @@ -2987,6 +2987,8 @@ impl Shell { as Box> }, global_position, + MenuAlignment::CORNER, + false, evlh.clone(), self.theme.clone(), ); @@ -3008,7 +3010,8 @@ impl Shell { ) -> Option<(MoveGrab, Focus)> { let serial = serial.into(); - let mut start_data = check_grab_preconditions(&seat, surface, serial, client_initiated)?; + let mut start_data = + check_grab_preconditions(&seat, serial, client_initiated.then_some(surface))?; let old_mapped = self.element_for_surface(surface).cloned()?; if old_mapped.is_minimized() { return None; @@ -3453,13 +3456,11 @@ impl Shell { ), (ResizeGrab, Focus), )> { - let active_window = mapped.active_window(); - let surface = active_window.wl_surface()?; if mapped.is_fullscreen(true) || mapped.is_maximized(true) { return None; } - let mut start_data = check_grab_preconditions(&seat, &surface, None, false)?; + let mut start_data = check_grab_preconditions(&seat, None, None)?; let (floating_layer, geometry) = if let Some(set) = self .workspaces @@ -3693,7 +3694,8 @@ impl Shell { client_initiated: bool, ) -> Option<(ResizeGrab, Focus)> { let serial = serial.into(); - let start_data = check_grab_preconditions(&seat, surface, serial, client_initiated)?; + let start_data = + check_grab_preconditions(&seat, serial, client_initiated.then_some(surface))?; let mapped = self.element_for_surface(surface).cloned()?; if mapped.is_fullscreen(true) || mapped.is_maximized(true) { return None; @@ -4119,9 +4121,8 @@ fn workspace_set_idx( pub fn check_grab_preconditions( seat: &Seat, - surface: &WlSurface, serial: Option, - client_initiated: bool, + client_initiated: Option<&WlSurface>, ) -> Option { use smithay::reexports::wayland_server::Resource; @@ -4141,7 +4142,7 @@ pub fn check_grab_preconditions( })) }; - if client_initiated { + if let Some(surface) = client_initiated { // Check that this surface has a click or touch down grab. if !match serial { Some(serial) => pointer.has_grab(serial) || touch.has_grab(serial),