use std::cmp::Reverse;
use bevy_ecs::change_detection::Tick;
use bevy_ecs::prelude::*;
use bevy_ecs::query::SpawnDetails;
use bevy_reflect::prelude::*;
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::Widget;
#[derive(SystemSet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TuiSystems {
Prepare,
Layout,
Fit,
Draw,
Render,
}
#[derive(Message, Clone, Copy, Debug, Default)]
pub struct RequestRedraw;
#[derive(Message, Clone, Copy, Debug, PartialEq, Eq)]
pub struct RedrawArea(pub Rect);
#[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TerminalSize(pub Rect);
#[derive(Component, Reflect, Clone, Debug, PartialEq, Eq)]
#[require(PanelCanvas)]
#[reflect(opaque, Component, Clone, Debug, PartialEq)]
pub struct TuiPanel {
pub placement: Placement,
}
impl TuiPanel {
pub fn new(placement: Placement) -> Self {
Self { placement }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Placement {
Top(Constraint),
Bottom(Constraint),
Left(Constraint),
Right(Constraint),
BelowInput(Constraint),
Over {
width: Constraint,
height: Constraint,
},
Rest,
Within {
panel: Entity,
part: u16,
of: u16,
},
}
#[derive(Component, Clone, Debug, Default)]
pub struct PanelCanvas {
buffer: Buffer,
}
impl PanelCanvas {
pub fn area(&self) -> Rect {
self.buffer.area
}
pub fn render(&mut self, widget: impl Widget) {
let area = self.buffer.area;
widget.render(area, &mut self.buffer);
}
pub fn buffer_mut(&mut self) -> &mut Buffer {
&mut self.buffer
}
pub(super) fn reset(&mut self, area: Rect) {
if self.buffer.area == area {
self.buffer.reset();
} else {
self.buffer = Buffer::empty(area);
}
}
pub(super) fn copy_to(&self, target: &mut Buffer) {
for position in self.buffer.area.intersection(target.area).positions() {
if let (Some(cell), Some(slot)) =
(self.buffer.cell(position), target.cell_mut(position))
{
slot.clone_from(cell);
}
}
}
}
pub(super) type PanelSpawns<'w, 's> = Query<'w, 's, SpawnDetails, With<TuiPanel>>;
pub(super) fn spawn_order(now: Tick, spawns: &PanelSpawns, panel: Entity) -> (Reverse<u32>, u32) {
let spawned = spawns.get(panel).map(SpawnDetails::spawn_tick);
let age = spawned.map_or(0, |spawned| now.get().wrapping_sub(spawned.get()));
(Reverse(age), panel.index_u32())
}
pub(super) const MIN_TRANSCRIPT: (u16, u16) = (20, 3);
pub(super) fn rows_of(size: Constraint, height: u16) -> u16 {
let [rows, _] = Layout::vertical([size, Constraint::Fill(1)]).areas(Rect::new(0, 0, 1, height));
rows.height
}
pub(super) fn below_rows(asked: impl IntoIterator<Item = u16>, mut room: u16) -> Vec<u16> {
asked
.into_iter()
.map(|rows| match room.checked_sub(rows) {
Some(left) => {
room = left;
rows
}
None => 0,
})
.collect()
}
pub(super) type ToLayOut<'a> = (Entity, &'a TuiPanel, Option<u16>, Mut<'a, PanelCanvas>);
pub(super) fn lay_out<'a>(
panels: impl Iterator<Item = ToLayOut<'a>>,
screen: Rect,
boxes: &[(Entity, Rect)],
) {
let panels: Vec<ToLayOut<'a>> = panels.collect();
let fitted: u16 = panels
.iter()
.filter(|(_, panel, ..)| !matches!(panel.placement, Placement::BelowInput(_)))
.filter_map(|(_, _, fits, _)| *fits)
.sum();
let asked = panels
.iter()
.filter_map(|(_, panel, fits, _)| match panel.placement {
Placement::BelowInput(size) => Some(fits.unwrap_or(rows_of(size, screen.height))),
_ => None,
});
let room = (screen.height.saturating_sub(fitted)).saturating_sub(MIN_TRANSCRIPT.1);
let below_rows = below_rows(asked, room);
let total: u16 = below_rows.iter().sum();
let [mut main, mut below] =
Layout::vertical([Constraint::Min(0), Constraint::Length(total)]).areas(screen);
let mut below_rows = below_rows.into_iter();
let (mut rest, mut within) = (Vec::new(), Vec::new());
let mut areas: Vec<(Entity, Rect)> = boxes.to_vec();
for (entity, panel, fits, mut canvas) in panels {
let fit = |size| fits.map_or(size, Constraint::Length);
let area = match panel.placement {
Placement::Rest => {
rest.push((entity, canvas));
continue;
}
Placement::Within { panel, part, of } => {
within.push((panel, (part, of), canvas));
continue;
}
Placement::Over { width, height } => screen.centered(width, height),
Placement::BelowInput(_) => {
let rows = below_rows.next().unwrap_or(0);
let [panel, left] =
Layout::vertical([Constraint::Length(rows), Constraint::Min(0)]).areas(below);
below = left;
if rows == 0 { Rect::default() } else { panel }
}
Placement::Top(size) => {
let [panel, rest] = Layout::vertical([fit(size), Constraint::Min(0)]).areas(main);
take(&mut main, panel, rest)
}
Placement::Bottom(size) => {
let [rest, panel] = Layout::vertical([Constraint::Min(0), fit(size)]).areas(main);
take(&mut main, panel, rest)
}
Placement::Left(size) => {
let [panel, rest] = Layout::horizontal([size, Constraint::Min(0)]).areas(main);
take(&mut main, panel, rest)
}
Placement::Right(size) => {
let [rest, panel] = Layout::horizontal([Constraint::Min(0), size]).areas(main);
take(&mut main, panel, rest)
}
};
canvas.bypass_change_detection().reset(area);
areas.push((entity, area));
}
for (entity, mut canvas) in rest {
canvas.bypass_change_detection().reset(main);
areas.push((entity, main));
}
for (host, (part, of), mut canvas) in within {
let host = areas.iter().find(|(entity, _)| *entity == host);
let host = host.map_or(Rect::default(), |(_, area)| *area);
let columns = Layout::horizontal(vec![Constraint::Fill(1); usize::from(of.max(1))]);
let column = columns.split(host).get(usize::from(part)).copied();
canvas
.bypass_change_detection()
.reset(column.unwrap_or_default());
}
}
fn take(main: &mut Rect, panel: Rect, rest: Rect) -> Rect {
if rest.width < MIN_TRANSCRIPT.0 || rest.height < MIN_TRANSCRIPT.1 {
return Rect::default();
}
*main = rest;
panel
}