group tui_engine and tui_content; cargo update

This commit is contained in:
🪞👃🪞 2025-03-16 23:33:27 +02:00
parent 877b344765
commit 71dead5150
13 changed files with 150 additions and 72 deletions

View file

@ -0,0 +1,46 @@
use crate::*;
pub fn buffer_update (buf: &mut Buffer, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
for row in 0..area.h() {
let y = area.y() + row;
for col in 0..area.w() {
let x = area.x() + col;
if x < buf.area.width && y < buf.area.height {
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
callback(cell, col, row);
}
}
}
}
}
#[derive(Default)] pub struct BigBuffer {
pub width: usize,
pub height: usize,
pub content: Vec<Cell>
}
impl std::fmt::Debug for BigBuffer {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len())
}
}
impl BigBuffer {
pub fn new (width: usize, height: usize) -> Self {
Self { width, height, content: vec![Cell::default(); width*height] }
}
pub fn get (&self, x: usize, y: usize) -> Option<&Cell> {
let i = self.index_of(x, y);
self.content.get(i)
}
pub fn get_mut (&mut self, x: usize, y: usize) -> Option<&mut Cell> {
let i = self.index_of(x, y);
self.content.get_mut(i)
}
pub fn index_of (&self, x: usize, y: usize) -> usize {
y * self.width + x
}
}
from!(|size:(usize, usize)| BigBuffer = Self::new(size.0, size.1));

View file

@ -0,0 +1,147 @@
use crate::*;
use std::time::Duration;
use std::thread::{spawn, JoinHandle};
use crossterm::event::{poll, read};
#[derive(Debug, Clone)] pub struct TuiIn(pub Arc<AtomicBool>, pub Event);
impl Input for TuiIn {
type Event = Event;
type Handled = bool;
fn event (&self) -> &Event { &self.1 }
fn is_done (&self) -> bool { self.0.fetch_and(true, Relaxed) }
fn done (&self) { self.0.store(true, Relaxed); }
}
impl TuiIn {
/// Spawn the input thread.
pub fn run_input <T: Handle<TuiIn> + 'static> (
engine: &Arc<RwLock<Tui>>,
state: &Arc<RwLock<T>>,
timer: Duration
) -> JoinHandle<()> {
let exited = engine.read().unwrap().exited.clone();
let state = state.clone();
spawn(move || loop {
if exited.fetch_and(true, Relaxed) {
break
}
if poll(timer).is_ok() {
let event = read().unwrap();
match event {
crossterm::event::Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
state: KeyEventState::NONE
}) => {
exited.store(true, Relaxed);
},
_ => {
let exited = exited.clone();
if let Err(e) = state.write().unwrap().handle(&TuiIn(exited, event)) {
panic!("{e}")
}
}
}
}
})
}
}
impl AtomInput for TuiIn {
fn matches_atom (&self, token: &str) -> bool {
if let Some(event) = KeyMatcher::new(token).build() {
&event == self.event()
} else {
false
}
}
//fn get_event (item: &AtomItem<impl AsRef<str>>) -> Option<Event> {
//match item { AtomItem::Sym(s) => KeyMatcher::new(s).build(), _ => None }
//}
}
struct KeyMatcher {
valid: bool,
key: Option<KeyCode>,
mods: KeyModifiers,
}
impl KeyMatcher {
fn new (token: impl AsRef<str>) -> Self {
let token = token.as_ref();
if token.len() < 2 {
Self { valid: false, key: None, mods: KeyModifiers::NONE }
} else if token.chars().next() != Some('@') {
Self { valid: false, key: None, mods: KeyModifiers::NONE }
} else {
Self { valid: true, key: None, mods: KeyModifiers::NONE }.next(&token[1..])
}
}
fn next (mut self, token: &str) -> Self {
let mut tokens = token.split('-').peekable();
while let Some(token) = tokens.next() {
if tokens.peek().is_some() {
match token {
"ctrl" | "Ctrl" | "c" | "C" => self.mods |= KeyModifiers::CONTROL,
"alt" | "Alt" | "m" | "M" => self.mods |= KeyModifiers::ALT,
"shift" | "Shift" | "s" | "S" => {
self.mods |= KeyModifiers::SHIFT;
// + TODO normalize character case, BackTab, etc.
},
_ => panic!("unknown modifier {token}"),
}
} else {
self.key = if token.len() == 1 {
Some(KeyCode::Char(token.chars().next().unwrap()))
} else {
Some(Self::named_key(token).unwrap_or_else(||panic!("unknown character {token}")))
}
}
}
self
}
fn named_key (token: &str) -> Option<KeyCode> {
use KeyCode::*;
Some(match token {
"up" => Up,
"down" => Down,
"left" => Left,
"right" => Right,
"enter" | "return" => Enter,
"delete" | "del" => Delete,
"tab" => Tab,
"space" => Char(' '),
"comma" => Char(','),
"period" => Char('.'),
"plus" => Char('+'),
"minus" | "dash" => Char('-'),
"equal" | "equals" => Char('='),
"underscore" => Char('_'),
"backtick" => Char('`'),
"lt" => Char('<'),
"gt" => Char('>'),
"openbracket" => Char('['),
"closebracket" => Char(']'),
_ => return None,
})
}
fn build (self) -> Option<Event> {
if self.valid && self.key.is_some() {
Some(Event::Key(KeyEvent::new(self.key.unwrap(), self.mods)))
} else {
None
}
}
}
#[cfg(test)] #[test] fn test_parse_key () {
use KeyModifiers as Mods;
let test = |x: &str, y|assert_eq!(KeyMatcher::new(x).build(), Some(Event::Key(y)));
//test(":x",
//KeyEvent::new(KeyCode::Char('x'), Mods::NONE));
//test(":ctrl-x",
//KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL));
//test(":alt-x",
//KeyEvent::new(KeyCode::Char('x'), Mods::ALT));
//test(":shift-x",
//KeyEvent::new(KeyCode::Char('x'), Mods::SHIFT));
//test(":ctrl-alt-shift-x",
//KeyEvent::new(KeyCode::Char('x'), Mods::CONTROL | Mods::ALT | Mods::SHIFT ));
}

