use std::time::SystemTime;
use anyhow::Result;
use ascii_agents_core::sprite::format::Pack;
use ascii_agents_core::sprite::{Rgb, RgbBuffer};
use ascii_agents_core::state::SceneState;
use ascii_agents_core::Renderer;
use ratatui::backend::Backend;
use ratatui::Terminal;
use ratatui::layout::Rect;
use crate::tui::floor::{build_floor_scene, num_floors, FloorCtx, FloorMeta, FloorTransition};
use crate::tui::layout::{Layout, Point, MAX_VISIBLE_DESKS};
use crate::tui::pathfind::Router;
use crate::tui::pixel_painter::render_to_rgb_buffer;
use crate::tui::renderer::{draw_scene, flush_buffer_to_term_at_offset, CatPetState, DrawCtx};
pub struct TuiRenderer<B: Backend> {
pub terminal: Terminal<B>,
floor_bufs: Vec<RgbBuffer>,
floor_ctxs: Vec<FloorCtx>,
current_floor: usize,
transition: Option<FloorTransition>,
mouse_pos: Option<(u16, u16)>,
pinned_agent: Option<ascii_agents_core::AgentId>,
pub ticker: crate::tui::renderer::TickerQueue,
theme: &'static crate::tui::theme::Theme,
theme_picker: Option<usize>,
cached_layout: Option<Layout>,
cat_pet: Option<CatPetState>,
last_cat_pos: Option<(Point, &'static str)>,
chitchat_state: std::collections::HashMap<(usize, usize), crate::tui::chitchat::ActiveChitchat>,
coffee_holders: std::collections::HashSet<ascii_agents_core::AgentId>,
coffee_fetched_at: std::collections::HashMap<ascii_agents_core::AgentId, SystemTime>,
}
impl<B: Backend> TuiRenderer<B> {
pub fn new(terminal: Terminal<B>, theme: &'static crate::tui::theme::Theme) -> Self {
Self {
terminal,
floor_bufs: vec![RgbBuffer::filled(0, 0, Rgb(0, 0, 0))],
floor_ctxs: vec![FloorCtx::new()],
current_floor: 0,
transition: None,
mouse_pos: None,
pinned_agent: None,
ticker: crate::tui::renderer::TickerQueue::new(),
theme,
theme_picker: None,
cached_layout: None,
cat_pet: None,
last_cat_pos: None,
chitchat_state: std::collections::HashMap::new(),
coffee_holders: std::collections::HashSet::new(),
coffee_fetched_at: std::collections::HashMap::new(),
}
}
pub fn current_floor(&self) -> usize {
self.current_floor
}
pub fn cached_layout(&self) -> Option<&Layout> {
self.cached_layout.as_ref()
}
pub fn current_floor_seed(&self) -> u64 {
let nf = self.floor_ctxs.len();
FloorMeta::for_floor(self.current_floor, nf).floor_seed
}
pub fn transition(&self) -> Option<&FloorTransition> {
self.transition.as_ref()
}
pub fn navigate_floor(&mut self, target: usize, now: SystemTime) {
if target == self.current_floor || self.transition.is_some() {
return;
}
self.set_pinned_agent(None);
self.transition = Some(FloorTransition::new(self.current_floor, target, now));
}
pub fn cancel_transition(&mut self) {
self.transition = None;
}
pub fn set_mouse_pos(&mut self, pos: Option<(u16, u16)>) {
self.mouse_pos = pos;
}
pub fn pinned_agent(&self) -> Option<ascii_agents_core::AgentId> {
self.pinned_agent
}
pub fn set_pinned_agent(&mut self, id: Option<ascii_agents_core::AgentId>) {
self.pinned_agent = id;
}
pub fn buf(&self) -> &RgbBuffer {
&self.floor_bufs[self.current_floor]
}
pub fn set_theme(&mut self, theme: &'static crate::tui::theme::Theme) {
if !std::ptr::eq(self.theme, theme) {
self.theme = theme;
for ctx in &mut self.floor_ctxs {
ctx.cache = crate::tui::frame_cache::FrameCache::new();
}
}
}
pub fn set_theme_picker(&mut self, picker: Option<usize>) {
self.theme_picker = picker;
}
pub fn set_cat_pet(&mut self, pet: Option<CatPetState>) {
self.cat_pet = pet;
}
pub fn cat_pet(&self) -> Option<&CatPetState> {
self.cat_pet.as_ref()
}
pub fn cached_cat_pos(&self) -> Option<(Point, &'static str)> {
self.last_cat_pos
}
pub fn evict_missing(&mut self, scene: &SceneState) {
for ctx in &mut self.floor_ctxs {
ctx.cache.evict_missing(scene);
}
}
pub fn invalidate_routes(&mut self) {
for ctx in &mut self.floor_ctxs {
ctx.router.invalidate();
}
}
}
impl<B: Backend> Renderer for TuiRenderer<B> {
fn render(&mut self, scene: &SceneState, pack: &Pack, now: SystemTime) -> Result<()> {
if self.cat_pet.as_ref().is_some_and(|p| !p.is_active(now)) {
self.cat_pet = None;
}
self.ticker.update(scene);
let nf = num_floors(scene).min(crate::tui::floor::MAX_FLOORS);
while self.floor_bufs.len() < nf {
self.floor_bufs.push(RgbBuffer::filled(0, 0, Rgb(0, 0, 0)));
}
while self.floor_ctxs.len() < nf {
self.floor_ctxs.push(FloorCtx::new());
}
if let Some(ref tr) = self.transition {
if tr.from_floor >= nf || tr.to_floor >= nf {
self.transition = None;
}
}
if let Some(ref tr) = self.transition {
if tr.is_done(now) {
self.current_floor = tr.to_floor;
self.transition = None;
}
}
if self.current_floor >= nf {
self.current_floor = nf.saturating_sub(1);
}
let floor_info = if nf > 1 {
Some((self.current_floor + 1, nf))
} else {
None
};
if let Some(ref tr) = self.transition {
let from_floor = tr.from_floor;
let to_floor = tr.to_floor;
let t = tr.t(now);
let going_down = to_floor > from_floor;
let (from_agents, from_dpf) = build_floor_scene(scene, from_floor);
let mut from_scene = SceneState::new(from_dpf);
for a in from_agents {
from_scene.agents.insert(a.agent_id, a);
}
let (to_agents, to_dpf) = build_floor_scene(scene, to_floor);
let mut to_scene = SceneState::new(to_dpf);
for a in to_agents {
to_scene.agents.insert(a.agent_id, a);
}
let term_size = self.terminal.size()?;
let full_rect = Rect {
x: 0,
y: 0,
width: term_size.width,
height: term_size.height,
};
let scene_rect = Rect {
x: 0,
y: 0,
width: full_rect.width,
height: full_rect.height.saturating_sub(1),
};
let buf_w = scene_rect.width;
let buf_h = scene_rect.height * 2;
let (lo, hi) = if from_floor < to_floor {
(from_floor, to_floor)
} else {
(to_floor, from_floor)
};
let (bufs_lo, bufs_hi) = self.floor_bufs.split_at_mut(hi);
let lo_buf = &mut bufs_lo[lo];
let hi_buf = &mut bufs_hi[0];
let (from_buf, to_buf) = if from_floor < to_floor {
(lo_buf, hi_buf)
} else {
(hi_buf, lo_buf)
};
let (ctxs_lo, ctxs_hi) = self.floor_ctxs.split_at_mut(hi);
let lo_ctx = &mut ctxs_lo[lo];
let hi_ctx = &mut ctxs_hi[0];
let (from_ctx, to_ctx) = if from_floor < to_floor {
(lo_ctx, hi_ctx)
} else {
(hi_ctx, lo_ctx)
};
from_buf.ensure_size(buf_w, buf_h, self.theme.surface.bg_fallback);
to_buf.ensure_size(buf_w, buf_h, self.theme.surface.bg_fallback);
let from_meta = FloorMeta::for_floor(from_floor, nf);
let to_meta = FloorMeta::for_floor(to_floor, nf);
let mut transition_chitchat = std::collections::HashMap::new();
let empty_coffee = std::collections::HashSet::new();
let empty_fetched = std::collections::HashMap::new();
if let Some(layout) =
Layout::compute_with_seed(buf_w, buf_h, MAX_VISIBLE_DESKS, from_meta.floor_seed)
{
from_ctx.router.set_preferred_zone(layout.corridor);
let _ = render_to_rgb_buffer(
&from_scene,
&layout,
pack,
now,
from_buf,
&mut from_ctx.cache,
&mut from_ctx.router,
&mut from_ctx.overlay,
&mut from_ctx.history,
self.theme,
from_meta,
None,
&mut transition_chitchat,
&empty_coffee,
&empty_fetched,
);
}
if let Some(layout) =
Layout::compute_with_seed(buf_w, buf_h, MAX_VISIBLE_DESKS, to_meta.floor_seed)
{
to_ctx.router.set_preferred_zone(layout.corridor);
let _ = render_to_rgb_buffer(
&to_scene,
&layout,
pack,
now,
to_buf,
&mut to_ctx.cache,
&mut to_ctx.router,
&mut to_ctx.overlay,
&mut to_ctx.history,
self.theme,
to_meta,
None,
&mut transition_chitchat,
&empty_coffee,
&empty_fetched,
);
}
let h = scene_rect.height as f32;
let divider_h = (scene_rect.height as f32) / 5.0;
let total = h + divider_h;
let (from_offset, to_offset) = if going_down {
let from_y = (t * total) as i32;
let to_y = -(total - t * total) as i32;
(from_y, to_y)
} else {
let from_y = -(t * total) as i32;
let to_y = (total - t * total) as i32;
(from_y, to_y)
};
let theme = self.theme;
let theme_picker = self.theme_picker;
self.terminal.draw(|f| {
crate::tui::renderer::paint_footer(f, scene, full_rect, theme, floor_info);
flush_buffer_to_term_at_offset(f, from_buf, scene_rect, from_offset);
flush_buffer_to_term_at_offset(f, to_buf, scene_rect, to_offset);
if let Some(idx) = theme_picker {
crate::tui::renderer::paint_theme_picker(f, idx, full_rect, theme);
}
})?;
self.cached_layout = None;
return Ok(());
}
let (floor_agents, desks_per_floor) = build_floor_scene(scene, self.current_floor);
let mut floor_scene = SceneState::new(desks_per_floor);
for agent in floor_agents {
floor_scene.agents.insert(agent.agent_id, agent);
}
self.coffee_holders
.retain(|id| scene.agents.contains_key(id));
self.coffee_fetched_at
.retain(|id, _| scene.agents.contains_key(id));
let fctx = &mut self.floor_ctxs[self.current_floor];
let mut draw_ctx = DrawCtx {
buf: &mut self.floor_bufs[self.current_floor],
cache: &mut fctx.cache,
router: &mut fctx.router,
overlay: &mut fctx.overlay,
history: &mut fctx.history,
mouse_pos: self.mouse_pos,
pinned_agent: self.pinned_agent,
ticker: &self.ticker,
theme: self.theme,
theme_picker: self.theme_picker,
floor_info,
floor: FloorMeta::for_floor(self.current_floor, nf),
cat_pet: self.cat_pet.as_ref(),
last_cat_pos: None,
chitchat_state: &mut self.chitchat_state,
chitchat_bubbles: Vec::new(),
coffee_holders: &self.coffee_holders,
coffee_fetched_at: &self.coffee_fetched_at,
new_coffee_carriers: Vec::new(),
};
let result = draw_scene(&mut self.terminal, &floor_scene, pack, now, &mut draw_ctx);
self.last_cat_pos = draw_ctx.last_cat_pos;
for id in draw_ctx.new_coffee_carriers {
if self.coffee_holders.insert(id) {
self.coffee_fetched_at.insert(id, now);
}
}
if let Ok(ref layout_opt) = result {
self.cached_layout = layout_opt.clone();
}
result.map(|_| ())
}
}