Compare commits
5 commits
master
...
perf/quick
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c3319351c | |||
| a322516f33 | |||
| 108441ef61 | |||
| 1d98eee6de | |||
| 77262dd0af |
7 changed files with 190 additions and 35 deletions
|
|
@ -4,6 +4,7 @@
|
||||||
//! Configurations available to libcosmic applications.
|
//! Configurations available to libcosmic applications.
|
||||||
|
|
||||||
use crate::cosmic_theme::Density;
|
use crate::cosmic_theme::Density;
|
||||||
|
use crate::widget::WindowControlsPosition;
|
||||||
use cosmic_config::cosmic_config_derive::CosmicConfigEntry;
|
use cosmic_config::cosmic_config_derive::CosmicConfigEntry;
|
||||||
use cosmic_config::{Config, CosmicConfigEntry};
|
use cosmic_config::{Config, CosmicConfigEntry};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -67,6 +68,12 @@ pub fn header_size() -> Density {
|
||||||
COSMIC_TK.read().unwrap().header_size
|
COSMIC_TK.read().unwrap().header_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Position of the window control buttons (close / minimize / maximize).
|
||||||
|
#[allow(clippy::missing_panics_doc)]
|
||||||
|
pub fn window_controls_position() -> WindowControlsPosition {
|
||||||
|
COSMIC_TK.read().unwrap().window_controls_position
|
||||||
|
}
|
||||||
|
|
||||||
/// Interface density.
|
/// Interface density.
|
||||||
#[allow(clippy::missing_panics_doc)]
|
#[allow(clippy::missing_panics_doc)]
|
||||||
pub fn interface_density() -> Density {
|
pub fn interface_density() -> Density {
|
||||||
|
|
@ -109,6 +116,10 @@ pub struct CosmicTk {
|
||||||
|
|
||||||
/// Mono font family
|
/// Mono font family
|
||||||
pub monospace_font: FontConfig,
|
pub monospace_font: FontConfig,
|
||||||
|
|
||||||
|
/// Side on which window control buttons (close / minimize / maximize)
|
||||||
|
/// are placed. `End` = right (Linux / GNOME), `Start` = left (macOS).
|
||||||
|
pub window_controls_position: WindowControlsPosition,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CosmicTk {
|
impl Default for CosmicTk {
|
||||||
|
|
@ -132,6 +143,7 @@ impl Default for CosmicTk {
|
||||||
stretch: iced::font::Stretch::Normal,
|
stretch: iced::font::Stretch::Normal,
|
||||||
style: iced::font::Style::Normal,
|
style: iced::font::Style::Normal,
|
||||||
},
|
},
|
||||||
|
window_controls_position: WindowControlsPosition::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,49 @@
|
||||||
// Copyright 2025 System76 <info@system76.com>
|
// Copyright 2025 System76 <info@system76.com>
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
use std::cell::Cell;
|
||||||
use std::os::raw::c_int;
|
use std::os::raw::c_int;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
const M_MMAP_THRESHOLD: c_int = -3;
|
const M_MMAP_THRESHOLD: c_int = -3;
|
||||||
|
|
||||||
|
/// Minimum interval between two actual `malloc_trim` calls.
|
||||||
|
///
|
||||||
|
/// `trim` is called at the end of every `update()` and `view()`, which can
|
||||||
|
/// reach 60-200 Hz during typical scrolling, resize, or animation. Each
|
||||||
|
/// `malloc_trim` walks the glibc heap (10 µs to several ms depending on
|
||||||
|
/// fragmentation), so calling it at render frequency can consume a
|
||||||
|
/// substantial fraction of the frame budget. Throttling to 1 Hz keeps RSS
|
||||||
|
/// bounded while removing the per-frame syscall from the hot path.
|
||||||
|
const TRIM_MIN_INTERVAL: Duration = Duration::from_millis(1000);
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static LAST_TRIM: Cell<Option<Instant>> = const { Cell::new(None) };
|
||||||
|
}
|
||||||
|
|
||||||
unsafe extern "C" {
|
unsafe extern "C" {
|
||||||
fn malloc_trim(pad: usize);
|
fn malloc_trim(pad: usize);
|
||||||
|
|
||||||
fn mallopt(param: c_int, value: c_int) -> c_int;
|
fn mallopt(param: c_int, value: c_int) -> c_int;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Throttled wrapper over `malloc_trim`. Safe to call at render frequency:
|
||||||
|
/// consecutive calls within `TRIM_MIN_INTERVAL` (per-thread) skip the syscall.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn trim(pad: usize) {
|
pub fn trim(pad: usize) {
|
||||||
unsafe {
|
LAST_TRIM.with(|last| {
|
||||||
malloc_trim(pad);
|
let now = Instant::now();
|
||||||
}
|
let should_trim = match last.get() {
|
||||||
|
None => true,
|
||||||
|
Some(prev) => now.duration_since(prev) >= TRIM_MIN_INTERVAL,
|
||||||
|
};
|
||||||
|
if should_trim {
|
||||||
|
last.set(Some(now));
|
||||||
|
unsafe {
|
||||||
|
malloc_trim(pad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prevents glibc from hoarding memory via memory fragmentation.
|
/// Prevents glibc from hoarding memory via memory fragmentation.
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,31 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> {
|
||||||
is_ssd: false,
|
is_ssd: false,
|
||||||
on_double_click: None,
|
on_double_click: None,
|
||||||
transparent: false,
|
transparent: false,
|
||||||
|
controls_position: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Position of the window control buttons (close/min/max) within the headerbar.
|
||||||
|
#[derive(
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
Debug,
|
||||||
|
Default,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
serde::Serialize,
|
||||||
|
serde::Deserialize,
|
||||||
|
)]
|
||||||
|
pub enum WindowControlsPosition {
|
||||||
|
/// Controls packed at the start (left on LTR) — macOS style.
|
||||||
|
/// Internal icon order becomes close → minimize → maximize.
|
||||||
|
Start,
|
||||||
|
/// Controls packed at the end (right on LTR) — Linux / GNOME style.
|
||||||
|
/// Internal icon order is minimize → maximize → close.
|
||||||
|
#[default]
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Setters)]
|
#[derive(Setters)]
|
||||||
pub struct HeaderBar<'a, Message> {
|
pub struct HeaderBar<'a, Message> {
|
||||||
/// Defines the title of the window
|
/// Defines the title of the window
|
||||||
|
|
@ -91,6 +113,14 @@ pub struct HeaderBar<'a, Message> {
|
||||||
|
|
||||||
/// Whether the headerbar should be transparent
|
/// Whether the headerbar should be transparent
|
||||||
transparent: bool,
|
transparent: bool,
|
||||||
|
|
||||||
|
/// Side on which the window control buttons (close / minimize / maximize)
|
||||||
|
/// are rendered. `None` falls back to the user's CosmicTk config
|
||||||
|
/// (`crate::config::window_controls_position()`). `Some` overrides it.
|
||||||
|
/// `End` matches Linux / GNOME conventions; `Start` provides macOS-style
|
||||||
|
/// left-side controls.
|
||||||
|
#[setters(strip_option)]
|
||||||
|
controls_position: Option<WindowControlsPosition>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
|
|
@ -372,12 +402,20 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
} = theme::spacing();
|
} = theme::spacing();
|
||||||
|
|
||||||
// Take ownership of the regions to be packed.
|
// Take ownership of the regions to be packed.
|
||||||
let start = std::mem::take(&mut self.start);
|
let mut start = std::mem::take(&mut self.start);
|
||||||
let center = std::mem::take(&mut self.center);
|
let center = std::mem::take(&mut self.center);
|
||||||
let mut end = std::mem::take(&mut self.end);
|
let mut end = std::mem::take(&mut self.end);
|
||||||
|
|
||||||
// Also packs the window controls at the very end.
|
// Pack window controls on the configured side (reads CosmicTk
|
||||||
end.push(self.window_controls(space_xxs));
|
// config when the builder did not set an explicit override).
|
||||||
|
let controls_position = self
|
||||||
|
.controls_position
|
||||||
|
.unwrap_or_else(crate::config::window_controls_position);
|
||||||
|
let controls = self.window_controls(space_xxs, controls_position);
|
||||||
|
match controls_position {
|
||||||
|
WindowControlsPosition::End => end.push(controls),
|
||||||
|
WindowControlsPosition::Start => start.insert(0, controls),
|
||||||
|
}
|
||||||
|
|
||||||
let padding = if self.is_ssd {
|
let padding = if self.is_ssd {
|
||||||
[2, 8, 2, 8]
|
[2, 8, 2, 8]
|
||||||
|
|
@ -447,7 +485,11 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the widget for window controls.
|
/// Creates the widget for window controls.
|
||||||
fn window_controls(&mut self, spacing: u16) -> Element<'a, Message> {
|
fn window_controls(
|
||||||
|
&mut self,
|
||||||
|
spacing: u16,
|
||||||
|
controls_position: WindowControlsPosition,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
macro_rules! icon {
|
macro_rules! icon {
|
||||||
($name:expr, $size:expr, $on_press:expr) => {{
|
($name:expr, $size:expr, $on_press:expr) => {{
|
||||||
widget::icon::from_name($name)
|
widget::icon::from_name($name)
|
||||||
|
|
@ -460,25 +502,37 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
widget::row::with_capacity(3)
|
let minimize = self
|
||||||
.push_maybe(
|
.on_minimize
|
||||||
self.on_minimize
|
.take()
|
||||||
.take()
|
.map(|m| icon!("window-minimize-symbolic", 16, m));
|
||||||
.map(|m| icon!("window-minimize-symbolic", 16, m)),
|
let maximize = self.on_maximize.take().map(|m| {
|
||||||
)
|
if self.maximized {
|
||||||
.push_maybe(self.on_maximize.take().map(|m| {
|
icon!("window-restore-symbolic", 16, m)
|
||||||
if self.maximized {
|
} else {
|
||||||
icon!("window-restore-symbolic", 16, m)
|
icon!("window-maximize-symbolic", 16, m)
|
||||||
} else {
|
}
|
||||||
icon!("window-maximize-symbolic", 16, m)
|
});
|
||||||
}
|
let close = self
|
||||||
}))
|
.on_close
|
||||||
.push_maybe(
|
.take()
|
||||||
self.on_close
|
.map(|m| icon!("window-close-symbolic", 16, m));
|
||||||
.take()
|
|
||||||
.map(|m| icon!("window-close-symbolic", 16, m)),
|
// Icon order follows the OS convention for the chosen side:
|
||||||
)
|
// End → minimize, maximize, close (Linux / GNOME)
|
||||||
.spacing(spacing)
|
// Start → close, minimize, maximize (macOS)
|
||||||
|
let row = widget::row::with_capacity(3);
|
||||||
|
let row = match controls_position {
|
||||||
|
WindowControlsPosition::End => row
|
||||||
|
.push_maybe(minimize)
|
||||||
|
.push_maybe(maximize)
|
||||||
|
.push_maybe(close),
|
||||||
|
WindowControlsPosition::Start => row
|
||||||
|
.push_maybe(close)
|
||||||
|
.push_maybe(minimize)
|
||||||
|
.push_maybe(maximize),
|
||||||
|
};
|
||||||
|
row.spacing(spacing)
|
||||||
.align_y(iced::Alignment::Center)
|
.align_y(iced::Alignment::Center)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -221,7 +221,7 @@ pub use grid::{Grid, grid};
|
||||||
|
|
||||||
mod header_bar;
|
mod header_bar;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use header_bar::{HeaderBar, header_bar};
|
pub use header_bar::{HeaderBar, WindowControlsPosition, header_bar};
|
||||||
|
|
||||||
pub mod icon;
|
pub mod icon;
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,10 @@ where
|
||||||
/// ```
|
/// ```
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
for entity in self.order.clone() {
|
// `remove()` mutates `self.order`, so transfer ownership first:
|
||||||
|
// the inner `self.order.remove(index)` then no-ops because
|
||||||
|
// `position()` can't find the entity in the empty VecDeque.
|
||||||
|
for entity in std::mem::take(&mut self.order) {
|
||||||
self.remove(entity);
|
self.remove(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,10 @@ where
|
||||||
pub(super) on_context: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
pub(super) on_context: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||||
#[setters(skip)]
|
#[setters(skip)]
|
||||||
pub(super) on_middle_press: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
pub(super) on_middle_press: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||||
|
/// Emits the ID of the item that was double-clicked with the left button.
|
||||||
|
/// Fires in addition to `on_activate` (which keeps firing on each click).
|
||||||
|
#[setters(skip)]
|
||||||
|
pub(super) on_double_click: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||||
#[setters(skip)]
|
#[setters(skip)]
|
||||||
pub(super) on_dnd_drop:
|
pub(super) on_dnd_drop:
|
||||||
Option<Box<dyn Fn(Entity, Vec<u8>, String, DndAction) -> Message + 'static>>,
|
Option<Box<dyn Fn(Entity, Vec<u8>, String, DndAction) -> Message + 'static>>,
|
||||||
|
|
@ -232,6 +236,7 @@ where
|
||||||
on_close: None,
|
on_close: None,
|
||||||
on_context: None,
|
on_context: None,
|
||||||
on_middle_press: None,
|
on_middle_press: None,
|
||||||
|
on_double_click: None,
|
||||||
on_dnd_drop: None,
|
on_dnd_drop: None,
|
||||||
on_dnd_enter: None,
|
on_dnd_enter: None,
|
||||||
on_dnd_leave: None,
|
on_dnd_leave: None,
|
||||||
|
|
@ -354,6 +359,16 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emitted when a tab is double-clicked with the left mouse button.
|
||||||
|
/// Fires in addition to `on_activate`, which keeps firing on each click.
|
||||||
|
pub fn on_double_click<T>(mut self, on_double_click: T) -> Self
|
||||||
|
where
|
||||||
|
T: Fn(Entity) -> Message + 'static,
|
||||||
|
{
|
||||||
|
self.on_double_click = Some(Box::new(on_double_click));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Enable drag-and-drop support for tabs using the provided payload builder.
|
/// Enable drag-and-drop support for tabs using the provided payload builder.
|
||||||
pub fn enable_tab_drag(mut self, mime: String) -> Self {
|
pub fn enable_tab_drag(mut self, mime: String) -> Self {
|
||||||
self.tab_drag = Some(TabDragSource::new(mime));
|
self.tab_drag = Some(TabDragSource::new(mime));
|
||||||
|
|
@ -391,11 +406,12 @@ where
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let position = state
|
// Always use positional swap (Konsole/Firefox/Chrome semantics):
|
||||||
.drop_hint
|
// dropping onto any part of a different tab swaps it with the dragged
|
||||||
.filter(|hint| hint.entity == target)
|
// tab. drop_hint.side-based Before/After is counter-intuitive: dropping
|
||||||
.map(|hint| InsertPosition::from(hint.side))
|
// A (pos 0) on the left half of B (pos 1) resolves to "Before B" which,
|
||||||
.unwrap_or_else(|| self.default_insert_position(dragged, target));
|
// after removing A, lands at pos 0 — so the tab appears not to move.
|
||||||
|
let position = self.default_insert_position(dragged, target);
|
||||||
Some(ReorderEvent {
|
Some(ReorderEvent {
|
||||||
dragged,
|
dragged,
|
||||||
target,
|
target,
|
||||||
|
|
@ -912,6 +928,7 @@ where
|
||||||
hovered: Default::default(),
|
hovered: Default::default(),
|
||||||
known_length: Default::default(),
|
known_length: Default::default(),
|
||||||
middle_clicked: Default::default(),
|
middle_clicked: Default::default(),
|
||||||
|
last_click: None,
|
||||||
internal_layout: Default::default(),
|
internal_layout: Default::default(),
|
||||||
context_cursor: Point::default(),
|
context_cursor: Point::default(),
|
||||||
show_context: Default::default(),
|
show_context: Default::default(),
|
||||||
|
|
@ -1185,7 +1202,14 @@ where
|
||||||
.dnd_state
|
.dnd_state
|
||||||
.drag_offer
|
.drag_offer
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|offer| offer.selected_action.contains(DndAction::Move));
|
.is_some_and(|offer| offer.selected_action.contains(DndAction::Move))
|
||||||
|
// Self-drop fallback: some compositors (cosmic-comp
|
||||||
|
// observed) do not emit OfferEvent::SelectedAction for
|
||||||
|
// internal drags, leaving selected_action empty.
|
||||||
|
// dragging_tab is only set by start_tab_drag on this
|
||||||
|
// same widget, so this covers the self-drop case
|
||||||
|
// safely; mime and on_reorder are checked below.
|
||||||
|
|| state.dragging_tab.is_some();
|
||||||
let pending_reorder = if allow_reorder
|
let pending_reorder = if allow_reorder
|
||||||
&& self.on_reorder.is_some()
|
&& self.on_reorder.is_some()
|
||||||
&& self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type)
|
&& self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type)
|
||||||
|
|
@ -1383,6 +1407,33 @@ where
|
||||||
state.set_focused();
|
state.set_focused();
|
||||||
state.focused_item = Item::Tab(key);
|
state.focused_item = Item::Tab(key);
|
||||||
state.pressed_item = None;
|
state.pressed_item = None;
|
||||||
|
|
||||||
|
// Double-click detection on the same entity
|
||||||
|
// within 400 ms — fires after on_activate so
|
||||||
|
// the tab is already focused when the handler
|
||||||
|
// runs.
|
||||||
|
if let Some(on_double_click) =
|
||||||
|
self.on_double_click.as_ref()
|
||||||
|
{
|
||||||
|
let now = Instant::now();
|
||||||
|
let is_double = match state.last_click {
|
||||||
|
Some((prev, t)) => {
|
||||||
|
prev == key
|
||||||
|
&& now.duration_since(t)
|
||||||
|
< Duration::from_millis(400)
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
state.last_click = if is_double {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((key, now))
|
||||||
|
};
|
||||||
|
if is_double {
|
||||||
|
shell.publish(on_double_click(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
shell.capture_event();
|
shell.capture_event();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2387,6 +2438,9 @@ pub struct LocalState {
|
||||||
hovered: Item,
|
hovered: Item,
|
||||||
/// The ID of the button that was middle-clicked, but not yet released.
|
/// The ID of the button that was middle-clicked, but not yet released.
|
||||||
middle_clicked: Option<Item>,
|
middle_clicked: Option<Item>,
|
||||||
|
/// Entity and timestamp of the most recent left-click activation, used
|
||||||
|
/// to detect double-clicks on the same tab.
|
||||||
|
last_click: Option<(Entity, Instant)>,
|
||||||
/// Last known length of the model.
|
/// Last known length of the model.
|
||||||
pub(super) known_length: usize,
|
pub(super) known_length: usize,
|
||||||
/// Dimensions of internal buttons when shrinking
|
/// Dimensions of internal buttons when shrinking
|
||||||
|
|
@ -2532,6 +2586,7 @@ mod tests {
|
||||||
hovered: Item::default(),
|
hovered: Item::default(),
|
||||||
known_length: 0,
|
known_length: 0,
|
||||||
middle_clicked: None,
|
middle_clicked: None,
|
||||||
|
last_click: None,
|
||||||
internal_layout: Vec::new(),
|
internal_layout: Vec::new(),
|
||||||
context_cursor: Point::ORIGIN,
|
context_cursor: Point::ORIGIN,
|
||||||
show_context: None,
|
show_context: None,
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,10 @@ where
|
||||||
/// model.clear();
|
/// model.clear();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
for entity in self.order.clone() {
|
// `remove()` mutates `self.order`, so transfer ownership first:
|
||||||
|
// the inner `self.order.remove(index)` then no-ops because
|
||||||
|
// `position()` can't find the entity in the empty VecDeque.
|
||||||
|
for entity in std::mem::take(&mut self.order) {
|
||||||
self.remove(entity);
|
self.remove(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue