Separate pointer focus from scrolling window reveal
This commit is contained in:
1 parent
2b0ec94ab7
commit
442b950bcd
5 files changed
+201
-37
No files matched your search
+17
-6
@@ -543,11 +543,16 @@ impl Ferese {
|
|||||||
|
|
||||||
if let Some(window) = self.window_under_visual(position) {
|
if let Some(window) = self.window_under_visual(position) {
|
||||||
let focused = self.window_ids.get(&window).copied();
|
let focused = self.window_ids.get(&window).copied();
|
||||||
if let Some(focused) = focused
|
if let Some(focused) = focused {
|
||||||
&& let Err(error) = self.workspaces.focus_window(focused)
|
let result = if raise {
|
||||||
{
|
self.workspaces.focus_window(focused)
|
||||||
tracing::error!(%error, ?focused, "failed to update workspace focus");
|
} else {
|
||||||
return;
|
self.workspaces.focus_window_without_reveal(focused)
|
||||||
|
};
|
||||||
|
if let Err(error) = result {
|
||||||
|
tracing::error!(%error, ?focused, "failed to update workspace focus");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.focused_window = focused;
|
self.focused_window = focused;
|
||||||
if raise {
|
if raise {
|
||||||
@@ -576,7 +581,13 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.relayout();
|
if raise {
|
||||||
|
self.relayout();
|
||||||
|
} else {
|
||||||
|
// Focus, dimming and the bar title update without moving the layout.
|
||||||
|
self.send_shell_snapshots();
|
||||||
|
crate::backends::direct::render_all(self);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus_window_under_pointer(
|
fn focus_window_under_pointer(
|
||||||
|
|||||||
@@ -119,7 +119,14 @@ impl WorkspaceLayout {
|
|||||||
|
|
||||||
pub fn activate_window(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
pub fn activate_window(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||||
match self {
|
match self {
|
||||||
Self::Scrolling(layout) => layout.focus(window),
|
Self::Scrolling(layout) => layout.focus_and_reveal(window),
|
||||||
|
Self::Tree(layout) => layout.activate_window(window),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn activate_window_without_reveal(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||||
|
match self {
|
||||||
|
Self::Scrolling(layout) => layout.focus_without_reveal(window),
|
||||||
Self::Tree(layout) => layout.activate_window(window),
|
Self::Tree(layout) => layout.activate_window(window),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -607,6 +614,18 @@ impl WorkspaceSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn focus_window(&mut self, window: WindowId) -> Result<(), WorkspaceError> {
|
pub fn focus_window(&mut self, window: WindowId) -> Result<(), WorkspaceError> {
|
||||||
|
self.focus_window_with_reveal(window, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focus_window_without_reveal(&mut self, window: WindowId) -> Result<(), WorkspaceError> {
|
||||||
|
self.focus_window_with_reveal(window, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_window_with_reveal(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
reveal: bool,
|
||||||
|
) -> Result<(), WorkspaceError> {
|
||||||
if self.workspace_for_window(window) != Some(self.active) {
|
if self.workspace_for_window(window) != Some(self.active) {
|
||||||
return Err(WorkspaceError::InvalidState(
|
return Err(WorkspaceError::InvalidState(
|
||||||
"focused window is not on the active workspace",
|
"focused window is not on the active workspace",
|
||||||
@@ -623,7 +642,13 @@ impl WorkspaceSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.placement(window) == Some(WindowPlacement::Tiled) {
|
if self.placement(window) == Some(WindowPlacement::Tiled) {
|
||||||
self.active_mut().layout.activate_window(window)?;
|
if reveal {
|
||||||
|
self.active_mut().layout.activate_window(window)?;
|
||||||
|
} else {
|
||||||
|
self.active_mut()
|
||||||
|
.layout
|
||||||
|
.activate_window_without_reveal(window)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.active_mut().last_focused = Some(window);
|
self.active_mut().last_focused = Some(window);
|
||||||
|
|
||||||
@@ -1418,6 +1443,63 @@ mod tests {
|
|||||||
assert!(workspaces.validate().is_ok());
|
assert!(workspaces.validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_focus_preserves_geometry_and_click_reveals_the_hovered_window() {
|
||||||
|
for strategy in [
|
||||||
|
ViewportFocusStrategy::Minimal,
|
||||||
|
ViewportFocusStrategy::Center,
|
||||||
|
ViewportFocusStrategy::Paged,
|
||||||
|
] {
|
||||||
|
let mut workspaces = WorkspaceSet::new(
|
||||||
|
LayoutMode::Scrolling,
|
||||||
|
ColumnWidth::Proportion(0.5),
|
||||||
|
strategy,
|
||||||
|
);
|
||||||
|
for id in 1..=3 {
|
||||||
|
workspaces
|
||||||
|
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0);
|
||||||
|
let gaps = GapConfig {
|
||||||
|
inner: 0.0,
|
||||||
|
outer: 0.0,
|
||||||
|
smart: false,
|
||||||
|
};
|
||||||
|
workspaces.focus_window(WindowId(2)).unwrap();
|
||||||
|
let before = workspaces
|
||||||
|
.active_mut()
|
||||||
|
.layout
|
||||||
|
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(WindowId(2)))
|
||||||
|
.unwrap();
|
||||||
|
let scroll = workspaces.active().layout.viewport_x();
|
||||||
|
// Rapid right/middle/left hover must never scroll or skip a focus target,
|
||||||
|
// even if a later client commit causes another layout calculation.
|
||||||
|
for id in [3, 2, 1, 2, 3] {
|
||||||
|
let id = WindowId(id);
|
||||||
|
workspaces.focus_window_without_reveal(id).unwrap();
|
||||||
|
assert_eq!(workspaces.active().last_focused, Some(id));
|
||||||
|
let after = workspaces
|
||||||
|
.active_mut()
|
||||||
|
.layout
|
||||||
|
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(id))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(before.geometry, after.geometry, "{strategy:?}");
|
||||||
|
assert_eq!(scroll, workspaces.active().layout.viewport_x());
|
||||||
|
}
|
||||||
|
// Clicking the already-hovered window still requests a reveal.
|
||||||
|
workspaces.focus_window(WindowId(3)).unwrap();
|
||||||
|
let clicked = workspaces
|
||||||
|
.active_mut()
|
||||||
|
.layout
|
||||||
|
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(WindowId(3)))
|
||||||
|
.unwrap();
|
||||||
|
let rect = clicked.geometry[&WindowId(3)];
|
||||||
|
assert!(rect.x >= 0.0 && rect.x + rect.width <= bounds.width);
|
||||||
|
assert_ne!(scroll, workspaces.active().layout.viewport_x());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn opening_a_focused_float_preserves_tiled_geometry_and_scroll() {
|
fn opening_a_focused_float_preserves_tiled_geometry_and_scroll() {
|
||||||
let mut workspaces = WorkspaceSet::new(
|
let mut workspaces = WorkspaceSet::new(
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ pub struct ScrollingLayout {
|
|||||||
neighbor_context: f64,
|
neighbor_context: f64,
|
||||||
default_width: ColumnWidth,
|
default_width: ColumnWidth,
|
||||||
focus_strategy: ViewportFocusStrategy,
|
focus_strategy: ViewportFocusStrategy,
|
||||||
reveal_pending: bool,
|
reveal_pending: Option<WindowId>,
|
||||||
width_cycle_pending: bool,
|
width_cycle_pending: bool,
|
||||||
last_viewport_width: Option<f64>,
|
last_viewport_width: Option<f64>,
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ impl Default for ScrollingLayout {
|
|||||||
neighbor_context: 48.0,
|
neighbor_context: 48.0,
|
||||||
default_width: ColumnWidth::default(),
|
default_width: ColumnWidth::default(),
|
||||||
focus_strategy: ViewportFocusStrategy::Minimal,
|
focus_strategy: ViewportFocusStrategy::Minimal,
|
||||||
reveal_pending: false,
|
reveal_pending: None,
|
||||||
width_cycle_pending: false,
|
width_cycle_pending: false,
|
||||||
last_viewport_width: None,
|
last_viewport_width: None,
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ impl ScrollingLayout {
|
|||||||
|
|
||||||
pub fn set_focus_strategy(&mut self, strategy: ViewportFocusStrategy) {
|
pub fn set_focus_strategy(&mut self, strategy: ViewportFocusStrategy) {
|
||||||
self.focus_strategy = strategy;
|
self.focus_strategy = strategy;
|
||||||
self.reveal_pending = true;
|
self.reveal_pending = self.active_window();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_width(&self) -> ColumnWidth {
|
pub fn default_width(&self) -> ColumnWidth {
|
||||||
@@ -163,7 +163,7 @@ impl ScrollingLayout {
|
|||||||
self.columns
|
self.columns
|
||||||
.insert(index, Column::new(window, self.default_width));
|
.insert(index, Column::new(window, self.default_width));
|
||||||
self.active_column = Some(index);
|
self.active_column = Some(index);
|
||||||
self.reveal_pending = self.columns.len() > 1;
|
self.reveal_pending = (self.columns.len() > 1).then_some(window);
|
||||||
|
|
||||||
debug_assert!(self.validate().is_ok());
|
debug_assert!(self.validate().is_ok());
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -191,9 +191,12 @@ impl ScrollingLayout {
|
|||||||
column.active = column.active.min(column.windows.len() - 1);
|
column.active = column.active.min(column.windows.len() - 1);
|
||||||
column.normalize_heights();
|
column.normalize_heights();
|
||||||
}
|
}
|
||||||
self.reveal_pending |= (removed_active_column
|
if self.reveal_pending == Some(window)
|
||||||
|| self.focus_strategy == ViewportFocusStrategy::Paged)
|
|| removed_active_column
|
||||||
&& !self.columns.is_empty();
|
|| self.focus_strategy == ViewportFocusStrategy::Paged
|
||||||
|
{
|
||||||
|
self.reveal_pending = self.active_window();
|
||||||
|
}
|
||||||
|
|
||||||
debug_assert!(self.validate().is_ok());
|
debug_assert!(self.validate().is_ok());
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -206,7 +209,27 @@ impl ScrollingLayout {
|
|||||||
let changed_column = self.active_column != Some(column);
|
let changed_column = self.active_column != Some(column);
|
||||||
self.active_column = Some(column);
|
self.active_column = Some(column);
|
||||||
self.columns[column].active = index;
|
self.columns[column].active = index;
|
||||||
self.reveal_pending |= changed_column;
|
if changed_column {
|
||||||
|
self.reveal_pending = Some(window);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hover changes the active window without requesting viewport movement.
|
||||||
|
/// Keep any earlier explicit reveal attached to its original window.
|
||||||
|
pub fn focus_without_reveal(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||||
|
let (column, index) = self
|
||||||
|
.window_location(window)
|
||||||
|
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||||
|
self.active_column = Some(column);
|
||||||
|
self.columns[column].active = index;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An explicit selection must reveal even a window already focused by hover.
|
||||||
|
pub fn focus_and_reveal(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||||
|
self.focus_without_reveal(window)?;
|
||||||
|
self.reveal_pending = Some(window);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +254,7 @@ impl ScrollingLayout {
|
|||||||
column.active = insertion;
|
column.active = insertion;
|
||||||
column.normalize_heights();
|
column.normalize_heights();
|
||||||
self.active_column = Some(target_column);
|
self.active_column = Some(target_column);
|
||||||
self.reveal_pending = true;
|
self.reveal_pending = self.active_window();
|
||||||
|
|
||||||
debug_assert!(self.validate().is_ok());
|
debug_assert!(self.validate().is_ok());
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -251,7 +274,7 @@ impl ScrollingLayout {
|
|||||||
self.columns
|
self.columns
|
||||||
.insert(insertion, Column::new(window, self.default_width));
|
.insert(insertion, Column::new(window, self.default_width));
|
||||||
self.active_column = Some(insertion);
|
self.active_column = Some(insertion);
|
||||||
self.reveal_pending = true;
|
self.reveal_pending = self.active_window();
|
||||||
|
|
||||||
debug_assert!(self.validate().is_ok());
|
debug_assert!(self.validate().is_ok());
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -266,7 +289,7 @@ impl ScrollingLayout {
|
|||||||
.window_location(window)
|
.window_location(window)
|
||||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||||
self.columns[column].width = normalized_width(width);
|
self.columns[column].width = normalized_width(width);
|
||||||
self.reveal_pending = true;
|
self.reveal_pending = self.active_window();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,12 +503,16 @@ impl ScrollingLayout {
|
|||||||
if self.width_cycle_pending {
|
if self.width_cycle_pending {
|
||||||
self.retarget_after_width_cycle(&column_positions, viewport_width);
|
self.retarget_after_width_cycle(&column_positions, viewport_width);
|
||||||
self.width_cycle_pending = false;
|
self.width_cycle_pending = false;
|
||||||
self.reveal_pending = false;
|
self.reveal_pending = None;
|
||||||
} else if self.reveal_pending
|
} else {
|
||||||
|| (viewport_resized && self.focus_strategy == ViewportFocusStrategy::Paged)
|
let reveal = self.reveal_pending.take().or_else(|| {
|
||||||
{
|
(viewport_resized && self.focus_strategy == ViewportFocusStrategy::Paged)
|
||||||
self.reveal_active_column(&column_positions, viewport_width);
|
.then(|| self.active_window())
|
||||||
self.reveal_pending = false;
|
.flatten()
|
||||||
|
});
|
||||||
|
if let Some((column, _)) = reveal.and_then(|window| self.window_location(window)) {
|
||||||
|
self.reveal_column(&column_positions, viewport_width, column);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut result = LayoutResult::default();
|
let mut result = LayoutResult::default();
|
||||||
@@ -518,14 +545,11 @@ impl ScrollingLayout {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reveal_active_column(&mut self, positions: &[(f64, f64)], viewport_width: f64) {
|
fn reveal_column(&mut self, positions: &[(f64, f64)], viewport_width: f64, active: usize) {
|
||||||
if let Some(start) = self.paged_viewport(positions, viewport_width) {
|
if let Some(start) = self.paged_viewport(positions, viewport_width, Some(active)) {
|
||||||
self.viewport_x = start;
|
self.viewport_x = start;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(active) = self.active_column else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let (start, width) = positions[active];
|
let (start, width) = positions[active];
|
||||||
let end = start + width;
|
let end = start + width;
|
||||||
if self.focus_strategy == ViewportFocusStrategy::Center {
|
if self.focus_strategy == ViewportFocusStrategy::Center {
|
||||||
@@ -555,11 +579,16 @@ impl ScrollingLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paged_viewport(&self, positions: &[(f64, f64)], viewport_width: f64) -> Option<f64> {
|
fn paged_viewport(
|
||||||
|
&self,
|
||||||
|
positions: &[(f64, f64)],
|
||||||
|
viewport_width: f64,
|
||||||
|
active: Option<usize>,
|
||||||
|
) -> Option<f64> {
|
||||||
if self.focus_strategy != ViewportFocusStrategy::Paged {
|
if self.focus_strategy != ViewportFocusStrategy::Paged {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let active = self.active_column?;
|
let active = active?;
|
||||||
// Pack actual allocated widths, including gaps and client constraints.
|
// Pack actual allocated widths, including gaps and client constraints.
|
||||||
// Page boundaries are independent of focus direction. An oversized
|
// Page boundaries are independent of focus direction. An oversized
|
||||||
// column occupies its own page rather than overlapping its neighbors.
|
// column occupies its own page rather than overlapping its neighbors.
|
||||||
@@ -582,7 +611,7 @@ impl ScrollingLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn retarget_after_width_cycle(&mut self, positions: &[(f64, f64)], viewport_width: f64) {
|
fn retarget_after_width_cycle(&mut self, positions: &[(f64, f64)], viewport_width: f64) {
|
||||||
if let Some(start) = self.paged_viewport(positions, viewport_width) {
|
if let Some(start) = self.paged_viewport(positions, viewport_width, self.active_column) {
|
||||||
self.viewport_x = start;
|
self.viewport_x = start;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -617,7 +646,7 @@ impl ScrollingLayout {
|
|||||||
if self.columns[source].windows.len() == 1 {
|
if self.columns[source].windows.len() == 1 {
|
||||||
self.columns.swap(source, destination);
|
self.columns.swap(source, destination);
|
||||||
self.active_column = Some(destination);
|
self.active_column = Some(destination);
|
||||||
self.reveal_pending = true;
|
self.reveal_pending = self.active_window();
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -631,7 +660,7 @@ impl ScrollingLayout {
|
|||||||
self.columns
|
self.columns
|
||||||
.insert(insertion, Column::new(window, self.default_width));
|
.insert(insertion, Column::new(window, self.default_width));
|
||||||
self.active_column = Some(insertion);
|
self.active_column = Some(insertion);
|
||||||
self.reveal_pending = true;
|
self.reveal_pending = self.active_window();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,6 +833,48 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hover_does_not_replace_a_pending_explicit_reveal() {
|
||||||
|
for strategy in [
|
||||||
|
ViewportFocusStrategy::Minimal,
|
||||||
|
ViewportFocusStrategy::Center,
|
||||||
|
ViewportFocusStrategy::Paged,
|
||||||
|
] {
|
||||||
|
let mut layout = ScrollingLayout::default();
|
||||||
|
layout.set_focus_strategy(strategy);
|
||||||
|
for id in 1..=3 {
|
||||||
|
layout.insert(window(id), None).unwrap();
|
||||||
|
}
|
||||||
|
let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0);
|
||||||
|
let gaps = GapConfig {
|
||||||
|
inner: 0.0,
|
||||||
|
outer: 0.0,
|
||||||
|
smart: false,
|
||||||
|
};
|
||||||
|
for target in [3, 1] {
|
||||||
|
layout.focus_and_reveal(window(target)).unwrap();
|
||||||
|
let mut expected = layout.clone();
|
||||||
|
expected
|
||||||
|
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(target)))
|
||||||
|
.unwrap();
|
||||||
|
layout.focus_without_reveal(window(2)).unwrap();
|
||||||
|
layout
|
||||||
|
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(layout.active_window(), Some(window(2)));
|
||||||
|
assert_eq!(layout.viewport_x(), expected.viewport_x(), "{strategy:?}");
|
||||||
|
}
|
||||||
|
layout.focus_and_reveal(window(3)).unwrap();
|
||||||
|
layout.focus_without_reveal(window(2)).unwrap();
|
||||||
|
layout.remove(window(3)).unwrap();
|
||||||
|
assert!(
|
||||||
|
layout
|
||||||
|
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn focus_reveal_scrolls_only_as_far_as_needed() {
|
fn focus_reveal_scrolls_only_as_far_as_needed() {
|
||||||
let mut layout = ScrollingLayout::default();
|
let mut layout = ScrollingLayout::default();
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ theme {
|
|||||||
|
|
||||||
| `input` key | Type | Default | Meaning |
|
| `input` key | Type | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `focus_follows_mouse` | boolean | `false` | Focus windows under the pointer without raising them; click to raise |
|
| `focus_follows_mouse` | boolean | `false` | Focus visible windows under the pointer without raising them or scrolling; click or use keyboard focus to reveal a window |
|
||||||
| `xkb_layout` | nonempty string | `"us"` | XKB layout |
|
| `xkb_layout` | nonempty string | `"us"` | XKB layout |
|
||||||
| `xkb_variant` | string | `""` | XKB variant |
|
| `xkb_variant` | string | `""` | XKB variant |
|
||||||
| `xkb_options` | string array | `[]` | XKB options |
|
| `xkb_options` | string array | `[]` | XKB options |
|
||||||
|
|||||||
@@ -917,7 +917,7 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
toggle(
|
toggle(
|
||||||
"input.focus_follows_mouse",
|
"input.focus_follows_mouse",
|
||||||
"Focus follows pointer",
|
"Focus follows pointer",
|
||||||
"Focus windows as the pointer moves over them.",
|
"Focus visible windows without raising or scrolling. Click to bring them into view.",
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
toggle(
|
toggle(
|
||||||
|
|||||||
Reference in new issue
Block a user