Compare commits
5 commits
master
...
backup/pre
| 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.
|
||||
|
||||
use crate::cosmic_theme::Density;
|
||||
use crate::widget::WindowControlsPosition;
|
||||
use cosmic_config::cosmic_config_derive::CosmicConfigEntry;
|
||||
use cosmic_config::{Config, CosmicConfigEntry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -67,6 +68,12 @@ pub fn header_size() -> Density {
|
|||
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.
|
||||
#[allow(clippy::missing_panics_doc)]
|
||||
pub fn interface_density() -> Density {
|
||||
|
|
@ -109,6 +116,10 @@ pub struct CosmicTk {
|
|||
|
||||
/// Mono font family
|
||||
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 {
|
||||
|
|
@ -132,6 +143,7 @@ impl Default for CosmicTk {
|
|||
stretch: iced::font::Stretch::Normal,
|
||||
style: iced::font::Style::Normal,
|
||||
},
|
||||
window_controls_position: WindowControlsPosition::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,49 @@
|
|||
// Copyright 2025 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use std::cell::Cell;
|
||||
use std::os::raw::c_int;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
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" {
|
||||
fn malloc_trim(pad: usize);
|
||||
|
||||
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]
|
||||
pub fn trim(pad: usize) {
|
||||
unsafe {
|
||||
malloc_trim(pad);
|
||||
}
|
||||
LAST_TRIM.with(|last| {
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -27,9 +27,31 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> {
|
|||
is_ssd: false,
|
||||
on_double_click: None,
|
||||
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)]
|
||||
pub struct HeaderBar<'a, Message> {
|
||||
/// Defines the title of the window
|
||||
|
|
@ -91,6 +113,14 @@ pub struct HeaderBar<'a, Message> {
|
|||
|
||||
/// Whether the headerbar should be transparent
|
||||
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> {
|
||||
|
|
@ -372,12 +402,20 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
} = theme::spacing();
|
||||
|
||||
// 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 mut end = std::mem::take(&mut self.end);
|
||||
|
||||
// Also packs the window controls at the very end.
|
||||
end.push(self.window_controls(space_xxs));
|
||||
// Pack window controls on the configured side (reads CosmicTk
|
||||
// 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 {
|
||||
[2, 8, 2, 8]
|
||||
|
|
@ -447,7 +485,11 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
($name:expr, $size:expr, $on_press:expr) => {{
|
||||
widget::icon::from_name($name)
|
||||
|
|
@ -460,25 +502,37 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
}};
|
||||
}
|
||||
|
||||
widget::row::with_capacity(3)
|
||||
.push_maybe(
|
||||
self.on_minimize
|
||||
.take()
|
||||
.map(|m| icon!("window-minimize-symbolic", 16, m)),
|
||||
)
|
||||
.push_maybe(self.on_maximize.take().map(|m| {
|
||||
if self.maximized {
|
||||
icon!("window-restore-symbolic", 16, m)
|
||||
} else {
|
||||
icon!("window-maximize-symbolic", 16, m)
|
||||
}
|
||||
}))
|
||||
.push_maybe(
|
||||
self.on_close
|
||||
.take()
|
||||
.map(|m| icon!("window-close-symbolic", 16, m)),
|
||||
)
|
||||
.spacing(spacing)
|
||||
let minimize = self
|
||||
.on_minimize
|
||||
.take()
|
||||
.map(|m| icon!("window-minimize-symbolic", 16, m));
|
||||
let maximize = self.on_maximize.take().map(|m| {
|
||||
if self.maximized {
|
||||
icon!("window-restore-symbolic", 16, m)
|
||||
} else {
|
||||
icon!("window-maximize-symbolic", 16, m)
|
||||
}
|
||||
});
|
||||
let close = self
|
||||
.on_close
|
||||
.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)
|
||||
// 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)
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ pub use grid::{Grid, grid};
|
|||
|
||||
mod header_bar;
|
||||
#[doc(inline)]
|
||||
pub use header_bar::{HeaderBar, header_bar};
|
||||
pub use header_bar::{HeaderBar, WindowControlsPosition, header_bar};
|
||||
|
||||
pub mod icon;
|
||||
#[doc(inline)]
|
||||
|
|
|
|||
|
|
@ -132,7 +132,10 @@ where
|
|||
/// ```
|
||||
#[inline]
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,6 +173,10 @@ where
|
|||
pub(super) on_context: Option<Box<dyn Fn(Entity) -> Message + 'static>>,
|
||||
#[setters(skip)]
|
||||
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)]
|
||||
pub(super) on_dnd_drop:
|
||||
Option<Box<dyn Fn(Entity, Vec<u8>, String, DndAction) -> Message + 'static>>,
|
||||
|
|
@ -232,6 +236,7 @@ where
|
|||
on_close: None,
|
||||
on_context: None,
|
||||
on_middle_press: None,
|
||||
on_double_click: None,
|
||||
on_dnd_drop: None,
|
||||
on_dnd_enter: None,
|
||||
on_dnd_leave: None,
|
||||
|
|
@ -354,6 +359,16 @@ where
|
|||
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.
|
||||
pub fn enable_tab_drag(mut self, mime: String) -> Self {
|
||||
self.tab_drag = Some(TabDragSource::new(mime));
|
||||
|
|
@ -391,11 +406,12 @@ where
|
|||
{
|
||||
return None;
|
||||
}
|
||||
let position = state
|
||||
.drop_hint
|
||||
.filter(|hint| hint.entity == target)
|
||||
.map(|hint| InsertPosition::from(hint.side))
|
||||
.unwrap_or_else(|| self.default_insert_position(dragged, target));
|
||||
// Always use positional swap (Konsole/Firefox/Chrome semantics):
|
||||
// dropping onto any part of a different tab swaps it with the dragged
|
||||
// tab. drop_hint.side-based Before/After is counter-intuitive: dropping
|
||||
// A (pos 0) on the left half of B (pos 1) resolves to "Before B" which,
|
||||
// after removing A, lands at pos 0 — so the tab appears not to move.
|
||||
let position = self.default_insert_position(dragged, target);
|
||||
Some(ReorderEvent {
|
||||
dragged,
|
||||
target,
|
||||
|
|
@ -912,6 +928,7 @@ where
|
|||
hovered: Default::default(),
|
||||
known_length: Default::default(),
|
||||
middle_clicked: Default::default(),
|
||||
last_click: None,
|
||||
internal_layout: Default::default(),
|
||||
context_cursor: Point::default(),
|
||||
show_context: Default::default(),
|
||||
|
|
@ -1185,7 +1202,14 @@ where
|
|||
.dnd_state
|
||||
.drag_offer
|
||||
.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
|
||||
&& self.on_reorder.is_some()
|
||||
&& self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type)
|
||||
|
|
@ -1383,6 +1407,33 @@ where
|
|||
state.set_focused();
|
||||
state.focused_item = Item::Tab(key);
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
|
@ -2387,6 +2438,9 @@ pub struct LocalState {
|
|||
hovered: Item,
|
||||
/// The ID of the button that was middle-clicked, but not yet released.
|
||||
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.
|
||||
pub(super) known_length: usize,
|
||||
/// Dimensions of internal buttons when shrinking
|
||||
|
|
@ -2532,6 +2586,7 @@ mod tests {
|
|||
hovered: Item::default(),
|
||||
known_length: 0,
|
||||
middle_clicked: None,
|
||||
last_click: None,
|
||||
internal_layout: Vec::new(),
|
||||
context_cursor: Point::ORIGIN,
|
||||
show_context: None,
|
||||
|
|
|
|||
|
|
@ -100,7 +100,10 @@ where
|
|||
/// model.clear();
|
||||
/// ```
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue