Compare commits

..

No commits in common. "backup/pre-yoda-fork-20260422" and "master" have entirely different histories.

7 changed files with 35 additions and 190 deletions

View file

@ -4,7 +4,6 @@
//! 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};
@ -68,12 +67,6 @@ 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 {
@ -116,10 +109,6 @@ 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 {
@ -143,7 +132,6 @@ impl Default for CosmicTk {
stretch: iced::font::Stretch::Normal,
style: iced::font::Style::Normal,
},
window_controls_position: WindowControlsPosition::default(),
}
}
}

View file

@ -1,49 +1,21 @@
// 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) {
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);
}
}
});
unsafe {
malloc_trim(pad);
}
}
/// Prevents glibc from hoarding memory via memory fragmentation.

View file

@ -27,31 +27,9 @@ 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
@ -113,14 +91,6 @@ 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> {
@ -402,20 +372,12 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
} = theme::spacing();
// Take ownership of the regions to be packed.
let mut start = std::mem::take(&mut self.start);
let start = std::mem::take(&mut self.start);
let center = std::mem::take(&mut self.center);
let mut end = std::mem::take(&mut self.end);
// 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),
}
// Also packs the window controls at the very end.
end.push(self.window_controls(space_xxs));
let padding = if self.is_ssd {
[2, 8, 2, 8]
@ -485,11 +447,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
}
/// Creates the widget for window controls.
fn window_controls(
&mut self,
spacing: u16,
controls_position: WindowControlsPosition,
) -> Element<'a, Message> {
fn window_controls(&mut self, spacing: u16) -> Element<'a, Message> {
macro_rules! icon {
($name:expr, $size:expr, $on_press:expr) => {{
widget::icon::from_name($name)
@ -502,37 +460,25 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
}};
}
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)
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)
.align_y(iced::Alignment::Center)
.into()
}

View file

@ -221,7 +221,7 @@ pub use grid::{Grid, grid};
mod header_bar;
#[doc(inline)]
pub use header_bar::{HeaderBar, WindowControlsPosition, header_bar};
pub use header_bar::{HeaderBar, header_bar};
pub mod icon;
#[doc(inline)]

View file

@ -132,10 +132,7 @@ where
/// ```
#[inline]
pub fn clear(&mut self) {
// `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) {
for entity in self.order.clone() {
self.remove(entity);
}
}

View file

@ -173,10 +173,6 @@ 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>>,
@ -236,7 +232,6 @@ 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,
@ -359,16 +354,6 @@ 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));
@ -406,12 +391,11 @@ where
{
return None;
}
// 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);
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));
Some(ReorderEvent {
dragged,
target,
@ -928,7 +912,6 @@ 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(),
@ -1202,14 +1185,7 @@ where
.dnd_state
.drag_offer
.as_ref()
.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();
.is_some_and(|offer| offer.selected_action.contains(DndAction::Move));
let pending_reorder = if allow_reorder
&& self.on_reorder.is_some()
&& self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type)
@ -1407,33 +1383,6 @@ 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;
}
@ -2438,9 +2387,6 @@ 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
@ -2586,7 +2532,6 @@ 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,

View file

@ -100,10 +100,7 @@ where
/// model.clear();
/// ```
pub fn clear(&mut self) {
// `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) {
for entity in self.order.clone() {
self.remove(entity);
}
}