214 lines
7.6 KiB
Rust
214 lines
7.6 KiB
Rust
//! Bottom-panel wire types (Q#BP15, Q#BP15a, Q#BP16).
|
|
//!
|
|
//! A panel frame is the daemon's painted projection of one side window.
|
|
//! It shares [`crate::wire_grid`]'s cell rules with
|
|
//! [`crate::terminal::TerminalFrame`] but not its per-axis PTY caps: a
|
|
//! 4K surface at a small font is legitimately wider than 512 columns,
|
|
//! and the area bound is what keeps the encoding inside the transport
|
|
//! budget.
|
|
//!
|
|
//! Presence is explicit. [`PanelFramePayload::Absent`] is authoritative
|
|
//! and must be sent on close *and* on hide, because the receiver
|
|
//! retains its last valid frame: silence would leave a stale band on
|
|
//! screen indefinitely.
|
|
|
|
use crate::cell::{Cell, CellCoord, CellSize};
|
|
use crate::ids::BufferId;
|
|
use crate::wire_grid::{
|
|
MAX_WIRE_GRID_GLYPH_BYTES, WireGridError, WireGridLimits, validate_wire_grid,
|
|
};
|
|
|
|
/// Shared visible-cell ceiling for a panel grid.
|
|
///
|
|
/// Identical to the terminal bound: it is the transport-safety limit,
|
|
/// not a PTY policy, so both messages answer to it.
|
|
pub const MAX_PANEL_VISIBLE_CELLS: usize = crate::wire_grid::MAX_WIRE_GRID_VISIBLE_CELLS;
|
|
|
|
/// Bounds a panel frame enforces on its cell grid.
|
|
///
|
|
/// The per-axis ceilings are the area bound itself rather than 512: any
|
|
/// axis larger than the area bound is already rejected by the area
|
|
/// check, so this expresses "no independent per-axis policy" without
|
|
/// leaving the multiplication unchecked.
|
|
const PANEL_GRID_LIMITS: WireGridLimits = WireGridLimits {
|
|
max_rows: MAX_PANEL_VISIBLE_CELLS as u32,
|
|
max_cols: MAX_PANEL_VISIBLE_CELLS as u32,
|
|
max_visible_cells: MAX_PANEL_VISIBLE_CELLS,
|
|
max_glyph_bytes: MAX_WIRE_GRID_GLYPH_BYTES,
|
|
};
|
|
|
|
/// The daemon's painted projection of one side window.
|
|
///
|
|
/// `panel_epoch` is opaque and monotonic per frontend: stable across
|
|
/// ordinary frames of one continuously present window/buffer, and
|
|
/// changed on buffer replacement, new side-window creation, and every
|
|
/// `Absent` → `Present` transition. That is what stops a stale
|
|
/// `PanelPointer` from addressing a reopened panel as if it were the
|
|
/// old one (Q#BP16).
|
|
///
|
|
/// `geometry_epoch` answers a *frontend* declaration and moves whenever
|
|
/// the frontend declares new effective cell geometry — including a font
|
|
/// or scale change that leaves [`CellSize`] identical, which is exactly
|
|
/// the case daemon-side value dedup cannot see (Q#BP2S1).
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub struct PanelFrame {
|
|
/// Buffer this frame projects.
|
|
pub buffer_id: BufferId,
|
|
/// Presentation identity, monotonic per frontend.
|
|
pub panel_epoch: u64,
|
|
/// The frontend geometry declaration this frame answers.
|
|
pub geometry_epoch: u64,
|
|
/// Panel grid dimensions in cells.
|
|
pub size: CellSize,
|
|
/// Row-major cells; exactly `size.area()` entries.
|
|
pub cells: Vec<Cell>,
|
|
/// Panel caret, or `None` when the panel shows no cursor.
|
|
///
|
|
/// `paint_frame` returns the cursor separately from the cells, so a
|
|
/// frame carrying cells alone would lose the caret.
|
|
pub cursor: Option<CellCoord>,
|
|
/// Whether the panel owns focus.
|
|
///
|
|
/// Presentation and focus-chrome routing only (Q#BP14b) — the
|
|
/// *keys* decision is `DispatchIdle` (Q#BP14a).
|
|
pub focused: bool,
|
|
}
|
|
|
|
/// Explicit panel presence.
|
|
///
|
|
/// `Absent` is authoritative rather than implied by silence, and is
|
|
/// duplicate-suppressed like any other payload.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
|
pub enum PanelFramePayload {
|
|
/// A panel is visible and this is its current frame.
|
|
Present(PanelFrame),
|
|
/// No panel is visible; clear any retained frame.
|
|
Absent,
|
|
}
|
|
|
|
/// Why a [`PanelFrame`] is not structurally valid.
|
|
///
|
|
/// Validation is atomic: the frame is rejected whole and the receiver
|
|
/// retains its previous valid frame.
|
|
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
|
|
pub enum PanelFrameError {
|
|
/// Rows or columns are zero or above the area-derived bounds.
|
|
#[error("panel size {rows}x{cols} is outside 1..={max_rows}x1..={max_cols}")]
|
|
Size {
|
|
/// Declared rows.
|
|
rows: u32,
|
|
/// Declared columns.
|
|
cols: u32,
|
|
/// Row bound in force.
|
|
max_rows: u32,
|
|
/// Column bound in force.
|
|
max_cols: u32,
|
|
},
|
|
/// The checked area exceeds the shared visible-cell bound.
|
|
#[error("panel area {area} exceeds the visible-cell bound {max}")]
|
|
Area {
|
|
/// Checked `rows * cols`.
|
|
area: usize,
|
|
/// Shared visible-cell bound.
|
|
max: usize,
|
|
},
|
|
/// `cells.len()` disagrees with the declared area.
|
|
#[error("panel frame carries {actual} cells for a {expected}-cell area")]
|
|
CellCount {
|
|
/// Declared area.
|
|
expected: usize,
|
|
/// Supplied cell count.
|
|
actual: usize,
|
|
},
|
|
/// The cursor lies outside the declared grid.
|
|
#[error("panel cursor ({row},{col}) is outside the {rows}x{cols} grid")]
|
|
Cursor {
|
|
/// Cursor row.
|
|
row: u32,
|
|
/// Cursor column.
|
|
col: u32,
|
|
/// Declared rows.
|
|
rows: u32,
|
|
/// Declared columns.
|
|
cols: u32,
|
|
},
|
|
/// A cell's glyph is not a legal wire glyph.
|
|
#[error("panel cell {index} has an invalid glyph: {reason}")]
|
|
Glyph {
|
|
/// Row-major cell index.
|
|
index: usize,
|
|
/// Why the glyph failed.
|
|
reason: &'static str,
|
|
},
|
|
/// A cell carries a frontend attachment, which panels never use.
|
|
#[error("panel cell {index} carries an attachment")]
|
|
Attachment {
|
|
/// Row-major cell index.
|
|
index: usize,
|
|
},
|
|
/// Aggregate glyph bytes exceed the shared budget.
|
|
#[error("panel frame glyph bytes exceed the aggregate bound {max}")]
|
|
GlyphBudget {
|
|
/// Shared aggregate bound.
|
|
max: usize,
|
|
},
|
|
/// An epoch is zero, which is reserved for "never declared".
|
|
#[error("panel {field} epoch is zero, which is reserved for 'never declared'")]
|
|
ZeroEpoch {
|
|
/// Which epoch was zero.
|
|
field: &'static str,
|
|
},
|
|
}
|
|
|
|
impl PanelFrame {
|
|
/// Check every structural rule a panel frame must satisfy.
|
|
///
|
|
/// Pure: a rejected frame mutates nothing, so callers get atomic
|
|
/// rejection for free.
|
|
pub fn validate(&self) -> Result<(), PanelFrameError> {
|
|
if self.panel_epoch == 0 {
|
|
return Err(PanelFrameError::ZeroEpoch { field: "panel" });
|
|
}
|
|
if self.geometry_epoch == 0 {
|
|
return Err(PanelFrameError::ZeroEpoch { field: "geometry" });
|
|
}
|
|
validate_wire_grid(self.size, &self.cells, self.cursor, PANEL_GRID_LIMITS)
|
|
.map_err(panel_grid_error)
|
|
}
|
|
}
|
|
|
|
/// Map a shared wire-grid failure onto this message's error type.
|
|
fn panel_grid_error(error: WireGridError) -> PanelFrameError {
|
|
match error {
|
|
WireGridError::Size {
|
|
rows,
|
|
cols,
|
|
max_rows,
|
|
max_cols,
|
|
} => PanelFrameError::Size {
|
|
rows,
|
|
cols,
|
|
max_rows,
|
|
max_cols,
|
|
},
|
|
WireGridError::Area { area, max } => PanelFrameError::Area { area, max },
|
|
WireGridError::CellCount { expected, actual } => {
|
|
PanelFrameError::CellCount { expected, actual }
|
|
}
|
|
WireGridError::Cursor {
|
|
row,
|
|
col,
|
|
rows,
|
|
cols,
|
|
} => PanelFrameError::Cursor {
|
|
row,
|
|
col,
|
|
rows,
|
|
cols,
|
|
},
|
|
WireGridError::Glyph { index, reason } => PanelFrameError::Glyph { index, reason },
|
|
WireGridError::Attachment { index } => PanelFrameError::Attachment { index },
|
|
WireGridError::GlyphBudget { max } => PanelFrameError::GlyphBudget { max },
|
|
}
|
|
}
|