mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
wip: slowly putting it back together
This commit is contained in:
parent
7fbb40fad6
commit
461c60d6b3
18 changed files with 788 additions and 774 deletions
7
crates/tek_core/src/component.rs
Normal file
7
crates/tek_core/src/component.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
use crate::*;
|
||||
|
||||
/// A UI component.
|
||||
pub trait Component<E: Engine>: Render<E, E::Rendered> + Handle<E, E::Handled> {}
|
||||
|
||||
/// Everything that implements [Render] and [Handle] is a [Component].
|
||||
impl<E: Engine, C: Render<E, E::Rendered> + Handle<E, E::Handled>> Component<E> for C {}
|
||||
14
crates/tek_core/src/edn.rs
Normal file
14
crates/tek_core/src/edn.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError};
|
||||
|
||||
/// EDN parsing helper.
|
||||
#[macro_export] macro_rules! edn {
|
||||
($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
match $edn { $($pat => $expr),* }
|
||||
};
|
||||
($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
for $edn in $args {
|
||||
edn!($edn { $($pat => $expr),* })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
23
crates/tek_core/src/engine.rs
Normal file
23
crates/tek_core/src/engine.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use crate::*;
|
||||
|
||||
/// Entry point for main loop
|
||||
pub trait App<T: Engine> {
|
||||
fn run (self, context: T) -> Usually<T>;
|
||||
}
|
||||
|
||||
/// Platform backend.
|
||||
pub trait Engine {
|
||||
type Handled;
|
||||
type Rendered;
|
||||
fn setup (&mut self) -> Usually<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn teardown (&mut self) -> Usually<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn handle (&self, _: &mut impl Handle<Self, Self::Handled>)
|
||||
-> Usually<()> where Self: Sized;
|
||||
fn render (&mut self, _: &impl Render<Self, Self::Rendered>)
|
||||
-> Usually<()> where Self: Sized;
|
||||
fn exited (&self) -> bool;
|
||||
}
|
||||
16
crates/tek_core/src/event.rs
Normal file
16
crates/tek_core/src/event.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#[derive(Debug)]
|
||||
pub enum AppEvent {
|
||||
/// Terminal input
|
||||
Input(::crossterm::event::Event),
|
||||
/// Update values but not the whole form.
|
||||
Update,
|
||||
/// Update the whole form.
|
||||
Redraw,
|
||||
/// Device gains focus
|
||||
Focus,
|
||||
/// Device loses focus
|
||||
Blur,
|
||||
// /// JACK notification
|
||||
// Jack(JackEvent)
|
||||
}
|
||||
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait Exit: Send {
|
||||
fn exited (&self) -> bool;
|
||||
fn exit (&mut self);
|
||||
|
|
@ -18,3 +20,11 @@ pub trait Exit: Send {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker trait for [Component]s that can [Exit]
|
||||
pub trait ExitableComponent<E>: Exit + Component<E> where E: Engine {
|
||||
/// Perform type erasure for collecting heterogeneous components.
|
||||
fn boxed (self) -> Box<dyn ExitableComponent<E>> where Self: Sized + 'static {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use crate::*;
|
||||
|
||||
/// A component that may contain [Focusable] components.
|
||||
pub trait Focus <'a, const N: usize, T, U>: Render<'a, T, U> + Handle {
|
||||
pub trait Focus <const N: usize, T, U>: Render<T, U> + Handle<T, U> {
|
||||
fn focus (&self) -> usize;
|
||||
fn focus_mut (&mut self) -> &mut usize;
|
||||
fn focusable (&self) -> [&dyn Focusable<T, U>;N];
|
||||
|
|
@ -33,13 +33,13 @@ pub trait Focus <'a, const N: usize, T, U>: Render<'a, T, U> + Handle {
|
|||
}
|
||||
|
||||
/// A component that may be focused.
|
||||
pub trait Focusable<'a, T, U>: Render<'a, T, U> + Handle {
|
||||
pub trait Focusable<T, U>: Render<T, U> + Handle<T, U> {
|
||||
fn is_focused (&self) -> bool;
|
||||
fn set_focused (&mut self, focused: bool);
|
||||
}
|
||||
|
||||
impl<'a, F: Focusable<'a, T, U>, T, U> Focusable<'a, T, U> for Option<F>
|
||||
where Option<F>: Render<'a, T, U>
|
||||
impl<F: Focusable<T, U>, T, U> Focusable<T, U> for Option<F>
|
||||
where Option<F>: Render<T, U>
|
||||
{
|
||||
fn is_focused (&self) -> bool {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -1,114 +1,46 @@
|
|||
use crate::*;
|
||||
|
||||
/// Spawn thread that listens for user input
|
||||
pub fn input_thread (
|
||||
exited: &Arc<AtomicBool>,
|
||||
device: &Arc<RwLock<impl Handle + Send + Sync + 'static>>
|
||||
) -> JoinHandle<()> {
|
||||
let poll = Duration::from_millis(100);
|
||||
let exited = exited.clone();
|
||||
let device = device.clone();
|
||||
spawn(move || loop {
|
||||
// Exit if flag is set
|
||||
if exited.fetch_and(true, Ordering::Relaxed) {
|
||||
break
|
||||
}
|
||||
// Listen for events and send them to the main thread
|
||||
if ::crossterm::event::poll(poll).is_ok() {
|
||||
let event = ::crossterm::event::read().unwrap();
|
||||
if let Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, ..
|
||||
}) = event {
|
||||
exited.store(true, Ordering::Relaxed);
|
||||
} else if let Err(e) = device.write().unwrap().handle(&AppEvent::Input(event)) {
|
||||
panic!("{e}")
|
||||
}
|
||||
}
|
||||
})
|
||||
/// Handle input
|
||||
pub trait Handle<T, U>: Send + Sync {
|
||||
fn handle (&mut self, context: &mut T) -> Perhaps<U>;
|
||||
}
|
||||
|
||||
/// Trait for things that handle input events.
|
||||
pub trait Handle: Send + Sync {
|
||||
/// Handle an input event.
|
||||
/// Returns Ok(true) if the device handled the event.
|
||||
/// This is the mechanism which allows nesting of components;.
|
||||
fn handle (&mut self, _e: &AppEvent) -> Usually<bool> {
|
||||
Ok(false)
|
||||
impl<H, T, U> Handle<T, U> for &mut H where H: Handle<T, U> {
|
||||
fn handle (&mut self, context: &mut T) -> Perhaps<U> {
|
||||
(*self).handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Handle> Handle for &mut T {
|
||||
fn handle (&mut self, e: &AppEvent) -> Usually<bool> {
|
||||
(*self).handle(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Handle> Handle for Option<T> {
|
||||
fn handle (&mut self, e: &AppEvent) -> Usually<bool> {
|
||||
match self {
|
||||
Some(handle) => handle.handle(e),
|
||||
None => Ok(false)
|
||||
impl<H, T, U> Handle<T, U> for Option<H> where H: Handle<T, U> {
|
||||
fn handle (&mut self, context: &mut T) -> Perhaps<U> {
|
||||
if let Some(ref mut handle) = self {
|
||||
handle.handle(context)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Handle> Handle for Mutex<T> {
|
||||
fn handle (&mut self, e: &AppEvent) -> Usually<bool> {
|
||||
self.lock().unwrap().handle(e)
|
||||
impl<H, T, U> Handle<T, U> for Mutex<H> where H: Handle<T, U> {
|
||||
fn handle (&mut self, context: &mut T) -> Perhaps<U> {
|
||||
self.lock().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Handle> Handle for Arc<Mutex<T>> {
|
||||
fn handle (&mut self, e: &AppEvent) -> Usually<bool> {
|
||||
self.lock().unwrap().handle(e)
|
||||
impl<H, T, U> Handle<T, U> for Arc<Mutex<H>> where H: Handle<T, U> {
|
||||
fn handle (&mut self, context: &mut T) -> Perhaps<U> {
|
||||
self.lock().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Handle> Handle for RwLock<T> {
|
||||
fn handle (&mut self, e: &AppEvent) -> Usually<bool> {
|
||||
self.write().unwrap().handle(e)
|
||||
impl<H, T, U> Handle<T, U> for RwLock<H> where H: Handle<T, U> {
|
||||
fn handle (&mut self, context: &mut T) -> Perhaps<U> {
|
||||
self.write().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Handle> Handle for Arc<RwLock<T>> {
|
||||
fn handle (&mut self, e: &AppEvent) -> Usually<bool> {
|
||||
self.write().unwrap().handle(e)
|
||||
impl<H, T, U> Handle<T, U> for Arc<RwLock<H>> where H: Handle<T, U> {
|
||||
fn handle (&mut self, context: &mut T) -> Perhaps<U> {
|
||||
self.write().unwrap().handle(context)
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the `Handle` trait.
|
||||
#[macro_export] macro_rules! handle {
|
||||
($T:ty) => {
|
||||
impl Handle for $T {}
|
||||
};
|
||||
($T:ty |$self:ident, $e:ident|$block:expr) => {
|
||||
impl Handle for $T {
|
||||
fn handle (&mut $self, $e: &AppEvent) -> Usually<bool> {
|
||||
$block
|
||||
}
|
||||
}
|
||||
};
|
||||
($T:ty = $handle:path) => {
|
||||
impl Handle for $T {
|
||||
fn handle (&mut self, e: &AppEvent) -> Usually<bool> {
|
||||
$handle(self, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppEvent {
|
||||
/// Terminal input
|
||||
Input(::crossterm::event::Event),
|
||||
/// Update values but not the whole form.
|
||||
Update,
|
||||
/// Update the whole form.
|
||||
Redraw,
|
||||
/// Device gains focus
|
||||
Focus,
|
||||
/// Device loses focus
|
||||
Blur,
|
||||
// /// JACK notification
|
||||
// Jack(JackEvent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,33 +5,16 @@ pub use midly;
|
|||
pub use clap;
|
||||
pub use std::sync::{Arc, Mutex, LockResult, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
pub use std::collections::BTreeMap;
|
||||
pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
|
||||
pub use ratatui::prelude::{Rect, Style, Color, Buffer};
|
||||
pub use ratatui::style::Stylize;
|
||||
pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError};
|
||||
pub use once_cell::sync::Lazy;
|
||||
pub use std::sync::atomic::{Ordering, AtomicBool};
|
||||
pub use std::rc::Rc;
|
||||
pub use std::cell::RefCell;
|
||||
|
||||
pub(crate) use std::error::Error;
|
||||
pub(crate) use std::io::{stdout};
|
||||
pub(crate) use std::thread::{spawn, JoinHandle};
|
||||
pub(crate) use std::time::Duration;
|
||||
pub(crate) use atomic_float::*;
|
||||
//pub(crate) use std::path::PathBuf;
|
||||
//pub(crate) use std::fs::read_dir;
|
||||
//pub(crate) use std::ffi::OsString;
|
||||
|
||||
// Non-stdlib dependencies:
|
||||
//pub(crate) use microxdg::XdgApp;
|
||||
//pub(crate) use midly::{MidiMessage, live::LiveEvent, num::u7};
|
||||
pub(crate) use crossterm::{ExecutableCommand};
|
||||
use better_panic::{Settings, Verbosity};
|
||||
use crossterm::terminal::{
|
||||
EnterAlternateScreen, LeaveAlternateScreen,
|
||||
enable_raw_mode, disable_raw_mode
|
||||
};
|
||||
|
||||
/// Define and reexport submodules.
|
||||
#[macro_export] macro_rules! submod {
|
||||
|
|
@ -44,38 +27,26 @@ use crossterm::terminal::{
|
|||
}
|
||||
|
||||
submod! {
|
||||
component
|
||||
edn
|
||||
engine
|
||||
event
|
||||
exit
|
||||
focus
|
||||
handle
|
||||
handle_keymap
|
||||
jack_core
|
||||
jack_device
|
||||
jack_event
|
||||
jack_ports
|
||||
keymap
|
||||
render
|
||||
render_axis
|
||||
render_buffer
|
||||
render_collect
|
||||
render_layered
|
||||
render_split
|
||||
render_tui
|
||||
render_tui_theme
|
||||
render_tui_border
|
||||
time_base
|
||||
time_note
|
||||
time_tick
|
||||
}
|
||||
|
||||
/// EDN parsing helper.
|
||||
#[macro_export] macro_rules! edn {
|
||||
($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
match $edn { $($pat => $expr),* }
|
||||
};
|
||||
($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
|
||||
for $edn in $args {
|
||||
edn!($edn { $($pat => $expr),* })
|
||||
}
|
||||
};
|
||||
time
|
||||
tui
|
||||
}
|
||||
|
||||
/// Standard result type.
|
||||
|
|
@ -83,37 +54,3 @@ pub type Usually<T> = Result<T, Box<dyn Error>>;
|
|||
|
||||
/// Standard optional result type.
|
||||
pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
|
||||
|
||||
/// A UI component.
|
||||
pub trait Component<'a, T, U>: Render<'a, T, U> + Handle + Sync {
|
||||
/// Perform type erasure for collecting heterogeneous components.
|
||||
fn boxed (self) -> Box<dyn Component<'a, T, U>> where Self: Sized + 'static {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, C, T, U> Component<'a, T, U> for C where C: Render<'a, T, U> + Handle + Sync {}
|
||||
|
||||
/// Marker trait for [Component]s that can [Exit]
|
||||
pub trait ExitableComponent<'a, T, U>: Exit + Component<'a, T, U> {
|
||||
/// Perform type erasure for collecting heterogeneous components.
|
||||
fn boxed (self) -> Box<dyn ExitableComponent<'a, T, U>> where Self: Sized + 'static {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, E: Exit + Component<'a, T, U>, T, U> ExitableComponent<'a, T, U> for E {}
|
||||
|
||||
/// Run the main loop.
|
||||
pub fn run <R> (state: Arc<RwLock<R>>) -> Usually<Arc<RwLock<R>>>
|
||||
where R: for <'a> Render<'a, TuiOutput<'a>, Rect> + Handle + Sized + 'static
|
||||
{
|
||||
let exited = Arc::new(AtomicBool::new(false));
|
||||
let _input_thread = input_thread(&exited, &state);
|
||||
terminal_setup()?;
|
||||
panic_hook_setup();
|
||||
let main_thread = tui_render_thread(&exited, &state)?;
|
||||
main_thread.join().expect("main thread failed");
|
||||
terminal_teardown()?;
|
||||
Ok(state)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
//! Rendering of application to display.
|
||||
|
||||
use crate::*;
|
||||
pub(crate) use ratatui::prelude::CrosstermBackend;
|
||||
|
||||
/// Trait for things that are displayed to the user.
|
||||
pub trait Render<'a, T, U>: Send + Sync {
|
||||
fn render (&self, to: &'a mut T) -> Perhaps<U>;
|
||||
/// Render to output.
|
||||
pub trait Render<T, U>: Send + Sync {
|
||||
fn render (&self, context: &mut T) -> Perhaps<U>;
|
||||
}
|
||||
|
||||
/// Options can be rendered optionally.
|
||||
impl<'a, R, T, U> Render<'a, T, U> for Option<R> where R: Render<'a, T, U> {
|
||||
fn render (&self, to: &'a mut T) -> Perhaps<U> {
|
||||
impl<R, T, U> Render<T, U> for Option<R> where R: Render<T, U> {
|
||||
fn render (&self, to: &mut T) -> Perhaps<U> {
|
||||
match self {
|
||||
Some(component) => component.render(to),
|
||||
None => Ok(None)
|
||||
|
|
@ -19,43 +16,43 @@ impl<'a, R, T, U> Render<'a, T, U> for Option<R> where R: Render<'a, T, U> {
|
|||
}
|
||||
|
||||
/// Boxed references can be rendered.
|
||||
impl<'a, T, U> Render<'a, T, U> for Box<dyn Render<'a, T, U> + 'a> {
|
||||
fn render (&self, to: &'a mut T) -> Perhaps<U> {
|
||||
impl<'a, T, U> Render<T, U> for Box<dyn Render<T, U> + 'a> {
|
||||
fn render (&self, to: &mut T) -> Perhaps<U> {
|
||||
(**self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable references can be rendered.
|
||||
impl<'a, R, T, U> Render<'a, T, U> for &R where R: Render<'a, T, U> {
|
||||
fn render (&self, to: &'a mut T) -> Perhaps<U> {
|
||||
impl<R, T, U> Render<T, U> for &R where R: Render<T, U> {
|
||||
fn render (&self, to: &mut T) -> Perhaps<U> {
|
||||
(*self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutable references can be rendered.
|
||||
impl<'a, R, T, U> Render<'a, T, U> for &mut R where R: Render<'a, T, U> {
|
||||
fn render (&self, to: &'a mut T) -> Perhaps<U> {
|
||||
impl<R, T, U> Render<T, U> for &mut R where R: Render<T, U> {
|
||||
fn render (&self, to: &mut T) -> Perhaps<U> {
|
||||
(**self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
/// Counted references can be rendered.
|
||||
impl<'a, R, T, U> Render<'a, T, U> for Arc<R> where R: Render<'a, T, U> {
|
||||
fn render (&self, to: &'a mut T) -> Perhaps<U> {
|
||||
impl<R, T, U> Render<T, U> for Arc<R> where R: Render<T, U> {
|
||||
fn render (&self, to: &mut T) -> Perhaps<U> {
|
||||
self.as_ref().render(to)
|
||||
}
|
||||
}
|
||||
|
||||
/// References behind a [Mutex] can be rendered.
|
||||
impl<'a, R, T, U> Render<'a, T, U> for Mutex<R> where R: Render<'a, T, U> {
|
||||
fn render (&self, to: &'a mut T) -> Perhaps<U> {
|
||||
impl<R, T, U> Render<T, U> for Mutex<R> where R: Render<T, U> {
|
||||
fn render (&self, to: &mut T) -> Perhaps<U> {
|
||||
self.lock().unwrap().render(to)
|
||||
}
|
||||
}
|
||||
|
||||
/// References behind a [RwLock] can be rendered.
|
||||
impl<'a, R, T, U> Render<'a, T, U> for RwLock<R> where R: Render<'a, T, U> {
|
||||
fn render (&self, to: &'a mut T) -> Perhaps<U> {
|
||||
impl<R, T, U> Render<T, U> for RwLock<R> where R: Render<T, U> {
|
||||
fn render (&self, to: &mut T) -> Perhaps<U> {
|
||||
self.read().unwrap().render(to)
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +62,7 @@ impl<'a, R, T, U> Render<'a, T, U> for RwLock<R> where R: Render<'a, T, U> {
|
|||
/// Rendering unboxed closures should also be possible;
|
||||
/// but in practice implementing the trait for an unboxed
|
||||
/// `Fn` closure causes an impl conflict.
|
||||
impl<'a, T, U> Render<'a, T, U> for Box<dyn Fn(&mut T) -> Perhaps<U> + Send + Sync + 'a> {
|
||||
impl<'a, T, U> Render<T, U> for Box<dyn Fn(&mut T) -> Perhaps<U> + Send + Sync + 'a> {
|
||||
fn render (&self, to: &mut T) -> Perhaps<U> {
|
||||
(*self)(to)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,19 +9,19 @@ impl<'a, T, U> Layered<'a, T, U> {
|
|||
}
|
||||
|
||||
impl<'a, T, U> Collect<'a, T, U> for Layered<'a, T, U> {
|
||||
fn add_box (mut self, item: Box<dyn Render<'a, T, U> + 'a>) -> Self {
|
||||
fn add_box (mut self, item: Box<dyn Render<T, U> + 'a>) -> Self {
|
||||
self.0 = self.0.add_box(item);
|
||||
self
|
||||
}
|
||||
fn add_ref (mut self, item: &'a dyn Render<'a, T, U>) -> Self {
|
||||
fn add_ref (mut self, item: &'a dyn Render<T, U>) -> Self {
|
||||
self.0 = self.0.add_ref(item);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Render<'a, TuiOutput<'b>, Rect> for Layered<'a, TuiOutput<'b>, Rect> {
|
||||
fn render (&self, to: &'a mut TuiOutput<'b>) -> Perhaps<Rect> {
|
||||
let area = to.area;
|
||||
impl<'a> Render<TuiContext, Rect> for Layered<'a, TuiContext, Rect> {
|
||||
fn render (&self, to: &mut impl TuiTarget) -> Perhaps<Rect> {
|
||||
let area = to.area();
|
||||
for layer in self.0.0.iter() {
|
||||
layer.render(to)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,161 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub(crate) use ratatui::buffer::Cell;
|
||||
use ratatui::backend::Backend;
|
||||
|
||||
pub struct TuiOutput<'a> {
|
||||
pub buffer: &'a mut Buffer,
|
||||
pub area: Rect
|
||||
}
|
||||
impl<'a> TuiOutput<'a> {
|
||||
pub fn area (&'a mut self, area: Rect) -> Self {
|
||||
Self { buffer: self.buffer, area }
|
||||
}
|
||||
}
|
||||
|
||||
/// Main thread render loop
|
||||
pub fn tui_render_thread <T> (exited: &Arc<AtomicBool>, device: &Arc<RwLock<T>>)
|
||||
-> Usually<JoinHandle<()>>
|
||||
where
|
||||
T: for <'a> Render<'a, TuiOutput<'a>, Rect> + 'static
|
||||
{
|
||||
let exited = exited.clone();
|
||||
let device = device.clone();
|
||||
let mut backend = CrosstermBackend::new(stdout());
|
||||
let area = backend.size()?;
|
||||
let mut buffers = [Buffer::empty(area), Buffer::empty(area)];
|
||||
let mut index = 0;
|
||||
let sleep = Duration::from_millis(20);
|
||||
Ok(spawn(move || {
|
||||
loop {
|
||||
if let Ok(device) = device.try_read() {
|
||||
let mut target = TuiOutput { buffer: &mut buffers[index], area };
|
||||
device.render(&mut target).expect("render failed");
|
||||
let previous_buffer = &buffers[1 - index];
|
||||
let current_buffer = &buffers[index];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
backend.draw(updates.into_iter()).expect("failed to render");
|
||||
buffers[1 - index].reset();
|
||||
index = 1 - index;
|
||||
}
|
||||
if exited.fetch_and(true, Ordering::Relaxed) {
|
||||
break
|
||||
}
|
||||
std::thread::sleep(sleep);
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/// Set up panic hook
|
||||
pub fn panic_hook_setup () {
|
||||
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
||||
std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{
|
||||
stdout().execute(LeaveAlternateScreen).unwrap();
|
||||
disable_raw_mode().unwrap();
|
||||
better_panic_handler(info);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Set up terminal
|
||||
pub fn terminal_setup () -> Usually<()> {
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cleanup
|
||||
pub fn terminal_teardown () -> Usually<()> {
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A simpler analog to [Render].
|
||||
pub trait Blit {
|
||||
// Render something to X, Y coordinates in a buffer, ignoring width/height.
|
||||
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect>;
|
||||
}
|
||||
|
||||
/// Text can be rendered.
|
||||
impl<T: AsRef<str>> Blit for T {
|
||||
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect> {
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
buf.set_string(x, y, self.as_ref(), style.unwrap_or(Style::default()));
|
||||
}
|
||||
Ok(Some(Rect { x, y, width: self.as_ref().len() as u16, height: 1 }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Rendering unit struct to Ratatui returns zero-sized [Rect] at render coordinates.
|
||||
impl<'a> Render<'a, TuiOutput<'a>, Rect> for () {
|
||||
fn render (&self, to: &mut TuiOutput<'a>) -> Perhaps<Rect> {
|
||||
Ok(Some(Rect { x: to.area.x, y: to.area.y, width: 0, height: 0 }))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_box (area: Rect, w: u16, h: u16) -> Rect {
|
||||
let width = w.min(area.width * 3 / 5);
|
||||
let height = h.min(area.width * 3 / 5);
|
||||
let x = area.x + (area.width - width) / 2;
|
||||
let y = area.y + (area.height - height) / 2;
|
||||
Rect { x, y, width, height }
|
||||
}
|
||||
|
||||
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
||||
match (lower, upper) {
|
||||
(true, true) => Some('█'),
|
||||
(true, false) => Some('▄'),
|
||||
(false, true) => Some('▀'),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FillBg(pub Color);
|
||||
|
||||
impl<'a> Render<'a, TuiOutput<'a>, Rect> for FillBg {
|
||||
fn render (&self, to: &mut TuiOutput<'a>) -> Perhaps<Rect> {
|
||||
fill_bg(to.buffer, to.area, self.0);
|
||||
Ok(Some(to.area))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_dim (buf: &mut Buffer) {
|
||||
for cell in buf.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 buffer_update (
|
||||
buf: &mut Buffer, area: Rect, callback: &impl Fn(&mut Cell, u16, u16)
|
||||
) {
|
||||
for row in 0..area.height {
|
||||
let y = area.y + row;
|
||||
for col in 0..area.width {
|
||||
let x = area.x + col;
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
callback(buf.get_mut(x, y), col, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_fg (buf: &mut Buffer, area: Rect, color: Color) {
|
||||
buffer_update(buf, area, &|cell,_,_|{cell.set_fg(color);})
|
||||
}
|
||||
|
||||
pub fn fill_bg (buf: &mut Buffer, area: Rect, color: Color) {
|
||||
buffer_update(buf, area, &|cell,_,_|{cell.set_bg(color);})
|
||||
}
|
||||
|
||||
pub fn fill_ul (buf: &mut Buffer, area: Rect, color: Color) {
|
||||
buffer_update(buf, area, &|cell,_,_|{
|
||||
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
|
||||
cell.underline_color = color;
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fill_char (buf: &mut Buffer, area: Rect, c: char) {
|
||||
buffer_update(buf, area, &|cell,_,_|{cell.set_char(c);})
|
||||
}
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
pub trait BorderStyle {
|
||||
const NW: &'static str = "";
|
||||
const N: &'static str = "";
|
||||
const NE: &'static str = "";
|
||||
const E: &'static str = "";
|
||||
const SE: &'static str = "";
|
||||
const S: &'static str = "";
|
||||
const SW: &'static str = "";
|
||||
const W: &'static str = "";
|
||||
|
||||
#[inline]
|
||||
fn draw <'a> (&self, to: &mut TuiOutput<'a>) -> Perhaps<Rect> {
|
||||
self.draw_horizontal(to.buffer, to.area, None)?;
|
||||
self.draw_vertical(to.buffer, to.area, None)?;
|
||||
self.draw_corners(to.buffer, to.area, None)?;
|
||||
Ok(Some(to.area))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn draw_horizontal (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||
let style = style.or_else(||self.style_horizontal());
|
||||
for x in area.x..(area.x+area.width).saturating_sub(1) {
|
||||
self.draw_north(buf, x, area.y, style)?;
|
||||
self.draw_south(buf, x, (area.y + area.height).saturating_sub(1), style)?;
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
#[inline]
|
||||
fn draw_north (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect> {
|
||||
Self::N.blit(buf, x, y, style)
|
||||
}
|
||||
#[inline]
|
||||
fn draw_south (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect> {
|
||||
Self::S.blit(buf, x, y, style)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn draw_vertical (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||
let style = style.or_else(||self.style_vertical());
|
||||
for y in area.y..(area.y+area.height).saturating_sub(1) {
|
||||
Self::W.blit(buf, area.x, y, style)?;
|
||||
Self::E.blit(buf, area.x + area.width - 1, y, style)?;
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn draw_corners (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||
let style = style.or_else(||self.style_corners());
|
||||
if area.width > 0 && area.height > 0 {
|
||||
Self::NW.blit(buf, area.x, area.y, style)?;
|
||||
Self::NE.blit(buf, area.x + area.width - 1, area.y, style)?;
|
||||
Self::SW.blit(buf, area.x, area.y + area.height - 1, style)?;
|
||||
Self::SE.blit(buf, area.x + area.width - 1, area.y + area.height - 1, style)?;
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn style (&self) -> Option<Style> {
|
||||
None
|
||||
}
|
||||
#[inline]
|
||||
fn style_horizontal (&self) -> Option<Style> {
|
||||
self.style()
|
||||
}
|
||||
#[inline]
|
||||
fn style_vertical (&self) -> Option<Style> {
|
||||
self.style()
|
||||
}
|
||||
#[inline]
|
||||
fn style_corners (&self) -> Option<Style> {
|
||||
self.style()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! border {
|
||||
($($T:ty {
|
||||
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
||||
$($x:tt)*
|
||||
}),+) => {
|
||||
$(impl BorderStyle for $T {
|
||||
const NW: &'static str = $nw;
|
||||
const N: &'static str = $n;
|
||||
const NE: &'static str = $ne;
|
||||
const W: &'static str = $w;
|
||||
const E: &'static str = $e;
|
||||
const SW: &'static str = $sw;
|
||||
const S: &'static str = $s;
|
||||
const SE: &'static str = $se;
|
||||
$($x)*
|
||||
})+
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Lozenge(pub Style);
|
||||
pub struct LozengeV(pub Style);
|
||||
pub struct LozengeDotted(pub Style);
|
||||
pub struct Quarter(pub Style);
|
||||
pub struct QuarterV(pub Style);
|
||||
pub struct Chamfer(pub Style);
|
||||
pub struct Corners(pub Style);
|
||||
|
||||
border! {
|
||||
Lozenge {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"╰" "─" "╯"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
LozengeV {
|
||||
"╭" "" "╮"
|
||||
"│" "│"
|
||||
"╰" "" "╯"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
LozengeDotted {
|
||||
"╭" "┅" "╮"
|
||||
"┇" "┇"
|
||||
"╰" "┅" "╯"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
Quarter {
|
||||
"▎" "▔" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "▁" "🮇"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
QuarterV {
|
||||
"▎" "" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "" "🮇"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
Chamfer {
|
||||
"🭂" "▔" "🭍"
|
||||
"▎" "🮇"
|
||||
"🭓" "▁" "🭞"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
Corners {
|
||||
"🬆" "" "🬊" // 🬴 🬸
|
||||
"" ""
|
||||
"🬱" "" "🬵"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
use crate::*;
|
||||
use ratatui::style::Modifier;
|
||||
|
||||
pub const COLOR_BG0: Color = Color::Rgb(30, 33, 36);
|
||||
pub const COLOR_BG1: Color = Color::Rgb(41, 46, 57);
|
||||
pub const COLOR_BG2: Color = Color::Rgb(46, 52, 64);
|
||||
pub const COLOR_BG3: Color = Color::Rgb(59, 66, 82);
|
||||
pub const COLOR_BG4: Color = Color::Rgb(67, 76, 94);
|
||||
pub const COLOR_BG5: Color = Color::Rgb(76, 86, 106);
|
||||
|
||||
pub trait Theme {
|
||||
const BG0: Color;
|
||||
const BG1: Color;
|
||||
const BG2: Color;
|
||||
const BG3: Color;
|
||||
const BG4: Color;
|
||||
const RED: Color;
|
||||
const YELLOW: Color;
|
||||
const GREEN: Color;
|
||||
|
||||
const PLAYING: Color;
|
||||
const SEPARATOR: Color;
|
||||
|
||||
fn bg_hier (focused: bool, entered: bool) -> Color {
|
||||
if focused && entered {
|
||||
Self::BG3
|
||||
} else if focused {
|
||||
Self::BG2
|
||||
} else {
|
||||
Self::BG1
|
||||
}
|
||||
}
|
||||
|
||||
fn bg_hi (focused: bool, entered: bool) -> Color {
|
||||
if focused && entered {
|
||||
Self::BG2
|
||||
} else if focused {
|
||||
Self::BG1
|
||||
} else {
|
||||
Self::BG0
|
||||
}
|
||||
}
|
||||
|
||||
fn bg_lo (focused: bool, entered: bool) -> Color {
|
||||
if focused && entered {
|
||||
Self::BG1
|
||||
} else if focused {
|
||||
Self::BG0
|
||||
} else {
|
||||
Color::Reset
|
||||
}
|
||||
}
|
||||
|
||||
fn style_hi (focused: bool, highlight: bool) -> Style {
|
||||
if highlight && focused {
|
||||
Style::default().yellow().not_dim()
|
||||
} else if highlight {
|
||||
Style::default().yellow().dim()
|
||||
} else {
|
||||
Style::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Nord;
|
||||
|
||||
impl Theme for Nord {
|
||||
const BG0: Color = Color::Rgb(41, 46, 57);
|
||||
const BG1: Color = Color::Rgb(46, 52, 64);
|
||||
const BG2: Color = Color::Rgb(59, 66, 82);
|
||||
const BG3: Color = Color::Rgb(67, 76, 94);
|
||||
const BG4: Color = Color::Rgb(76, 86, 106);
|
||||
const RED: Color = Color::Rgb(191, 97, 106);
|
||||
const YELLOW: Color = Color::Rgb(235, 203, 139);
|
||||
const GREEN: Color = Color::Rgb(163, 190, 140);
|
||||
|
||||
const PLAYING: Color = Color::Rgb(60, 100, 50);
|
||||
const SEPARATOR: Color = Color::Rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
pub const GRAY: Style = Style {
|
||||
fg: Some(Color::Gray),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
};
|
||||
|
||||
pub const GRAY_NOT_DIM: Style = Style {
|
||||
fg: Some(Color::Gray),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
|
||||
pub const DIM: Style = Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::DIM,
|
||||
sub_modifier: Modifier::empty(),
|
||||
};
|
||||
|
||||
pub const GRAY_DIM: Style = Style {
|
||||
fg: Some(Color::Gray),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::DIM,
|
||||
sub_modifier: Modifier::empty(),
|
||||
};
|
||||
|
||||
pub const WHITE_NOT_DIM_BOLD: Style = Style {
|
||||
fg: Some(Color::White),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::BOLD,
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
|
||||
pub const GRAY_NOT_DIM_BOLD: Style = Style {
|
||||
fg: Some(Color::Gray),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::BOLD,
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
|
||||
pub const NOT_DIM: Style = Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
|
||||
pub const NOT_DIM_GREEN: Style = Style {
|
||||
fg: Some(Color::Rgb(96, 255, 32)),
|
||||
bg: Some(COLOR_BG1),
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
|
||||
pub const NOT_DIM_BOLD: Style = Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::BOLD,
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
|
|
@ -10,6 +10,7 @@ pub struct Timebase {
|
|||
/// Ticks per beat
|
||||
pub ppq: AtomicF64,
|
||||
}
|
||||
|
||||
impl Default for Timebase {
|
||||
fn default () -> Self {
|
||||
Self {
|
||||
|
|
@ -19,6 +20,7 @@ impl Default for Timebase {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Timebase {
|
||||
pub fn new (rate: f64, bpm: f64, ppq: f64) -> Self {
|
||||
Self { rate: rate.into(), bpm: bpm.into(), ppq: ppq.into() }
|
||||
|
|
@ -101,3 +103,112 @@ impl Timebase {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
/// (pulses, name)
|
||||
pub const NOTE_DURATIONS: [(usize, &str);26] = [
|
||||
(1, "1/384"),
|
||||
(2, "1/192"),
|
||||
(3, "1/128"),
|
||||
(4, "1/96"),
|
||||
(6, "1/64"),
|
||||
(8, "1/48"),
|
||||
(12, "1/32"),
|
||||
(16, "1/24"),
|
||||
(24, "1/16"),
|
||||
(32, "1/12"),
|
||||
(48, "1/8"),
|
||||
(64, "1/6"),
|
||||
(96, "1/4"),
|
||||
(128, "1/3"),
|
||||
(192, "1/2"),
|
||||
(256, "2/3"),
|
||||
(384, "1/1"),
|
||||
(512, "4/3"),
|
||||
(576, "3/2"),
|
||||
(768, "2/1"),
|
||||
(1152, "3/1"),
|
||||
(1536, "4/1"),
|
||||
(2304, "6/1"),
|
||||
(3072, "8/1"),
|
||||
(3456, "9/1"),
|
||||
(6144, "16/1"),
|
||||
];
|
||||
|
||||
/// Returns the next shorter length
|
||||
pub fn prev_note_length (ppq: usize) -> usize {
|
||||
for i in 1..=16 {
|
||||
let length = NOTE_DURATIONS[16-i].0;
|
||||
if length < ppq {
|
||||
return length
|
||||
}
|
||||
}
|
||||
ppq
|
||||
}
|
||||
|
||||
/// Returns the next longer length
|
||||
pub fn next_note_length (ppq: usize) -> usize {
|
||||
for (length, _) in &NOTE_DURATIONS {
|
||||
if *length > ppq {
|
||||
return *length
|
||||
}
|
||||
}
|
||||
ppq
|
||||
}
|
||||
|
||||
pub fn ppq_to_name (ppq: usize) -> &'static str {
|
||||
for (length, name) in &NOTE_DURATIONS {
|
||||
if *length == ppq {
|
||||
return name
|
||||
}
|
||||
}
|
||||
""
|
||||
}
|
||||
|
||||
/// Defines frames per tick.
|
||||
pub struct Ticks(pub f64);
|
||||
|
||||
impl Ticks {
|
||||
/// Iterate over ticks between start and end.
|
||||
pub fn between_frames (&self, start: usize, end: usize) -> TicksIterator {
|
||||
TicksIterator(self.0, start, start, end)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator that emits subsequent ticks within a range.
|
||||
pub struct TicksIterator(f64, usize, usize, usize);
|
||||
|
||||
impl Iterator for TicksIterator {
|
||||
type Item = (usize, usize);
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.1 > self.3 {
|
||||
return None
|
||||
}
|
||||
let fpt = self.0;
|
||||
let frame = self.1 as f64;
|
||||
let start = self.2;
|
||||
let end = self.3;
|
||||
self.1 = self.1 + 1;
|
||||
//println!("{fpt} {frame} {start} {end}");
|
||||
let jitter = frame.rem_euclid(fpt); // ramps
|
||||
let next_jitter = (frame + 1.0).rem_euclid(fpt);
|
||||
if jitter > next_jitter { // at crossing:
|
||||
let time = (frame as usize) % (end as usize-start as usize);
|
||||
let tick = (frame / fpt) as usize;
|
||||
return Some((time, tick))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_frames_to_ticks () {
|
||||
let ticks = Ticks(12.3).between_frames(0, 100).collect::<Vec<_>>();
|
||||
println!("{ticks:?}");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
/// (pulses, name)
|
||||
pub const NOTE_DURATIONS: [(usize, &str);26] = [
|
||||
(1, "1/384"),
|
||||
(2, "1/192"),
|
||||
(3, "1/128"),
|
||||
(4, "1/96"),
|
||||
(6, "1/64"),
|
||||
(8, "1/48"),
|
||||
(12, "1/32"),
|
||||
(16, "1/24"),
|
||||
(24, "1/16"),
|
||||
(32, "1/12"),
|
||||
(48, "1/8"),
|
||||
(64, "1/6"),
|
||||
(96, "1/4"),
|
||||
(128, "1/3"),
|
||||
(192, "1/2"),
|
||||
(256, "2/3"),
|
||||
(384, "1/1"),
|
||||
(512, "4/3"),
|
||||
(576, "3/2"),
|
||||
(768, "2/1"),
|
||||
(1152, "3/1"),
|
||||
(1536, "4/1"),
|
||||
(2304, "6/1"),
|
||||
(3072, "8/1"),
|
||||
(3456, "9/1"),
|
||||
(6144, "16/1"),
|
||||
];
|
||||
|
||||
/// Returns the next shorter length
|
||||
pub fn prev_note_length (ppq: usize) -> usize {
|
||||
for i in 1..=16 {
|
||||
let length = NOTE_DURATIONS[16-i].0;
|
||||
if length < ppq {
|
||||
return length
|
||||
}
|
||||
}
|
||||
ppq
|
||||
}
|
||||
|
||||
/// Returns the next longer length
|
||||
pub fn next_note_length (ppq: usize) -> usize {
|
||||
for (length, _) in &NOTE_DURATIONS {
|
||||
if *length > ppq {
|
||||
return *length
|
||||
}
|
||||
}
|
||||
ppq
|
||||
}
|
||||
|
||||
pub fn ppq_to_name (ppq: usize) -> &'static str {
|
||||
for (length, name) in &NOTE_DURATIONS {
|
||||
if *length == ppq {
|
||||
return name
|
||||
}
|
||||
}
|
||||
""
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
/// Defines frames per tick.
|
||||
pub struct Ticks(pub f64);
|
||||
|
||||
impl Ticks {
|
||||
/// Iterate over ticks between start and end.
|
||||
pub fn between_frames (&self, start: usize, end: usize) -> TicksIterator {
|
||||
TicksIterator(self.0, start, start, end)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator that emits subsequent ticks within a range.
|
||||
pub struct TicksIterator(f64, usize, usize, usize);
|
||||
|
||||
impl Iterator for TicksIterator {
|
||||
type Item = (usize, usize);
|
||||
fn next (&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if self.1 > self.3 {
|
||||
return None
|
||||
}
|
||||
let fpt = self.0;
|
||||
let frame = self.1 as f64;
|
||||
let start = self.2;
|
||||
let end = self.3;
|
||||
self.1 = self.1 + 1;
|
||||
//println!("{fpt} {frame} {start} {end}");
|
||||
let jitter = frame.rem_euclid(fpt); // ramps
|
||||
let next_jitter = (frame + 1.0).rem_euclid(fpt);
|
||||
if jitter > next_jitter { // at crossing:
|
||||
let time = (frame as usize) % (end as usize-start as usize);
|
||||
let tick = (frame / fpt) as usize;
|
||||
return Some((time, tick))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_frames_to_ticks () {
|
||||
let ticks = Ticks(12.3).between_frames(0, 100).collect::<Vec<_>>();
|
||||
println!("{ticks:?}");
|
||||
}
|
||||
|
||||
}
|
||||
549
crates/tek_core/src/tui.rs
Normal file
549
crates/tek_core/src/tui.rs
Normal file
|
|
@ -0,0 +1,549 @@
|
|||
use crate::*;
|
||||
|
||||
pub(crate) use ratatui::buffer::Cell;
|
||||
pub(crate) use crossterm::{ExecutableCommand};
|
||||
pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers};
|
||||
pub use ratatui::prelude::{Rect, Style, Color, Buffer};
|
||||
pub use ratatui::style::{Stylize, Modifier};
|
||||
use ratatui::backend::{Backend, CrosstermBackend};
|
||||
use std::io::Stdout;
|
||||
use crossterm::terminal::{
|
||||
EnterAlternateScreen, LeaveAlternateScreen,
|
||||
enable_raw_mode, disable_raw_mode
|
||||
};
|
||||
|
||||
pub struct TuiContext {
|
||||
exited: Arc<AtomicBool>,
|
||||
buffer: usize,
|
||||
buffers: [Buffer;2],
|
||||
backend: CrosstermBackend<Stdout>,
|
||||
area: Rect,
|
||||
sleep: Duration,
|
||||
poll: Duration,
|
||||
}
|
||||
impl Engine for TuiContext {
|
||||
type Handled = bool;
|
||||
type Rendered = Rect;
|
||||
fn exited (&self) -> bool {
|
||||
self.exited.fetch_and(true, Ordering::Relaxed)
|
||||
}
|
||||
fn setup (&mut self) -> Usually<()> {
|
||||
panic_hook_setup();
|
||||
terminal_setup()
|
||||
}
|
||||
fn teardown (&mut self) -> Usually<()> {
|
||||
terminal_teardown()
|
||||
}
|
||||
fn handle (&mut self, state: &mut impl Handle<Self, bool>) -> Usually<()> {
|
||||
if ::crossterm::event::poll(self.poll).is_ok() {
|
||||
let event = ::crossterm::event::read().unwrap();
|
||||
if let Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, ..
|
||||
}) = event {
|
||||
self.exited.store(true, Ordering::Relaxed);
|
||||
} else if let Err(e) = state.handle(&mut self) {
|
||||
panic!("{e}")
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn render (&mut self, state: &impl Render<Self, Rect>) -> Usually<()> {
|
||||
if let Ok(state) = state.try_read() {
|
||||
state.render(&mut self).expect("render failed");
|
||||
}
|
||||
std::thread::sleep(self.sleep)
|
||||
}
|
||||
}
|
||||
impl TuiContext {
|
||||
/// Run the main loop.
|
||||
pub fn run <R: Component<TuiContext> + Sized + 'static> (
|
||||
state: Arc<RwLock<R>>
|
||||
) -> Usually<Arc<RwLock<R>>> {
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let area = backend.size()?;
|
||||
let engine = Arc::new(Self {
|
||||
sleep: Duration::from_millis(20),
|
||||
poll: Duration::from_millis(100),
|
||||
exited: Arc::new(AtomicBool::new(false)),
|
||||
buffer: 0,
|
||||
buffers: [Buffer::empty(area), Buffer::empty(area)],
|
||||
backend,
|
||||
area,
|
||||
});
|
||||
let _input_thread = {
|
||||
let engine = engine.clone();
|
||||
let state = state.clone();
|
||||
spawn(move || loop {
|
||||
if engine.exited() {
|
||||
break
|
||||
}
|
||||
engine.handle(&mut state).expect("handle failed");
|
||||
})
|
||||
};
|
||||
let main_thread = {
|
||||
let engine = engine.clone();
|
||||
let state = state.clone();
|
||||
spawn(move || loop {
|
||||
if engine.exited() {
|
||||
break
|
||||
}
|
||||
engine.render(&mut state).expect("render failed");
|
||||
})
|
||||
};
|
||||
main_thread.join().expect("main thread failed");
|
||||
Ok(state)
|
||||
}
|
||||
pub fn target <'a> (&'a mut self) -> impl TuiTarget + 'a {
|
||||
let area = self.area();
|
||||
(self.buffer(), area)
|
||||
}
|
||||
fn flip (&mut self) {
|
||||
let previous_buffer = &self.buffers[1 - self.buffer];
|
||||
let current_buffer = &self.buffers[self.buffer];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
self.backend.draw(updates.into_iter()).expect("failed to render");
|
||||
self.buffers[1 - self.buffer].reset();
|
||||
self.buffer = 1 - self.buffer;
|
||||
}
|
||||
pub fn fill_bg (&mut self, area: Rect, color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_bg(color);})
|
||||
}
|
||||
pub fn fill_fg (&mut self, area: Rect, color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{cell.set_fg(color);})
|
||||
}
|
||||
pub fn fill_ul (&mut self, area: Rect, color: Color) {
|
||||
self.buffer_update(area, &|cell,_,_|{
|
||||
cell.modifier = ratatui::prelude::Modifier::UNDERLINED;
|
||||
cell.underline_color = color;
|
||||
})
|
||||
}
|
||||
pub fn fill_char (&mut self, area: Rect, 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 buffer_update (&mut self, area: Rect, callback: &impl Fn(&mut Cell, u16, u16)) {
|
||||
let buf = self.buffer();
|
||||
for row in 0..area.height {
|
||||
let y = area.y + row;
|
||||
for col in 0..area.width {
|
||||
let x = area.x + col;
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
callback(buf.get_mut(x, y), col, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TuiTarget {
|
||||
fn area (&self) -> Rect;
|
||||
fn buffer (&mut self) -> &mut Buffer;
|
||||
}
|
||||
|
||||
impl TuiTarget for TuiContext {
|
||||
fn area (&self) -> Rect {
|
||||
self.area
|
||||
}
|
||||
fn buffer (&mut self) -> &mut Buffer {
|
||||
&mut self.buffers[self.buffer]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TuiTarget for (&'a mut Buffer, Rect) {
|
||||
fn area (&self) -> Rect {
|
||||
self.1
|
||||
}
|
||||
fn buffer (&mut self) -> &mut Buffer {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Set up panic hook
|
||||
pub fn panic_hook_setup () {
|
||||
let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler();
|
||||
std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{
|
||||
stdout().execute(LeaveAlternateScreen).unwrap();
|
||||
disable_raw_mode().unwrap();
|
||||
better_panic_handler(info);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Set up terminal
|
||||
pub fn terminal_setup () -> Usually<()> {
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cleanup
|
||||
pub fn terminal_teardown () -> Usually<()> {
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A simpler analog to [Render].
|
||||
pub trait Blit {
|
||||
// Render something to X, Y coordinates in a buffer, ignoring width/height.
|
||||
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect>;
|
||||
}
|
||||
|
||||
/// Text can be rendered.
|
||||
impl<T: AsRef<str>> Blit for T {
|
||||
fn blit (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect> {
|
||||
if x < buf.area.width && y < buf.area.height {
|
||||
buf.set_string(x, y, self.as_ref(), style.unwrap_or(Style::default()));
|
||||
}
|
||||
Ok(Some(Rect { x, y, width: self.as_ref().len() as u16, height: 1 }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Rendering unit struct to Ratatui returns zero-sized [Rect] at render coordinates.
|
||||
impl Render<TuiContext, Rect> for () {
|
||||
fn render (&self, to: &mut TuiContext) -> Perhaps<Rect> {
|
||||
Ok(Some(Rect { x: to.area.x, y: to.area.y, width: 0, height: 0 }))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn center_box (area: Rect, w: u16, h: u16) -> Rect {
|
||||
let width = w.min(area.width * 3 / 5);
|
||||
let height = h.min(area.width * 3 / 5);
|
||||
let x = area.x + (area.width - width) / 2;
|
||||
let y = area.y + (area.height - height) / 2;
|
||||
Rect { x, y, width, height }
|
||||
}
|
||||
|
||||
pub fn half_block (lower: bool, upper: bool) -> Option<char> {
|
||||
match (lower, upper) {
|
||||
(true, true) => Some('█'),
|
||||
(true, false) => Some('▄'),
|
||||
(false, true) => Some('▀'),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FillBg(pub Color);
|
||||
|
||||
impl Render<TuiContext, Rect> for FillBg {
|
||||
fn render (&self, to: &mut TuiContext) -> Perhaps<Rect> {
|
||||
to.fill_bg(to.area, self.0);
|
||||
Ok(Some(to.area))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait BorderStyle {
|
||||
const NW: &'static str = "";
|
||||
const N: &'static str = "";
|
||||
const NE: &'static str = "";
|
||||
const E: &'static str = "";
|
||||
const SE: &'static str = "";
|
||||
const S: &'static str = "";
|
||||
const SW: &'static str = "";
|
||||
const W: &'static str = "";
|
||||
|
||||
#[inline]
|
||||
fn draw <'a> (&self, to: &mut TuiOutput<'a>) -> Perhaps<Rect> {
|
||||
self.draw_horizontal(to.buffer, to.area, None)?;
|
||||
self.draw_vertical(to.buffer, to.area, None)?;
|
||||
self.draw_corners(to.buffer, to.area, None)?;
|
||||
Ok(Some(to.area))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn draw_horizontal (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||
let style = style.or_else(||self.style_horizontal());
|
||||
for x in area.x..(area.x+area.width).saturating_sub(1) {
|
||||
self.draw_north(buf, x, area.y, style)?;
|
||||
self.draw_south(buf, x, (area.y + area.height).saturating_sub(1), style)?;
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
#[inline]
|
||||
fn draw_north (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect> {
|
||||
Self::N.blit(buf, x, y, style)
|
||||
}
|
||||
#[inline]
|
||||
fn draw_south (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Perhaps<Rect> {
|
||||
Self::S.blit(buf, x, y, style)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn draw_vertical (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||
let style = style.or_else(||self.style_vertical());
|
||||
for y in area.y..(area.y+area.height).saturating_sub(1) {
|
||||
Self::W.blit(buf, area.x, y, style)?;
|
||||
Self::E.blit(buf, area.x + area.width - 1, y, style)?;
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn draw_corners (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||
let style = style.or_else(||self.style_corners());
|
||||
if area.width > 0 && area.height > 0 {
|
||||
Self::NW.blit(buf, area.x, area.y, style)?;
|
||||
Self::NE.blit(buf, area.x + area.width - 1, area.y, style)?;
|
||||
Self::SW.blit(buf, area.x, area.y + area.height - 1, style)?;
|
||||
Self::SE.blit(buf, area.x + area.width - 1, area.y + area.height - 1, style)?;
|
||||
}
|
||||
Ok(area)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn style (&self) -> Option<Style> {
|
||||
None
|
||||
}
|
||||
#[inline]
|
||||
fn style_horizontal (&self) -> Option<Style> {
|
||||
self.style()
|
||||
}
|
||||
#[inline]
|
||||
fn style_vertical (&self) -> Option<Style> {
|
||||
self.style()
|
||||
}
|
||||
#[inline]
|
||||
fn style_corners (&self) -> Option<Style> {
|
||||
self.style()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! border {
|
||||
($($T:ty {
|
||||
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
||||
$($x:tt)*
|
||||
}),+) => {
|
||||
$(impl BorderStyle for $T {
|
||||
const NW: &'static str = $nw;
|
||||
const N: &'static str = $n;
|
||||
const NE: &'static str = $ne;
|
||||
const W: &'static str = $w;
|
||||
const E: &'static str = $e;
|
||||
const SW: &'static str = $sw;
|
||||
const S: &'static str = $s;
|
||||
const SE: &'static str = $se;
|
||||
$($x)*
|
||||
})+
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Lozenge(pub Style);
|
||||
pub struct LozengeV(pub Style);
|
||||
pub struct LozengeDotted(pub Style);
|
||||
pub struct Quarter(pub Style);
|
||||
pub struct QuarterV(pub Style);
|
||||
pub struct Chamfer(pub Style);
|
||||
pub struct Corners(pub Style);
|
||||
|
||||
border! {
|
||||
Lozenge {
|
||||
"╭" "─" "╮"
|
||||
"│" "│"
|
||||
"╰" "─" "╯"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
LozengeV {
|
||||
"╭" "" "╮"
|
||||
"│" "│"
|
||||
"╰" "" "╯"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
LozengeDotted {
|
||||
"╭" "┅" "╮"
|
||||
"┇" "┇"
|
||||
"╰" "┅" "╯"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
Quarter {
|
||||
"▎" "▔" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "▁" "🮇"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
QuarterV {
|
||||
"▎" "" "🮇"
|
||||
"▎" "🮇"
|
||||
"▎" "" "🮇"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
Chamfer {
|
||||
"🭂" "▔" "🭍"
|
||||
"▎" "🮇"
|
||||
"🭓" "▁" "🭞"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
},
|
||||
Corners {
|
||||
"🬆" "" "🬊" // 🬴 🬸
|
||||
"" ""
|
||||
"🬱" "" "🬵"
|
||||
fn style (&self) -> Option<Style> {
|
||||
Some(self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const COLOR_BG0: Color = Color::Rgb(30, 33, 36);
|
||||
pub const COLOR_BG1: Color = Color::Rgb(41, 46, 57);
|
||||
pub const COLOR_BG2: Color = Color::Rgb(46, 52, 64);
|
||||
pub const COLOR_BG3: Color = Color::Rgb(59, 66, 82);
|
||||
pub const COLOR_BG4: Color = Color::Rgb(67, 76, 94);
|
||||
pub const COLOR_BG5: Color = Color::Rgb(76, 86, 106);
|
||||
|
||||
pub trait Theme {
|
||||
const BG0: Color;
|
||||
const BG1: Color;
|
||||
const BG2: Color;
|
||||
const BG3: Color;
|
||||
const BG4: Color;
|
||||
const RED: Color;
|
||||
const YELLOW: Color;
|
||||
const GREEN: Color;
|
||||
|
||||
const PLAYING: Color;
|
||||
const SEPARATOR: Color;
|
||||
|
||||
fn bg_hier (focused: bool, entered: bool) -> Color {
|
||||
if focused && entered {
|
||||
Self::BG3
|
||||
} else if focused {
|
||||
Self::BG2
|
||||
} else {
|
||||
Self::BG1
|
||||
}
|
||||
}
|
||||
|
||||
fn bg_hi (focused: bool, entered: bool) -> Color {
|
||||
if focused && entered {
|
||||
Self::BG2
|
||||
} else if focused {
|
||||
Self::BG1
|
||||
} else {
|
||||
Self::BG0
|
||||
}
|
||||
}
|
||||
|
||||
fn bg_lo (focused: bool, entered: bool) -> Color {
|
||||
if focused && entered {
|
||||
Self::BG1
|
||||
} else if focused {
|
||||
Self::BG0
|
||||
} else {
|
||||
Color::Reset
|
||||
}
|
||||
}
|
||||
|
||||
fn style_hi (focused: bool, highlight: bool) -> Style {
|
||||
if highlight && focused {
|
||||
Style::default().yellow().not_dim()
|
||||
} else if highlight {
|
||||
Style::default().yellow().dim()
|
||||
} else {
|
||||
Style::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Nord;
|
||||
|
||||
impl Theme for Nord {
|
||||
const BG0: Color = Color::Rgb(41, 46, 57);
|
||||
const BG1: Color = Color::Rgb(46, 52, 64);
|
||||
const BG2: Color = Color::Rgb(59, 66, 82);
|
||||
const BG3: Color = Color::Rgb(67, 76, 94);
|
||||
const BG4: Color = Color::Rgb(76, 86, 106);
|
||||
const RED: Color = Color::Rgb(191, 97, 106);
|
||||
const YELLOW: Color = Color::Rgb(235, 203, 139);
|
||||
const GREEN: Color = Color::Rgb(163, 190, 140);
|
||||
|
||||
const PLAYING: Color = Color::Rgb(60, 100, 50);
|
||||
const SEPARATOR: Color = Color::Rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
pub const GRAY: Style = Style {
|
||||
fg: Some(Color::Gray),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
};
|
||||
|
||||
pub const GRAY_NOT_DIM: Style = Style {
|
||||
fg: Some(Color::Gray),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
|
||||
pub const DIM: Style = Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::DIM,
|
||||
sub_modifier: Modifier::empty(),
|
||||
};
|
||||
|
||||
pub const GRAY_DIM: Style = Style {
|
||||
fg: Some(Color::Gray),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::DIM,
|
||||
sub_modifier: Modifier::empty(),
|
||||
};
|
||||
|
||||
pub const WHITE_NOT_DIM_BOLD: Style = Style {
|
||||
fg: Some(Color::White),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::BOLD,
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
|
||||
pub const GRAY_NOT_DIM_BOLD: Style = Style {
|
||||
fg: Some(Color::Gray),
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::BOLD,
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
|
||||
pub const NOT_DIM: Style = Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
|
||||
pub const NOT_DIM_GREEN: Style = Style {
|
||||
fg: Some(Color::Rgb(96, 255, 32)),
|
||||
bg: Some(COLOR_BG1),
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
|
||||
pub const NOT_DIM_BOLD: Style = Style {
|
||||
fg: None,
|
||||
bg: None,
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::BOLD,
|
||||
sub_modifier: Modifier::DIM,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue