use super::layout::{terminal_content_layout, terminal_lines};
use super::mod_private::Terminal;
use super::node::TerminalNode;
use super::screen::TerminalViewport;
use super::selection::{ScrollbackLineage, rebase_selection};
use crate::core::node::{NodeId, NodeKind, NodeTree};
use crate::style::{LayoutConstraints, Rect, ScrollbarVariant};
pub(crate) fn reconcile_terminal(
tree: &mut NodeTree,
id: NodeId,
terminal: &Terminal,
rect: Rect,
constraints: &LayoutConstraints,
) -> NodeId {
let live = terminal.screen.as_ref().map(|screen| {
let snapshot = screen.snapshot();
if terminal.decorations.is_empty() {
snapshot
} else {
snapshot.decorated(&terminal.decorations)
}
});
let lines = match &live {
Some(snapshot) => snapshot.color_lines.clone(),
None => terminal
.color_lines
.clone()
.unwrap_or_else(|| terminal_lines(terminal.content.as_ref())),
};
let total_scrollback_rows = live
.as_ref()
.map_or(terminal.total_scrollback_rows, |snapshot| {
snapshot.total_scrollback_rows
});
let snapshot_scrollback_offset = live
.as_ref()
.map_or(terminal.scrollback_offset, |snapshot| {
snapshot.scrollback_offset
});
let avail_w = rect.w;
let avail_h = rect.h;
let mut rect = rect;
let parent_v_edge = tree.parent_frame_integrated_v_edge(id).unwrap_or(false);
let scrollbar_visible = terminal.scrollbar && total_scrollback_rows > 0;
let scrollbar_integrated = scrollbar_visible
&& matches!(terminal.scrollbar_variant, ScrollbarVariant::Integrated)
&& (terminal.border || parent_v_edge);
let _scrollbar_cols = if scrollbar_visible && !scrollbar_integrated {
1
} else {
0
};
rect.w = constraints.clamp_width(rect.w, avail_w);
rect.h = constraints.clamp_height(rect.h, avail_h);
let inner = rect.inner(terminal.border, terminal.padding);
let layout = terminal_content_layout(
inner,
terminal.border,
terminal.scrollbar,
terminal.scrollbar_variant,
terminal.scrollbar_gap,
total_scrollback_rows,
parent_v_edge,
);
let viewport_rows = layout.content_rect.h as usize;
let viewport_cols = layout.content_rect.w as usize;
let (
old_scroll_override,
old_snapshot_scrollback_offset,
old_selection,
old_lineage,
old_viewport_rows,
old_viewport_cols,
) = if let NodeKind::Terminal(old) = &tree.node(id).kind {
(
old.scroll_override,
old.snapshot_scrollback_offset,
old.selection,
old.lineage,
old.viewport_rows,
old.viewport_cols,
)
} else {
(
None,
snapshot_scrollback_offset,
None,
ScrollbackLineage::default(),
0,
0,
)
};
let snapshot_offset_changed = snapshot_scrollback_offset != old_snapshot_scrollback_offset;
let scroll_override = if snapshot_offset_changed {
None
} else {
old_scroll_override
};
let scrollback_offset = scroll_override.unwrap_or(snapshot_scrollback_offset);
let snapshot_lineage = live
.as_ref()
.map_or(ScrollbackLineage::default(), |snapshot| ScrollbackLineage {
evicted_lines: snapshot.evicted_lines,
history_epoch: snapshot.history_epoch,
});
let selection = {
let candidate = if terminal.selection_controlled {
terminal.selection
} else {
old_selection.or(terminal.selection)
};
rebase_selection(candidate, old_lineage, snapshot_lineage)
};
if let Some(cb) = terminal.on_resize.as_ref()
&& (viewport_cols != old_viewport_cols || viewport_rows != old_viewport_rows)
{
let cols = viewport_cols.max(1).min(u16::MAX as usize) as u16;
let rows = viewport_rows.max(1).min(u16::MAX as usize) as u16;
cb.emit(TerminalViewport { cols, rows });
}
let node = tree.node_mut(id);
node.rect = rect;
node.children.clear();
node.kind = NodeKind::Terminal(TerminalNode {
lines,
cursor_row: live.as_ref().map_or(terminal.cursor_row, |s| s.cursor_row),
cursor_col: live.as_ref().map_or(terminal.cursor_col, |s| s.cursor_col),
cursor_visible: live.as_ref().map_or(terminal.show_cursor, |s| {
s.cursor_visible && terminal.show_cursor
}),
cursor_shape: live
.as_ref()
.map_or(terminal.cursor_shape, |s| s.cursor_shape),
cursor_blinking: live
.as_ref()
.map_or(terminal.cursor_blinking, |s| s.cursor_blinking),
caret_color: terminal.caret_color,
selection,
selection_controlled: terminal.selection_controlled,
lineage: snapshot_lineage,
selection_style: terminal.selection_style,
mouse_mode: live.as_ref().map_or(terminal.mouse_mode, |s| s.mouse_mode),
key_modes: live.as_ref().map_or(terminal.key_modes, |s| s.key_modes),
#[cfg(feature = "terminal-images")]
images: live
.as_ref()
.map_or_else(|| terminal.images.clone(), |s| s.images.clone()),
screen: terminal.screen.clone(),
decorations: terminal.decorations.clone(),
live_sequence: live.as_ref().map(|s| s.sequence),
show_cursor_requested: terminal.show_cursor,
paste_shortcut_behavior: terminal.paste_shortcut_behavior,
on_selection: terminal.on_selection.clone(),
on_mouse_forward: terminal.on_mouse_forward.clone(),
style: terminal.style,
hover_style: terminal.hover_style,
focus_style: terminal.focus_style,
focus_content_style: terminal.focus_content_style,
border: terminal.border,
border_style: terminal.border_style,
padding: terminal.padding,
scrollback_offset,
snapshot_scrollback_offset,
total_scrollback_rows,
viewport_rows,
viewport_cols,
scroll_wheel: terminal.scroll_wheel,
scroll_override,
scrollbar: terminal.scrollbar,
scrollbar_variant: terminal.scrollbar_variant,
scrollbar_gap: terminal.scrollbar_gap,
scrollbar_thumb: terminal.scrollbar_thumb,
scrollbar_thumb_style: terminal.scrollbar_thumb_style,
scrollbar_thumb_focus_style: terminal.scrollbar_thumb_focus_style,
scrollbar_track_style: terminal.scrollbar_track_style,
on_scroll: terminal.on_scroll.clone(),
on_scroll_to: terminal.on_scroll_to.clone(),
focusable: terminal.focusable,
tab_stop: terminal.tab_stop,
on_focus: terminal.on_focus.clone(),
on_blur: terminal.on_blur.clone(),
on_key: terminal.on_key.clone(),
on_input: terminal.on_input.clone(),
});
tree.register_scrollbar_zone(id);
id
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Element;
use crate::layout::LayoutEngine;
use crate::widgets::{Terminal, TerminalRenderSnapshot};
fn reconcile(tree: &mut NodeTree, scrollback_offset: usize) {
let terminal: Element = Terminal::new()
.snapshot(TerminalRenderSnapshot {
scrollback_offset,
total_scrollback_rows: 100,
..TerminalRenderSnapshot::default()
})
.into();
LayoutEngine::reconcile_with_focus(
tree,
&terminal,
Rect {
x: 0,
y: 0,
w: 80,
h: 24,
},
None,
);
}
#[test]
fn changed_snapshot_offset_clears_mouse_scroll_override() {
let mut tree = NodeTree::new();
reconcile(&mut tree, 10);
let NodeKind::Terminal(node) = &mut tree.node_mut(tree.root).kind else {
panic!("expected terminal root");
};
node.scrollback_offset = 20;
node.scroll_override = Some(20);
reconcile(&mut tree, 0);
let NodeKind::Terminal(node) = &tree.node(tree.root).kind else {
panic!("expected terminal root");
};
assert_eq!(node.scrollback_offset, 0);
assert_eq!(node.scroll_override, None);
assert_eq!(node.snapshot_scrollback_offset, 0);
}
#[test]
fn unchanged_snapshot_preserves_pending_mouse_scroll_override() {
let mut tree = NodeTree::new();
reconcile(&mut tree, 10);
let NodeKind::Terminal(node) = &mut tree.node_mut(tree.root).kind else {
panic!("expected terminal root");
};
node.scrollback_offset = 20;
node.scroll_override = Some(20);
reconcile(&mut tree, 10);
let NodeKind::Terminal(node) = &tree.node(tree.root).kind else {
panic!("expected terminal root");
};
assert_eq!(node.scrollback_offset, 20);
assert_eq!(node.scroll_override, Some(20));
}
}