Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions rust/limux-host-linux/src/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,46 @@ pub fn focus_active_tab_in_pane(pane_widget: &gtk::Widget) -> bool {
true
}

/// Returns the active tab id in the given pane, if any.
pub fn active_tab_in_pane(pane_widget: &gtk::Widget) -> Option<String> {
let internals = find_pane_internals(pane_widget)?;
let tab_state = internals.tab_state.borrow();
tab_state.active_tab.clone()
}

/// Returns the number of tabs in the given pane.
pub fn tab_count_in_pane(pane_widget: &gtk::Widget) -> usize {
let Some(internals) = find_pane_internals(pane_widget) else {
return 0;
};
let count = internals.tab_state.borrow().tabs.len();
count
}

/// Closes the tab with the given id in the pane. If the tab is pinned, it is ignored.
pub fn close_tab_in_pane(pane_widget: &gtk::Widget, tab_id: &str) {
let Some(internals) = find_pane_internals(pane_widget) else {
return;
};
let is_pinned = internals
.tab_state
.borrow()
.tabs
.iter()
.any(|entry| entry.id == tab_id && entry.pinned);
if !is_pinned {
remove_tab(
&internals.tab_strip,
&internals.content_stack,
&internals.tab_state,
tab_id,
&internals.callbacks,
&internals.pane_outer,
PaneEmptyReason::ClosedLastTab,
);
}
}

pub fn refresh_terminal_displays_in_root(root: &gtk::Widget) {
for internals in pane_internals_for_root(root) {
for entry in &internals.tab_state.borrow().tabs {
Expand Down
62 changes: 49 additions & 13 deletions rust/limux-host-linux/src/terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,15 +412,29 @@ fn request_terminal_focus(gl_area: &gtk::GLArea, had_focus: &Cell<bool>) {
gl_area.grab_focus();
}

fn refresh_surface_display(surface: ghostty_surface_t, gl_area: &gtk::GLArea) {
/// Convert a GLArea's logical (CSS-pixel) allocation to physical (device) pixels.
fn gl_area_device_size(gl_area: &gtk::GLArea) -> (u32, u32) {
let alloc = gl_area.allocation();
let w = alloc.width() as u32;
let h = alloc.height() as u32;
if w > 0 && h > 0 {
let scale = gl_area.scale_factor() as f64;
let w = (alloc.width() as f64 * scale).round().max(0.0) as u32;
let h = (alloc.height() as f64 * scale).round().max(0.0) as u32;
(w, h)
Comment thread
tnfssc marked this conversation as resolved.
}

/// Push the current content scale and physical size into Ghostty.
/// `device_width` and `device_height` must be in physical (device) pixels,
/// not CSS/logical pixels.
fn refresh_surface_display(
surface: ghostty_surface_t,
gl_area: &gtk::GLArea,
device_width: u32,
device_height: u32,
) {
if device_width > 0 && device_height > 0 {
let scale = gl_area.scale_factor() as f64;
unsafe {
ghostty_surface_set_content_scale(surface, scale, scale);
ghostty_surface_set_size(surface, w, h);
ghostty_surface_set_size(surface, device_width, device_height);
}
}
unsafe { ghostty_surface_refresh(surface) };
Expand All @@ -434,7 +448,8 @@ fn refresh_realized_surface_display(surface: ghostty_surface_t, gl_area: &gtk::G
unsafe { ghostty_surface_display_realized(surface) };
}
}
refresh_surface_display(surface, gl_area);
let (w, h) = gl_area_device_size(gl_area);
refresh_surface_display(surface, gl_area, w, h);
}

fn clear_ghostty_preedit(surface: ghostty_surface_t) {
Expand Down Expand Up @@ -1380,13 +1395,11 @@ pub fn create_terminal(
}
}

// Set initial size — GLArea gives unscaled CSS pixels,
// Ghostty handles scaling internally via content_scale.
let alloc = gl_area.allocation();
let w = alloc.width() as u32;
let h = alloc.height() as u32;
// Set initial size — GLArea allocation is CSS (logical) pixels,
// but ghostty_surface_set_size expects physical (device) pixels.
let (w, h) = gl_area_device_size(gl_area);
if w > 0 && h > 0 {
refresh_surface_display(surface, gl_area);
refresh_surface_display(surface, gl_area, w, h);
}

let surface_key = surface as usize;
Expand Down Expand Up @@ -1483,7 +1496,8 @@ pub fn create_terminal(
let w = width as u32;
let h = height as u32;
if w > 0 && h > 0 {
refresh_surface_display(surface, gl_area);
// GLArea::resize passes physical (device) pixels directly.
refresh_surface_display(surface, gl_area, w, h);
}
}

Expand All @@ -1496,6 +1510,28 @@ pub fn create_terminal(
});
}

// On scale-factor change: update Ghostty's content scale and size.
// GTK4 usually emits resize when the backing surface resizes, but
// fractional-scale transitions can leave the allocation unchanged
// while the device-pixel size changes, so we refresh explicitly here.
{
let surface_cell = surface_cell.clone();
let last_device_size: Rc<Cell<Option<(u32, u32)>>> = Rc::new(Cell::new(None));
gl_area.connect_scale_factor_notify(move |gl_area| {
if let Some(surface) = *surface_cell.borrow() {
let (w, h) = gl_area_device_size(gl_area);
if w > 0 && h > 0 {
let size = (w, h);
if last_device_size.get() == Some(size) {
return;
}
last_device_size.set(Some(size));
refresh_surface_display(surface, gl_area, w, h);
}
}
});
}

// Keyboard input
//
// Send key events with the text field populated. Ghostty uses the
Expand Down
24 changes: 12 additions & 12 deletions rust/limux-host-linux/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5098,10 +5098,6 @@ fn split_pane(
Some(new_pane.upcast())
}

fn remove_pane(state: &State, ws_id: &str, pane_widget: &gtk::Widget) {
remove_pane_internal(state, ws_id, pane_widget, true);
}

fn remove_pane_internal(state: &State, ws_id: &str, pane_widget: &gtk::Widget, persist: bool) {
let container = {
let s = state.borrow();
Expand Down Expand Up @@ -5386,15 +5382,19 @@ fn cycle_focused_pane_tab(state: &State, delta: i32) {
}

fn close_focused_tab(state: &State) {
if let Some((ws_id, pane_widget)) = find_focused_pane(state) {
let parent = pane_widget.parent();
// If this is the only pane (parent is Stack), don't close — keep workspace alive
if let Some(ref p) = parent {
if p.downcast_ref::<gtk::Stack>().is_some() {
return;
}
let Some((_ws_id, pane_widget)) = find_focused_pane(state) else {
return;
};
// If this is the only pane in the workspace, keep it alive when it has
// a single tab — mirroring the original guard that prevented removing the
// last pane and tearing down the workspace.
if let Some(ref p) = pane_widget.parent() {
if p.downcast_ref::<gtk::Stack>().is_some() && pane::tab_count_in_pane(&pane_widget) <= 1 {
return;
}
remove_pane(state, &ws_id, &pane_widget);
}
if let Some(tab_id) = pane::active_tab_in_pane(&pane_widget) {
pane::close_tab_in_pane(&pane_widget, &tab_id);
}
}
Comment thread
tnfssc marked this conversation as resolved.

Expand Down