// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. use smithay_client_toolkit::reexports::client::QueueHandle; use super::app_data::AppData; use super::surface::SurfaceFocus; use crate::app::{ App, OverlayId }; use crate::tree::find_handlers; use crate::types::Point; use crate::widget::WidgetHandlers; impl AppData { /// Push `on_dismiss` for every active xdg-popup overlay whose /// anchor widget was not hit by a press at `pos` (logical pixels /// in main-surface space). Used to compensate for compositors /// (notably Mutter) that route pointer button events to the main /// surface while a popup grab is technically active, instead of /// breaking the grab. The check skips presses on the trigger pill /// itself so the trigger's own `toggle` message keeps owning that /// transition. pub( crate ) fn dismiss_main_outside_popups( &mut self, pos: Point ) { if self.overlays.is_empty() { return; } let specs = self.app.overlays(); for spec in specs { // Only xdg-popups need the main-side fallback: they grab // the seat and rely on the compositor to break the grab on // outside input, which Mutter does not do reliably. Layer- // shell overlays own their own `input_region` and dispatch // dismissal from the overlay tap path, so firing it again // from a press on the main surface produces a duplicate // dismiss that races other intents (e.g. forge's topbar // taps that route a separate IPC into the same overlay). let Some( anchor_id ) = spec.anchor_widget_id else { continue; }; let anchor_rect = self.main.frame.widget_rects.iter() .find( | w | w.id == Some( anchor_id ) ) .map( | w | w.rect ); let on_anchor = anchor_rect .map( | r | pos.x >= r.x && pos.x < r.x + r.width && pos.y >= r.y && pos.y < r.y + r.height ) .unwrap_or( false ); if !on_anchor { if let Some( msg ) = spec.on_dismiss { self.pending_msgs.push( msg ); } } } } /// Push `on_dismiss` for every active xdg-popup overlay /// unconditionally (used by keyboard Escape handling — there is /// no spatial test to do). pub( crate ) fn dismiss_all_popups( &mut self ) { if self.overlays.is_empty() { return; } let specs = self.app.overlays(); for spec in specs { if spec.anchor_widget_id.is_some() { if let Some( msg ) = spec.on_dismiss { self.pending_msgs.push( msg ); } } } } /// Look up the dismiss message for an overlay (tap on empty area). /// Returns `None` if the overlay has no `on_dismiss` set or was removed. pub( crate ) fn overlay_dismiss_msg( &self, id: OverlayId ) -> Option { self.app.overlays().into_iter() .find( |s| s.id == id ) .and_then( |s| s.on_dismiss ) } pub( crate ) fn set_focus( &mut self, focus: SurfaceFocus, idx: Option, qh: &QueueHandle ) { let was_text_input; let is_text_input; let secure; { let ss = self.surface_mut( focus ); is_text_input = idx .and_then( |i| find_handlers( &ss.frame.widget_rects, i ) ) .map( |h| h.is_text_input() ) .unwrap_or( false ); secure = idx .and_then( |i| find_handlers( &ss.frame.widget_rects, i ) ) .map( |h| matches!( h, WidgetHandlers::TextEdit { secure: true, .. } ) ) .unwrap_or( false ); was_text_input = ss.focused_idx .and_then( |i| find_handlers( &ss.frame.widget_rects, i ) ) .map( |h| h.is_text_input() ) .unwrap_or( false ); // Clear pending text value when losing focus if let Some( prev_idx ) = ss.focused_idx { if idx != Some( prev_idx ) { ss.pending_text_values.remove( &prev_idx ); } } ss.focused_idx = idx; ss.focused_id = idx.and_then( |i| { ss.frame.widget_rects.iter() .find( |w| w.flat_idx == i ) .and_then( |w| w.id ) } ); ss.request_redraw(); // Sync cursor to end of current value when focusing a text // input. Default behaviour collapses the selection to the // cursor — focus changes always discard any prior // selection state. Fields built with `.select_on_focus( // true )` instead anchor the selection at `0` so the // whole value is highlighted, ready to be replaced by the // next keystroke (typical for numeric pickers). if is_text_input { if let Some( i ) = idx { let handler = find_handlers( &ss.frame.widget_rects, i ); let cursor = handler .and_then( |h| h.current_value() ) .map( |v| v.len() ) .unwrap_or( 0 ); let anchor = match handler { Some( WidgetHandlers::TextEdit { select_on_focus: true, .. } ) => 0, _ => cursor, }; ss.frame.cursor_state.insert( i, cursor ); ss.frame.selection_anchor.insert( i, anchor ); } } } if was_text_input && !is_text_input { self.deactivate_text_input(); self.app.on_text_input_focused( false ); self.dirty_caches(); } else if is_text_input && !was_text_input { self.activate_text_input( qh, secure ); self.app.on_text_input_focused( true ); self.dirty_caches(); } else if is_text_input { // Focus moved between text fields: refresh the content type so a // password field is flagged even without re-activating. self.activate_text_input( qh, secure ); } } }