View file

@ -0,0 +1,104 @@
use crate::*;
use std::time::Duration;
use std::thread::{spawn, JoinHandle};
#[derive(Default)]
pub struct TuiOut {
pub buffer: Buffer,
pub area: [u16;4]
}
impl Output for TuiOut {
type Unit = u16;
type Size = [Self::Unit;2];
type Area = [Self::Unit;4];
#[inline] fn area (&self) -> [u16;4] { self.area }
#[inline] fn area_mut (&mut self) -> &mut [u16;4] { &mut self.area }
#[inline] fn place (&mut self, area: [u16;4], content: &impl Render<TuiOut>) {
let last = self.area();
*self.area_mut() = area;
content.render(self);
*self.area_mut() = last;
}
}
impl TuiOut {
/// Spawn the output thread.
pub fn run_output <T: Render<TuiOut> + Send + Sync + 'static> (
engine: &Arc<RwLock<Tui>>,
state: &Arc<RwLock<T>>,
timer: Duration
) -> JoinHandle<()> {
let exited = engine.read().unwrap().exited.clone();
let engine = engine.clone();
let state = state.clone();
let Size { width, height } = engine.read().unwrap().backend.size().expect("get size failed");
let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height });
spawn(move || loop {
if exited.fetch_and(true, Relaxed) {
break
}
//let t0 = engine.read().unwrap().perf.get_t0();
let Size { width, height } = engine.read().unwrap().backend.size()
.expect("get size failed");
if let Ok(state) = state.try_read() {
let size = Rect { x: 0, y: 0, width, height };
if buffer.area != size {
engine.write().unwrap().backend.clear_region(ClearType::All)
.expect("clear failed");
buffer.resize(size);
buffer.reset();
}
let mut output = TuiOut { buffer, area: [0, 0, width, height] };
state.render(&mut output);
buffer = engine.write().unwrap().flip(output.buffer, size);
}
//let t1 = engine.read().unwrap().perf.get_t1(t0).unwrap();
//buffer.set_string(0, 0, &format!("{:>3}.{:>3}ms", t1.as_millis(), t1.as_micros() % 1000), Style::default());
std::thread::sleep(timer);
})
}
pub fn buffer_update (&mut self, area: [u16;4], callback: &impl Fn(&mut Cell, u16, u16)) {
buffer_update(&mut self.buffer, area, callback);
}
pub fn fill_bold (&mut self, area: [u16;4], on: bool) {
if on {
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(Modifier::BOLD))
} else {
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(Modifier::BOLD))
}
}
pub fn fill_bg (&mut self, area: [u16;4], color: Color) {
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
}
pub fn fill_fg (&mut self, area: [u16;4], color: Color) {
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
}
pub fn fill_ul (&mut self, area: [u16;4], color: Color) {
self.buffer_update(area, &|cell,_,_|{
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
cell.underline_color = color;
})
}
pub fn fill_char (&mut self, area: [u16;4], c: char) {
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
}
pub fn make_dim (&mut self) {
for cell in self.buffer.content.iter_mut() {
cell.bg = ratatui::style::Color::Rgb(30,30,30);
cell.fg = ratatui::style::Color::Rgb(100,100,100);
cell.modifier = ratatui::style::Modifier::DIM;
}
}
pub fn blit (
&mut self, text: &impl AsRef<str>, x: u16, y: u16, style: Option<Style>
) {
let text = text.as_ref();
let buf = &mut self.buffer;
if x < buf.area.width && y < buf.area.height {
buf.set_string(x, y, text, style.unwrap_or(Style::default()));
}
}
#[inline]
pub fn with_rect (&mut self, area: [u16;4]) -> &mut Self {
self.area = area;
self
}
}

View file

@ -0,0 +1,64 @@
use crate::*;
/// Performance counter
#[derive(Debug)]
pub struct PerfModel {
pub enabled: bool,
pub clock: quanta::Clock,
// In nanoseconds. Time used by last iteration.
pub used: AtomicF64,
// In microseconds. Max prescribed time for iteration (frame, chunk...).
pub window: AtomicF64,
}
pub trait HasPerf {
fn perf (&self) -> &PerfModel;
}
impl Default for PerfModel {
fn default () -> Self {
Self {
enabled: true,
clock: quanta::Clock::new(),
used: Default::default(),
window: Default::default(),
}
}
}
impl PerfModel {
pub fn get_t0 (&self) -> Option<u64> {
if self.enabled {
Some(self.clock.raw())
} else {
None
}
}
pub fn get_t1 (&self, t0: Option<u64>) -> Option<std::time::Duration> {
if let Some(t0) = t0 {
if self.enabled {
Some(self.clock.delta(t0, self.clock.raw()))
} else {
None
}
} else {
None
}
}
pub fn update (&self, t0: Option<u64>, microseconds: f64) {
if let Some(t0) = t0 {
let t1 = self.clock.raw();
self.used.store(self.clock.delta_as_nanos(t0, t1) as f64, Relaxed);
self.window.store(microseconds, Relaxed,);
}
}
pub fn percentage (&self) -> Option<f64> {
let window = self.window.load(Relaxed) * 1000.0;
if window > 0.0 {
let used = self.used.load(Relaxed);
Some(100.0 * used / window)
} else {
None
}
}
}