use std::sync::Arc;
use crate::callback::{Callback, KeyHandler};
use crate::core::node::{
NodeId, ScrollbarZone, ScrollbarZonesParams, WidgetNode, compute_scrollbar_zones,
};
use crate::style::{
BorderStyle, CaretShape, Color, Padding, Rect, ScrollbarVariant, Span, Style, StyleSlot, Theme,
ThemeRole,
};
use crate::widgets::ScrollEvent;
use super::events::{
MouseModeState, TerminalInputEvent, TerminalKeyModes, TerminalPasteShortcutBehavior,
};
#[cfg(feature = "terminal-images")]
use super::graphics::TerminalImagePlacement;
use super::layout::terminal_content_layout;
use super::screen::{TerminalDecoration, TerminalRenderSnapshot, TerminalScreenHandle};
use super::selection::{
ScrollbackLineage, TerminalSelection, TerminalSelectionEvent, rebase_selection,
};
#[derive(Clone)]
pub(crate) struct TerminalNode {
pub lines: Arc<[Vec<Span>]>,
pub cursor_row: u16,
pub cursor_col: u16,
pub cursor_visible: bool,
pub cursor_shape: CaretShape,
pub cursor_blinking: bool,
pub caret_color: Option<Color>,
pub selection: Option<TerminalSelection>,
pub selection_controlled: bool,
pub lineage: ScrollbackLineage,
pub selection_style: StyleSlot,
pub mouse_mode: MouseModeState,
pub key_modes: TerminalKeyModes,
#[cfg(feature = "terminal-images")]
pub images: Arc<[TerminalImagePlacement]>,
pub screen: Option<TerminalScreenHandle>,
pub decorations: Arc<[TerminalDecoration]>,
pub live_sequence: Option<u64>,
pub show_cursor_requested: bool,
pub paste_shortcut_behavior: TerminalPasteShortcutBehavior,
pub on_selection: Option<Callback<TerminalSelectionEvent>>,
pub on_mouse_forward: Option<Callback<Vec<u8>>>,
pub style: Style,
pub hover_style: StyleSlot,
pub focus_style: StyleSlot,
pub focus_content_style: Style,
pub border: bool,
pub border_style: BorderStyle,
pub padding: Padding,
pub scrollback_offset: usize,
pub snapshot_scrollback_offset: usize,
pub total_scrollback_rows: usize,
pub viewport_rows: usize,
pub viewport_cols: usize,
pub scroll_wheel: bool,
pub scroll_override: Option<usize>,
pub scrollbar: bool,
pub scrollbar_variant: ScrollbarVariant,
pub scrollbar_gap: u16,
pub scrollbar_thumb: Option<char>,
pub scrollbar_thumb_style: Option<Style>,
pub scrollbar_thumb_focus_style: Option<Style>,
pub scrollbar_track_style: Option<Style>,
pub on_scroll: Option<Callback<ScrollEvent>>,
pub on_scroll_to: Option<Callback<usize>>,
pub focusable: bool,
pub tab_stop: bool,
pub on_focus: Option<Callback<()>>,
pub on_blur: Option<Callback<()>>,
pub on_key: Option<KeyHandler>,
pub on_input: Option<Callback<TerminalInputEvent>>,
}
impl TerminalNode {
pub(crate) fn refresh_from_live_screen(&mut self) -> bool {
let Some(screen) = self.screen.clone() else {
return false;
};
let snapshot = screen.snapshot();
let snapshot = if self.decorations.is_empty() {
snapshot
} else {
snapshot.decorated(&self.decorations)
};
if self.live_sequence == Some(snapshot.sequence) {
return false;
}
self.live_sequence = Some(snapshot.sequence);
self.apply_snapshot(&snapshot);
true
}
fn apply_snapshot(&mut self, snapshot: &TerminalRenderSnapshot) {
let to = ScrollbackLineage {
evicted_lines: snapshot.evicted_lines,
history_epoch: snapshot.history_epoch,
};
self.selection = rebase_selection(self.selection, self.lineage, to);
self.lineage = to;
self.lines = snapshot.color_lines.clone();
self.cursor_row = snapshot.cursor_row;
self.cursor_col = snapshot.cursor_col;
self.cursor_visible = snapshot.cursor_visible && self.show_cursor_requested;
self.cursor_shape = snapshot.cursor_shape;
self.cursor_blinking = snapshot.cursor_blinking;
self.mouse_mode = snapshot.mouse_mode;
self.key_modes = snapshot.key_modes;
#[cfg(feature = "terminal-images")]
{
self.images = snapshot.images.clone();
}
self.total_scrollback_rows = snapshot.total_scrollback_rows;
if snapshot.scrollback_offset != self.snapshot_scrollback_offset {
self.snapshot_scrollback_offset = snapshot.scrollback_offset;
self.scroll_override = None;
}
self.scrollback_offset = self.scroll_override.unwrap_or(snapshot.scrollback_offset);
}
}
pub(crate) fn apply_terminal_selection_input(
selection: &mut Option<TerminalSelection>,
controlled: bool,
proposed: Option<TerminalSelection>,
) {
if !controlled {
*selection = proposed;
}
}
impl WidgetNode for TerminalNode {
fn is_focusable(&self) -> bool {
self.focusable
}
fn is_tab_stop(&self) -> bool {
self.focusable && self.tab_stop
}
fn on_focus_callback(&self) -> Option<&Callback<()>> {
self.on_focus.as_ref()
}
fn on_blur_callback(&self) -> Option<&Callback<()>> {
self.on_blur.as_ref()
}
fn has_on_click(&self) -> bool {
self.on_scroll.is_some()
|| self.on_scroll_to.is_some()
|| self.scrollbar
|| self.scroll_wheel
}
fn is_hoverable(&self) -> bool {
self.hover_style.has_explicit_style()
}
fn is_hoverable_for_theme(&self, theme: &Theme) -> bool {
self.hover_style.resolves_non_empty(theme, ThemeRole::Hover)
}
fn scrollbar_zones(
&self,
id: NodeId,
rect: Rect,
parent_border_x: Option<i16>,
_parent_border_y: Option<i16>,
) -> Vec<ScrollbarZone> {
if !self.scrollbar {
return Vec::new();
}
let inner = rect.inner(self.border, self.padding);
if inner.w == 0 || inner.h == 0 {
return Vec::new();
}
let layout = terminal_content_layout(
inner,
self.border,
self.scrollbar,
self.scrollbar_variant,
self.scrollbar_gap,
self.total_scrollback_rows,
parent_border_x.is_some(),
);
if !layout.scrollbar_visible {
return Vec::new();
}
compute_scrollbar_zones(ScrollbarZonesParams {
id,
rect,
inner,
border: self.border,
scrollbar: self.scrollbar,
scrollbar_variant: self.scrollbar_variant,
scrollbar_gap: self.scrollbar_gap,
h_scrollbar: false,
h_scrollbar_variant: ScrollbarVariant::default(),
content_x: inner.x,
content_width: inner.w,
max_content_width: 0,
wrap: false,
parent_border_x,
parent_border_y: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widgets::terminal::selection::TerminalPos;
fn selection(col: usize) -> TerminalSelection {
TerminalSelection {
anchor: TerminalPos { line: 1, col },
cursor: TerminalPos { line: 1, col },
}
}
#[test]
fn controlled_selection_ignores_mouse_selection_input() {
let original = selection(4);
let mut current = Some(original);
apply_terminal_selection_input(&mut current, true, None);
assert_eq!(current, Some(original));
apply_terminal_selection_input(&mut current, true, Some(selection(8)));
assert_eq!(current, Some(original));
}
#[test]
fn uncontrolled_selection_accepts_mouse_selection_input() {
let mut current = Some(selection(4));
apply_terminal_selection_input(&mut current, false, None);
assert_eq!(current, None);
}
}