mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2026-02-21 18:49:04 +01:00
This commit is contained in:
parent
b7b1055fbc
commit
4fa5d74fa2
26 changed files with 1550 additions and 1548 deletions
885
tui/src/tui_impls.rs
Normal file
885
tui/src/tui_impls.rs
Normal file
|
|
@ -0,0 +1,885 @@
|
|||
use crate::*;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::{prelude::Position, style::Color::*};
|
||||
use crate::ratatui::prelude::Position;
|
||||
use unicode_width::{UnicodeWidthStr, UnicodeWidthChar};
|
||||
use ratatui::prelude::Position;
|
||||
use rand::{thread_rng, distributions::uniform::UniformSampler};
|
||||
|
||||
impl Tui {
|
||||
pub const fn fg <T> (color: Color, w: T) -> Foreground<Color, T> { Foreground(color, w) }
|
||||
pub const fn bg <T> (color: Color, w: T) -> Background<Color, T> { Background(color, w) }
|
||||
pub const fn fg_bg <T> (fg: Color, bg: Color, w: T) -> Background<Color, Foreground<Color, T>> { Background(bg, Foreground(fg, w)) }
|
||||
pub const fn modify <T> (enable: bool, modifier: Modifier, w: T) -> Modify<T> { Modify(enable, modifier, w) }
|
||||
pub const fn bold <T> (enable: bool, w: T) -> Modify<T> { Self::modify(enable, Modifier::BOLD, w) }
|
||||
pub const fn border <S, T> (enable: bool, style: S, w: T) -> Bordered<S, T> { Bordered(enable, style, w) }
|
||||
pub const fn null () -> Color { Color::Reset }
|
||||
pub const fn g (g: u8) -> Color { Color::Rgb(g, g, g) }
|
||||
pub const fn red () -> Color { Color::Rgb(255,0, 0) }
|
||||
pub const fn orange () -> Color { Color::Rgb(255,128,0) }
|
||||
pub const fn yellow () -> Color { Color::Rgb(255,255,0) }
|
||||
pub const fn brown () -> Color { Color::Rgb(128,255,0) }
|
||||
pub const fn green () -> Color { Color::Rgb(0,255,0) }
|
||||
pub const fn electric () -> Color { Color::Rgb(0,255,128) }
|
||||
//fn bg0 () -> Color { Color::Rgb(20, 20, 20) }
|
||||
//fn bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
//fn border_bg () -> Color { Color::Rgb(40, 50, 30) }
|
||||
//fn border_fg (f: bool) -> Color { if f { Self::bo1() } else { Self::bo2() } }
|
||||
//fn title_fg (f: bool) -> Color { if f { Self::ti1() } else { Self::ti2() } }
|
||||
//fn separator_fg (_: bool) -> Color { Color::Rgb(0, 0, 0) }
|
||||
//fn mode_bg () -> Color { Color::Rgb(150, 160, 90) }
|
||||
//fn mode_fg () -> Color { Color::Rgb(255, 255, 255) }
|
||||
//fn status_bar_bg () -> Color { Color::Rgb(28, 35, 25) }
|
||||
//fn bo1 () -> Color { Color::Rgb(100, 110, 40) }
|
||||
//fn bo2 () -> Color { Color::Rgb(70, 80, 50) }
|
||||
//fn ti1 () -> Color { Color::Rgb(150, 160, 90) }
|
||||
//fn ti2 () -> Color { Color::Rgb(120, 130, 100) }
|
||||
/// Construct a new TUI engine and wrap it for shared ownership.
|
||||
pub fn new () -> Usually<Arc<RwLock<Self>>> {
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let Size { width, height } = backend.size()?;
|
||||
Ok(Arc::new(RwLock::new(Self {
|
||||
exited: Arc::new(AtomicBool::new(false)),
|
||||
buffer: Buffer::empty(Rect { x: 0, y: 0, width, height }),
|
||||
area: [0, 0, width, height],
|
||||
backend,
|
||||
perf: Default::default(),
|
||||
})))
|
||||
}
|
||||
/// True if done
|
||||
pub fn exited (&self) -> bool {
|
||||
self.exited.fetch_and(true, Relaxed)
|
||||
}
|
||||
/// Prepare before run
|
||||
pub fn setup (&mut self) -> Usually<()> {
|
||||
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
||||
std::panic::set_hook(Box::new(move |info: &std::panic::PanicHookInfo|{
|
||||
stdout().execute(LeaveAlternateScreen).unwrap();
|
||||
CrosstermBackend::new(stdout()).show_cursor().unwrap();
|
||||
disable_raw_mode().unwrap();
|
||||
better_panic_handler(info);
|
||||
}));
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
self.backend.hide_cursor()?;
|
||||
enable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
/// Update the display buffer.
|
||||
pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer {
|
||||
if self.buffer.area != size {
|
||||
self.backend.clear_region(ClearType::All).unwrap();
|
||||
self.buffer.resize(size);
|
||||
self.buffer.reset();
|
||||
}
|
||||
let updates = self.buffer.diff(&buffer);
|
||||
self.backend.draw(updates.into_iter()).expect("failed to render");
|
||||
self.backend.flush().expect("failed to flush output buffer");
|
||||
std::mem::swap(&mut self.buffer, &mut buffer);
|
||||
buffer.reset();
|
||||
buffer
|
||||
}
|
||||
/// Clean up after run
|
||||
pub fn teardown (&mut self) -> Usually<()> {
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
self.backend.show_cursor()?;
|
||||
disable_raw_mode().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TuiWidget + Send + Sync + 'static> TuiRun<T> for Arc<RwLock<Tui>> {
|
||||
fn run (&self, state: &Arc<RwLock<T>>) -> Usually<()> {
|
||||
let _input_thread = TuiIn::run_input(self, state, Duration::from_millis(100));
|
||||
self.write().unwrap().setup()?;
|
||||
let render_thread = TuiOut::run_output(self, state, Duration::from_millis(10))?;
|
||||
match render_thread.join() {
|
||||
Ok(result) => {
|
||||
self.write().unwrap().teardown()?;
|
||||
println!("\n\rRan successfully: {result:?}\n\r");
|
||||
},
|
||||
Err(error) => {
|
||||
self.write().unwrap().teardown()?;
|
||||
panic!("\n\rDraw thread failed: error={error:?}.\n\r")
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for TuiEvent {
|
||||
fn cmp (&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.partial_cmp(other)
|
||||
.unwrap_or_else(||format!("{:?}", self).cmp(&format!("{other:?}"))) // FIXME perf
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiEvent {
|
||||
pub fn from_crossterm (event: Event) -> Self {
|
||||
Self(event)
|
||||
}
|
||||
#[cfg(feature = "dsl")]
|
||||
pub fn from_dsl (dsl: impl Language) -> Perhaps<Self> {
|
||||
Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self))
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiKey {
|
||||
const SPLIT: char = '/';
|
||||
#[cfg(feature = "dsl")]
|
||||
pub fn from_dsl (dsl: impl Language) -> Usually<Self> {
|
||||
if let Some(word) = dsl.word()? {
|
||||
let word = word.trim();
|
||||
Ok(if word == ":char" {
|
||||
Self(None, KeyModifiers::NONE)
|
||||
} else if word.chars().nth(0) == Some('@') {
|
||||
let mut key = None;
|
||||
let mut modifiers = KeyModifiers::NONE;
|
||||
let mut tokens = word[1..].split(Self::SPLIT).peekable();
|
||||
while let Some(token) = tokens.next() {
|
||||
if tokens.peek().is_some() {
|
||||
match token {
|
||||
"ctrl" | "Ctrl" | "c" | "C" => modifiers |= KeyModifiers::CONTROL,
|
||||
"alt" | "Alt" | "m" | "M" => modifiers |= KeyModifiers::ALT,
|
||||
"shift" | "Shift" | "s" | "S" => {
|
||||
modifiers |= KeyModifiers::SHIFT;
|
||||
// + TODO normalize character case, BackTab, etc.
|
||||
},
|
||||
_ => panic!("unknown modifier {token}"),
|
||||
}
|
||||
} else {
|
||||
key = if token.len() == 1 {
|
||||
Some(KeyCode::Char(token.chars().next().unwrap()))
|
||||
} else {
|
||||
Some(named_key(token).unwrap_or_else(||panic!("unknown character {token}")))
|
||||
}
|
||||
}
|
||||
}
|
||||
Self(key, modifiers)
|
||||
} else {
|
||||
return Err(format!("TuiKey: unexpected: {word}").into())
|
||||
})
|
||||
} else {
|
||||
return Err(format!("TuiKey: unspecified").into())
|
||||
}
|
||||
}
|
||||
pub fn to_crossterm (&self) -> Option<Event> {
|
||||
self.0.map(|code|Event::Key(KeyEvent {
|
||||
code,
|
||||
modifiers: self.1,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_update (buf: &mut Buffer, area: XY<u16>, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_debug!(BigBuffer |self, f| {
|
||||
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!(BigBuffer: |size:(usize, usize)| Self::new(size.0, size.1));
|
||||
impl Input for TuiIn {
|
||||
type Event = TuiEvent;
|
||||
type Handled = bool;
|
||||
fn event (&self) -> &TuiEvent { &self.event }
|
||||
fn is_done (&self) -> bool { self.exited.fetch_and(true, Relaxed) }
|
||||
fn done (&self) { self.exited.store(true, Relaxed); }
|
||||
}
|
||||
impl TuiIn {
|
||||
/// Spawn the input thread.
|
||||
pub fn run_input <T: Handle<TuiIn> + Send + Sync + '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 {
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
kind: KeyEventKind::Press,
|
||||
state: KeyEventState::NONE
|
||||
}) => {
|
||||
exited.store(true, Relaxed);
|
||||
},
|
||||
_ => {
|
||||
let exited = exited.clone();
|
||||
let event = TuiEvent::from_crossterm(event);
|
||||
if let Err(e) = state.write().unwrap().handle(&TuiIn { exited, event }) {
|
||||
panic!("{e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
from!(ItemColor: |okhsl: Okhsl<f32>|Self { okhsl, rgb: okhsl_to_rgb(okhsl) });
|
||||
from!(ItemColor: |rgb: Color|Self { rgb, okhsl: rgb_to_okhsl(rgb) });
|
||||
pub fn okhsl_to_rgb (color: Okhsl<f32>) -> Color {
|
||||
let Srgb { red, green, blue, .. }: Srgb<f32> = Srgb::from_color_unclamped(color);
|
||||
Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,)
|
||||
}
|
||||
pub fn rgb_to_okhsl (color: Color) -> Okhsl<f32> {
|
||||
if let Color::Rgb(r, g, b) = color {
|
||||
Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0))
|
||||
} else {
|
||||
unreachable!("only Color::Rgb is supported")
|
||||
}
|
||||
}
|
||||
|
||||
// A single color within item theme parameters, in OKHSL and RGB representations.
|
||||
impl ItemColor {
|
||||
pub const fn from_rgb (rgb: Color) -> Self {
|
||||
Self { rgb, okhsl: Okhsl::new_const(OklabHue::new(0.0), 0.0, 0.0) }
|
||||
}
|
||||
pub const fn from_okhsl (okhsl: Okhsl<f32>) -> Self {
|
||||
Self { rgb: Color::Rgb(0, 0, 0), okhsl }
|
||||
}
|
||||
pub fn random () -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let lo = Okhsl::new(-180.0, 0.01, 0.25);
|
||||
let hi = Okhsl::new( 180.0, 0.9, 0.5);
|
||||
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
||||
}
|
||||
pub fn random_dark () -> Self {
|
||||
let mut rng = thread_rng();
|
||||
let lo = Okhsl::new(-180.0, 0.025, 0.075);
|
||||
let hi = Okhsl::new( 180.0, 0.5, 0.150);
|
||||
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
|
||||
}
|
||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||
color.mix(Self::random(), distance)
|
||||
}
|
||||
pub fn mix (&self, other: Self, distance: f32) -> Self {
|
||||
if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); }
|
||||
self.okhsl.mix(other.okhsl, distance).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
from!(ItemTheme: |base: Color| Self::from_tui_color(base));
|
||||
from!(ItemTheme: |base: ItemColor|Self::from_item_color(base));
|
||||
impl ItemTheme {
|
||||
pub const G: [Self;256] = {
|
||||
let mut builder = konst::array::ArrayBuilder::new();
|
||||
while !builder.is_full() {
|
||||
let index = builder.len() as u8;
|
||||
let light = (index as f64 * 1.15) as u8;
|
||||
let lighter = (index as f64 * 1.7) as u8;
|
||||
let lightest = (index as f64 * 1.85) as u8;
|
||||
let dark = (index as f64 * 0.9) as u8;
|
||||
let darker = (index as f64 * 0.6) as u8;
|
||||
let darkest = (index as f64 * 0.3) as u8;
|
||||
builder.push(ItemTheme {
|
||||
base: ItemColor::from_rgb(Color::Rgb(index, index, index )),
|
||||
light: ItemColor::from_rgb(Color::Rgb(light, light, light, )),
|
||||
lighter: ItemColor::from_rgb(Color::Rgb(lighter, lighter, lighter, )),
|
||||
lightest: ItemColor::from_rgb(Color::Rgb(lightest, lightest, lightest, )),
|
||||
dark: ItemColor::from_rgb(Color::Rgb(dark, dark, dark, )),
|
||||
darker: ItemColor::from_rgb(Color::Rgb(darker, darker, darker, )),
|
||||
darkest: ItemColor::from_rgb(Color::Rgb(darkest, darkest, darkest, )),
|
||||
});
|
||||
}
|
||||
builder.build()
|
||||
};
|
||||
pub fn random () -> Self { ItemColor::random().into() }
|
||||
pub fn random_near (color: Self, distance: f32) -> Self {
|
||||
color.base.mix(ItemColor::random(), distance).into()
|
||||
}
|
||||
pub const G00: Self = {
|
||||
let color: ItemColor = ItemColor {
|
||||
okhsl: Okhsl { hue: OklabHue::new(0.0), lightness: 0.0, saturation: 0.0 },
|
||||
rgb: Color::Rgb(0, 0, 0)
|
||||
};
|
||||
Self {
|
||||
base: color,
|
||||
light: color,
|
||||
lighter: color,
|
||||
lightest: color,
|
||||
dark: color,
|
||||
darker: color,
|
||||
darkest: color,
|
||||
}
|
||||
};
|
||||
pub fn from_tui_color (base: Color) -> Self {
|
||||
Self::from_item_color(ItemColor::from_rgb(base))
|
||||
}
|
||||
pub fn from_item_color (base: ItemColor) -> Self {
|
||||
let mut light = base.okhsl;
|
||||
light.lightness = (light.lightness * 1.3).min(1.0);
|
||||
let mut lighter = light;
|
||||
lighter.lightness = (lighter.lightness * 1.3).min(1.0);
|
||||
let mut lightest = base.okhsl;
|
||||
lightest.lightness = 0.95;
|
||||
let mut dark = base.okhsl;
|
||||
dark.lightness = (dark.lightness * 0.75).max(0.0);
|
||||
dark.saturation = (dark.saturation * 0.75).max(0.0);
|
||||
let mut darker = dark;
|
||||
darker.lightness = (darker.lightness * 0.66).max(0.0);
|
||||
darker.saturation = (darker.saturation * 0.66).max(0.0);
|
||||
let mut darkest = darker;
|
||||
darkest.lightness = 0.1;
|
||||
darkest.saturation = (darkest.saturation * 0.50).max(0.0);
|
||||
Self {
|
||||
base,
|
||||
light: light.into(),
|
||||
lighter: lighter.into(),
|
||||
lightest: lightest.into(),
|
||||
dark: dark.into(),
|
||||
darker: darker.into(),
|
||||
darkest: darkest.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Draw<TuiOut> for u64 {
|
||||
fn draw (&self, _to: &mut TuiOut) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Draw<TuiOut> for f64 {
|
||||
fn draw (&self, _to: &mut TuiOut) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Phat<T> {
|
||||
pub const LO: &'static str = "▄";
|
||||
pub const HI: &'static str = "▀";
|
||||
/// A phat line
|
||||
pub fn lo (fg: Color, bg: Color) -> impl Content<TuiOut> { Fixed::Y(1, Tui::fg_bg(fg, bg, RepeatH(Self::LO))) }
|
||||
/// A phat line
|
||||
pub fn hi (fg: Color, bg: Color) -> impl Content<TuiOut> { Fixed::Y(1, Tui::fg_bg(fg, bg, RepeatH(Self::HI))) }
|
||||
}
|
||||
|
||||
impl<T: Content<TuiOut>> HasContent<TuiOut> for Phat<T> {
|
||||
fn content (&self) -> impl Content<TuiOut> {
|
||||
let [fg, bg, hi, lo] = self.colors;
|
||||
let top = Fixed::Y(1, Self::lo(bg, hi));
|
||||
let low = Fixed::Y(1, Self::hi(bg, lo));
|
||||
let content = Tui::fg_bg(fg, bg, &self.content);
|
||||
Min::XY(self.width, self.height, Bsp::s(top, Bsp::n(low, Fill::XY(content))))
|
||||
}
|
||||
}
|
||||
|
||||
impl Draw<TuiOut> for Repeat<'_> {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
let [x, y, w, h] = to.area().xywh();
|
||||
let a = self.0.len();
|
||||
for (_v, y) in (y..y+h).enumerate() {
|
||||
for (u, x) in (x..x+w).enumerate() {
|
||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||
let u = u % a;
|
||||
cell.set_symbol(&self.0[u..u+1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout<TuiOut> for RepeatV<'_> {}
|
||||
impl Draw<TuiOut> for RepeatV<'_> {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
let [x, y, _w, h] = to.area().xywh();
|
||||
for y in y..y+h {
|
||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout<TuiOut> for RepeatH<'_> {}
|
||||
impl Draw<TuiOut> for RepeatH<'_> {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
let [x, y, w, _h] = to.area().xywh();
|
||||
for x in x..x+w {
|
||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||
cell.set_symbol(&self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollbarV {
|
||||
const ICON_DEC: &[char] = &['▲'];
|
||||
const ICON_INC: &[char] = &['▼'];
|
||||
}
|
||||
|
||||
impl ScrollbarH {
|
||||
const ICON_DEC: &[char] = &[' ', '🞀', ' '];
|
||||
const ICON_INC: &[char] = &[' ', '🞂', ' '];
|
||||
}
|
||||
|
||||
impl Draw<TuiOut> for ScrollbarV {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
let [x, y1, _w, h] = to.area().xywh();
|
||||
let y2 = y1 + h;
|
||||
for (i, y) in (y1..=y2).enumerate() {
|
||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||
if (i as usize) < (Self::ICON_DEC.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(Self::ICON_DEC[i as usize]);
|
||||
} else if (i as usize) > (h as usize - Self::ICON_INC.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(Self::ICON_INC[h as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('‖'); // ━
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╎'); // ━
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Draw<TuiOut> for ScrollbarH {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
let [x1, y, w, _h] = to.area().xywh();
|
||||
let x2 = x1 + w;
|
||||
for (i, x) in (x1..=x2).enumerate() {
|
||||
if let Some(cell) = to.buffer.cell_mut(Position::from((x, y))) {
|
||||
if i < (Self::ICON_DEC.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(Self::ICON_DEC[i as usize]);
|
||||
} else if i > (w as usize - Self::ICON_INC.len()) {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Rgb(0, 0, 0));
|
||||
cell.set_char(Self::ICON_INC[w as usize - i]);
|
||||
} else if false {
|
||||
cell.set_fg(Rgb(255, 255, 255));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('━');
|
||||
} else {
|
||||
cell.set_fg(Rgb(0, 0, 0));
|
||||
cell.set_bg(Reset);
|
||||
cell.set_char('╌');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Draw<TuiOut> for &str {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
let [x, y, w, ..] = self.layout(to.area());
|
||||
to.text(&self, x, y, w)
|
||||
}
|
||||
}
|
||||
impl Draw<TuiOut> for String {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
self.as_str().draw(to)
|
||||
}
|
||||
}
|
||||
impl Draw<TuiOut> for Arc<str> {
|
||||
fn draw (&self, to: &mut TuiOut) { self.as_ref().draw(to) }
|
||||
}
|
||||
|
||||
impl Layout<TuiOut> for &str {
|
||||
fn layout (&self, to: XY<u16>) -> XY<u16> { to.center_xy([width_chars_max(to.w(), self), 1]) }
|
||||
}
|
||||
impl Layout<TuiOut> for String {
|
||||
fn layout (&self, to: XY<u16>) -> XY<u16> {
|
||||
self.as_str().layout(to)
|
||||
}
|
||||
}
|
||||
impl Layout<TuiOut> for Arc<str> {
|
||||
fn layout (&self, to: XY<u16>) -> XY<u16> {
|
||||
self.as_ref().layout(to)
|
||||
}
|
||||
}
|
||||
|
||||
fn width_chars_max (max: u16, text: impl AsRef<str>) -> u16 {
|
||||
let mut width: u16 = 0;
|
||||
let mut chars = text.as_ref().chars();
|
||||
while let Some(c) = chars.next() {
|
||||
width += c.width().unwrap_or(0) as u16;
|
||||
if width > max {
|
||||
break
|
||||
}
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
/// Trim string with [unicode_width].
|
||||
pub fn trim_string (max_width: usize, input: impl AsRef<str>) -> String {
|
||||
let input = input.as_ref();
|
||||
let mut output = Vec::with_capacity(input.len());
|
||||
let mut width: usize = 1;
|
||||
let mut chars = input.chars();
|
||||
while let Some(c) = chars.next() {
|
||||
if width > max_width {
|
||||
break
|
||||
}
|
||||
output.push(c);
|
||||
width += c.width().unwrap_or(0);
|
||||
}
|
||||
return output.into_iter().collect()
|
||||
}
|
||||
|
||||
|
||||
impl<'a, T: AsRef<str>> TrimString<T> {
|
||||
fn as_ref (&self) -> TrimStringRef<'_, T> { TrimStringRef(self.0, &self.1) }
|
||||
}
|
||||
impl<'a, T: AsRef<str>> Draw<TuiOut> for TrimString<T> {
|
||||
fn draw (&self, to: &mut TuiOut) { Draw::draw(&self.as_ref(), to) }
|
||||
}
|
||||
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimString<T> {
|
||||
fn layout (&self, to: XY<u16>) -> XY<u16> { Layout::layout(&self.as_ref(), to) }
|
||||
}
|
||||
|
||||
impl<'a, T: AsRef<str>> Layout<TuiOut> for TrimStringRef<'a, T> {
|
||||
fn layout (&self, to: XY<u16>) -> XY<u16> {
|
||||
[to.x(), to.y(), to.w().min(self.0).min(self.1.as_ref().width() as u16), to.h()]
|
||||
}
|
||||
}
|
||||
impl<T: AsRef<str>> Draw<TuiOut> for TrimStringRef<'_, T> {
|
||||
fn draw (&self, target: &mut TuiOut) {
|
||||
let area = target.area();
|
||||
let mut width: u16 = 1;
|
||||
let mut chars = self.1.as_ref().chars();
|
||||
while let Some(c) = chars.next() {
|
||||
if width > self.0 || width > area.w() {
|
||||
break
|
||||
}
|
||||
if let Some(cell) = target.buffer.cell_mut(Position {
|
||||
x: area.x() + width - 1,
|
||||
y: area.y()
|
||||
}) {
|
||||
cell.set_char(c);
|
||||
}
|
||||
width += c.width().unwrap_or(0) as u16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
A: Content<TuiOut>,
|
||||
B: Content<TuiOut>,
|
||||
C: Content<TuiOut>,
|
||||
> HasContent<TuiOut> for Tryptich<A, B, C> {
|
||||
fn content (&self) -> impl Content<TuiOut> {
|
||||
let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self;
|
||||
Fixed::Y(h, if top {
|
||||
Bsp::a(
|
||||
Fill::X(Align::n(Fixed::X(w_b, Align::x(Tui::bg(Color::Reset, b))))),
|
||||
Bsp::a(
|
||||
Fill::X(Align::nw(Fixed::X(w_a, Tui::bg(Color::Reset, a)))),
|
||||
Fill::X(Align::ne(Fixed::X(w_c, Tui::bg(Color::Reset, c)))),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Bsp::a(
|
||||
Fill::XY(Align::c(Fixed::X(w_b, Align::x(Tui::bg(Color::Reset, b))))),
|
||||
Bsp::a(
|
||||
Fill::XY(Align::w(Fixed::X(w_a, Tui::bg(Color::Reset, a)))),
|
||||
Fill::XY(Align::e(Fixed::X(w_c, Tui::bg(Color::Reset, c)))),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<O: Out, T: Draw<O>> ErrorBoundary<O, T> {
|
||||
pub fn new (content: Perhaps<T>) -> Self {
|
||||
Self(Default::default(), content)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Draw<TuiOut>> Draw<TuiOut> for ErrorBoundary<TuiOut, T> {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
match self.1.as_ref() {
|
||||
Ok(Some(content)) => content.draw(to),
|
||||
Ok(None) => to.blit(&"empty?", 0, 0, Some(Style::default().yellow())),
|
||||
Err(e) => {
|
||||
let err_fg = Color::Rgb(255,224,244);
|
||||
let err_bg = Color::Rgb(96,24,24);
|
||||
let title = Bsp::e(Tui::bold(true, "oops. "), "rendering failed.");
|
||||
let error = Bsp::e("\"why?\" ", Tui::bold(true, format!("{e}")));
|
||||
to.place(&Tui::fg_bg(err_fg, err_bg, Bsp::s(title, error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: BorderStyle, W: Content<TuiOut>> HasContent<TuiOut> for Bordered<S, W> {
|
||||
fn content (&self) -> impl Content<TuiOut> {
|
||||
Fill::XY(lay!( When::new(self.0, Border(self.0, self.1)), Pad::XY(1, 1, &self.2) ))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: BorderStyle> Draw<TuiOut> for Border<S> {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
let Border(enabled, style) = self;
|
||||
if *enabled {
|
||||
let area = to.area();
|
||||
if area.w() > 0 && area.y() > 0 {
|
||||
to.blit(&style.border_nw(), area.x(), area.y(), style.style());
|
||||
to.blit(&style.border_ne(), area.x() + area.w() - 1, area.y(), style.style());
|
||||
to.blit(&style.border_sw(), area.x(), area.y() + area.h() - 1, style.style());
|
||||
to.blit(&style.border_se(), area.x() + area.w() - 1, area.y() + area.h() - 1, style.style());
|
||||
for x in area.x()+1..area.x()+area.w()-1 {
|
||||
to.blit(&style.border_n(), x, area.y(), style.style());
|
||||
to.blit(&style.border_s(), x, area.y() + area.h() - 1, style.style());
|
||||
}
|
||||
for y in area.y()+1..area.y()+area.h()-1 {
|
||||
to.blit(&style.border_w(), area.x(), y, style.style());
|
||||
to.blit(&style.border_e(), area.x() + area.w() - 1, y, style.style());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Content<TuiOut>> Draw<TuiOut> for Foreground<Color, T> {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
let area = self.layout(to.area());
|
||||
to.fill_fg(area, self.0);
|
||||
to.place_at(area, &self.1);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Content<TuiOut>> Draw<TuiOut> for Background<Color, T> {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
let area = self.layout(to.area());
|
||||
to.fill_bg(area, self.0);
|
||||
to.place_at(area, &self.1);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Content<TuiOut>> Layout<TuiOut> for Modify<T> {}
|
||||
|
||||
impl<T: Content<TuiOut>> Draw<TuiOut> for Modify<T> {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
to.fill_mod(to.area(), self.0, self.1);
|
||||
self.2.draw(to)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Content<TuiOut>> Layout<TuiOut> for Styled<T> {}
|
||||
|
||||
impl<T: Content<TuiOut>> Draw<TuiOut> for Styled<T> {
|
||||
fn draw (&self, to: &mut TuiOut) {
|
||||
to.place(&self.1);
|
||||
// TODO write style over area
|
||||
}
|
||||
}
|
||||
|
||||
impl TuiOut {
|
||||
/// Spawn the output thread.
|
||||
pub fn run_output <T: Draw<TuiOut> + Send + Sync + 'static> (
|
||||
engine: &Arc<RwLock<Tui>>,
|
||||
state: &Arc<RwLock<T>>,
|
||||
timer: Duration
|
||||
) -> Result<JoinHandle<()>, std::io::Error> {
|
||||
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 });
|
||||
std::thread::Builder::new()
|
||||
.name("tui output thread".into())
|
||||
.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.draw(&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);
|
||||
})
|
||||
}
|
||||
#[inline]
|
||||
pub fn with_rect (&mut self, area: XY<u16>) -> &mut Self {
|
||||
self.area = area;
|
||||
self
|
||||
}
|
||||
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;
|
||||
let style = style.unwrap_or(Style::default());
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
buf.set_string(x, y, text, style);
|
||||
}
|
||||
}
|
||||
/// Write a line of text
|
||||
///
|
||||
/// TODO: do a paragraph (handle newlines)
|
||||
pub fn text (&mut self, text: &impl AsRef<str>, x0: u16, y: u16, max_width: u16) {
|
||||
let text = text.as_ref();
|
||||
let buf = &mut self.buffer;
|
||||
let mut string_width: u16 = 0;
|
||||
for character in text.chars() {
|
||||
let x = x0 + string_width;
|
||||
let character_width = character.width().unwrap_or(0) as u16;
|
||||
string_width += character_width;
|
||||
if string_width > max_width {
|
||||
break
|
||||
}
|
||||
if let Some(cell) = buf.cell_mut(ratatui::prelude::Position { x, y }) {
|
||||
cell.set_char(character);
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn buffer_update (&mut self, area: XY<u16>, callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||
buffer_update(&mut self.buffer, area, callback);
|
||||
}
|
||||
pub fn fill_char (&mut self, area: XY<u16>, c: char) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_char(c);})
|
||||
}
|
||||
pub fn fill_bg (&mut self, area: XY<u16>, color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
|
||||
}
|
||||
pub fn fill_fg (&mut self, area: XY<u16>, color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
|
||||
}
|
||||
pub fn fill_mod (&mut self, area: XY<u16>, on: bool, modifier: Modifier) {
|
||||
if on {
|
||||
self.buffer_update(area, &|cell,_,_|cell.modifier.insert(modifier))
|
||||
} else {
|
||||
self.buffer_update(area, &|cell,_,_|cell.modifier.remove(modifier))
|
||||
}
|
||||
}
|
||||
pub fn fill_bold (&mut self, area: XY<u16>, on: bool) {
|
||||
self.fill_mod(area, on, Modifier::BOLD)
|
||||
}
|
||||
pub fn fill_reversed (&mut self, area: XY<u16>, on: bool) {
|
||||
self.fill_mod(area, on, Modifier::REVERSED)
|
||||
}
|
||||
pub fn fill_crossed_out (&mut self, area: XY<u16>, on: bool) {
|
||||
self.fill_mod(area, on, Modifier::CROSSED_OUT)
|
||||
}
|
||||
pub fn fill_ul (&mut self, area: XY<u16>, color: Option<Color>) {
|
||||
if let Some(color) = color {
|
||||
self.buffer_update(area, &|cell,_,_|{
|
||||
cell.modifier.insert(ratatui::prelude::Modifier::UNDERLINED);
|
||||
cell.underline_color = color;
|
||||
})
|
||||
} else {
|
||||
self.buffer_update(area, &|cell,_,_|{
|
||||
cell.modifier.remove(ratatui::prelude::Modifier::UNDERLINED);
|
||||
})
|
||||
}
|
||||
}
|
||||
pub fn tint_all (&mut self, fg: Color, bg: Color, modifier: Modifier) {
|
||||
for cell in self.buffer.content.iter_mut() {
|
||||
cell.fg = fg;
|
||||
cell.bg = bg;
|
||||
cell.modifier = modifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue