diff --git a/Cargo.toml b/Cargo.toml index 37bdf64..96c6bf9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,12 +5,15 @@ version = "0.15.0" description = "UI metaframework." [features] -default = ["tui", "jack", "dsl"] +default = ["lang", "sing", "draw", "tui"] +lang = ["dep:dizzle"] +sing = ["dep:jack"] +draw = [] +tui = ["draw", "dep:ratatui", "dep:crossterm"] +gui = ["draw", "dep:winit"] + bumpalo = ["dep:bumpalo"] -tui = ["dep:ratatui", "dep:crossterm"] -gui = ["dep:winit"] dsl = ["dep:dizzle"] -jack = ["dep:jack"] [dependencies] anyhow = { version = "1.0" } diff --git a/README.md b/README.md index 7c61fe7..0bf1e39 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,36 @@ -***tengri*** is the skygod in whose dream the [**`tek`**](https://codeberg.org/unspeaker/tek) -is happening. +# tengri [![Please don't upload to GitHub](https://nogithub.codeberg.page/badge.svg)](https://nogithub.codeberg.page) -# features +***tengri*** is the skygod in whose dream the [**`tek`**](https://codeberg.org/unspeaker/tek) +is happening. it looks at us from its [**`perch`**](https://codeberg.org/unspeaker/perch). + +## features it is here to do the following: -## sing +### sing connect to jack audio connection kit to process chunks of audio and midi. -## draw +### draw abstract interface layout system for defining interface layouts abstractly. -## play +### play the input handling system. -## tui +### tui uses `ratatui` to run in a terminal. -## gui (todo) +### gui (todo-todo-todo) opens windows, runs shaders in them and/or delegates them (to e.g. plugin guis). -## lang +### lang uses `dizzle` to let you livecode all of the above. -# license +## license here and now, the blessings of `tengri` are invokable under the [**`AGPL3`**](./LICENSE). diff --git a/src/draw.rs b/src/draw.rs new file mode 100644 index 0000000..8af6098 --- /dev/null +++ b/src/draw.rs @@ -0,0 +1,407 @@ +use crate::*; + +/// A numeric type that can be used as coordinate. +/// +/// FIXME: Replace this ad-hoc trait with `num` crate. +pub trait Coord: Send + Sync + Copy + + Add + + Sub + + Mul + + Div + + Ord + PartialEq + Eq + + Debug + Display + Default + + From + Into + + Into + + Into +{ + fn zero () -> Self { 0.into() } + fn plus (self, other: Self) -> Self; + fn minus (self, other: Self) -> Self { if self >= other { self - other } else { 0.into() } } + fn atomic (self) -> AtomicUsize { AtomicUsize::new(self.into()) } +} + +/// Point along horizontal axis. +pub trait X { fn x (&self) -> N { N::zero() } } + +/// Point along vertical axis. +pub trait Y { fn y (&self) -> N { N::zero() } } + +/// Length along horizontal axis. +pub trait W { fn w (&self) -> N { N::zero() } } + +/// Length along vertical axis. +pub trait H { fn h (&self) -> N { N::zero() } } + +/// Which corner/side of a box is 0, 0 +pub trait Anchor { fn anchor (&self) -> Alignment; } + +/// Area along (X, Y). +pub trait Area: W + H { fn wh (&self) -> WH { WH(self.w(), self.h()) } } + +/// Point along (X, Y). +pub trait Point: X + Y { fn xy (&self) -> XY { XY(self.x(), self.y()) } } + +// Something that has a 2D bounding box (X, Y, W, H). +pub trait Bounded: Point + Area + Anchor { + fn x2 (&self) -> N { self.x().plus(self.w()) } + fn y2 (&self) -> N { self.y().plus(self.h()) } + fn xywh (&self) -> [N;4] { [..self.xy(), ..self.wh()] } + fn expect_min (&self, w: N, h: N) -> Usually<&Self> { + if self.w() < w || self.h() < h { + Err(format!("min {w}x{h}").into()) + } else { + Ok(self) + } + } +} + +/// A point (X, Y). +/// +/// ``` +/// let xy = tengri::XY(0u16, 0); +/// ``` +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XY(pub C, pub C); + +/// A size (Width, Height). +/// +/// ``` +/// let wh = tengri::WH(0u16, 0); +/// ``` +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct WH(pub C, pub C); + +/// Point with size. +/// +/// ``` +/// let xywh = tengri::XYWH(0u16, 0, 0, 0); +/// assert_eq!(tengri::XYWH(10u16, 10, 20, 20).center(), tengri::XY(20, 20)); +/// ``` +/// +/// * [ ] TODO: anchor field (determines at which corner/side is X0 Y0) +/// +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Copy, Clone, Debug, Default, PartialEq)] pub struct XYWH( + pub C, pub C, pub C, pub C +); + +/// A cardinal direction. +/// +/// ``` +/// let direction = tengri::Direction::Above; +/// ``` +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Copy, Clone, PartialEq, Debug)] pub enum Direction { + North, South, East, West, Above, Below +} + +/// 9th of area to place. +/// +/// ``` +/// let alignment = tengri::Alignment::Center; +/// ``` +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Debug, Copy, Clone, Default)] pub enum Alignment { + #[default] Center, X, Y, NW, N, NE, E, SE, S, SW, W +} + +/// Drawable with dynamic dispatch. +pub trait Draw: Fn(&mut T)->Usually<()> { fn draw (&self, to: &mut T) -> Usually<()>; } + +/// Drawable thunk. +impl()> Draw for F { fn draw (&self, to: &mut T) -> Usually<()> { self(to) } } + +impl Draw for () { fn draw (&self, _: &mut S) -> Usually<()> { Ok(()) } } +impl Draw for Box> { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } } + +impl> Draw for &D { fn draw (&self, to: &mut S) -> Usually<()> { (*self).draw(to) } } +impl> Draw for &mut D { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } } +impl> Draw for Arc { fn draw (&self, to: &mut S) -> Usually<()> { (**self).draw(to) } } +impl> Draw for RwLock { fn draw (&self, to: &mut S) -> Usually<()> { self.read().unwrap().draw(to) } } +impl> Draw for Option { fn draw (&self, to: &mut S) { if let Some(draw) = self { draw.draw(to) } } } + +/// Draw the content or its error message. +/// +/// ``` +/// let _ = tengri::Catcher::::new(Ok(Some("hello"))); +/// let _ = tengri::Catcher::::new(Ok(None)); +/// let _ = tengri::Catcher::::new(Err("draw fail".into())); +/// ``` +pub fn catcher (error: Perhaps, draw: impl Draw) -> impl Draw { + move|to|match self.0.as_ref() { + Ok(Some(content)) => draw(to), + Ok(None) => to.blit(&"", 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, "oopsie daisy. "), "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))) + } + } +} +/// Only render when condition is true. +/// +/// ``` +/// fn test () -> impl tengri::Draw { +/// tengri::when(true, "Yes") +/// } +/// ``` +pub fn when (condition: bool, draw: impl Draw) -> impl Draw { + move|to|Ok(if condition { draw(to)? } }) +} + +/// Render one thing if a condition is true and another false. +/// +/// ``` +/// fn test () -> impl tengri::Draw { +/// tengri::either(true, "Yes", "No") +/// } +/// ``` +pub fn either (condition: bool, a: impl Draw, b: impl Draw) -> impl Draw { + move|to|Ok(if condition { a(to)? } else { b(to)? }) +} + +/// Set maximum width and/or height of drawing area. +pub fn clip (w: Option, h: Option, draw: impl Draw) -> impl Draw { + move|to|draw(to.clip(w, h)) +} + +/// Shrink drawing area symmetrically. +pub fn pad (w: Option, h: Option, draw: impl Draw) -> impl Draw { + move|to|draw(to.pad(w, h)) +} + +/// Only draw content if area is above a certain size. +/// +/// ``` +/// let minimum = tengri::Min::XY(3, 5, "Hello"); // 5x5 +/// ``` +pub fn min (w: N, draw: impl Draw) -> impl Draw { + move|to|draw(to.min(w, h)) +} + +/// Set the maximum width and/or height of the content. +/// +/// ``` +/// let maximum = tengri::Max::XY(3, 5, "Hello"); // 3x1 +/// ``` +pub fn max (x: N, draw: impl Draw) -> impl Draw { + move|to|draw(to.max(w, h)) +} + +// pub fn displace ... + +/// Shrink by amount on each axis. +/// +/// ``` +/// /// TODO +/// ``` +pub fn border (on: bool, style: S, draw: impl Draw) -> impl Draw { + fill_wh(above(when(on, |output|{/*TODO*/}), pad(Some(1), Some(1), draw))) +} + +/// Stackably padded. +/// +/// ``` +/// /// TODO +/// ``` +pub fn phat ( + w: N, h: N, [fg, bg, hi, lo]: [Color;4], draw: impl Draw +) -> impl Draw { + let top = exact_w(1, Self::lo(bg, hi)); + let low = exact_h(1, Self::hi(bg, lo)); + let draw = TuiOut::fg_bg(fg, bg, draw); + min_wh(w, h, bsp_s(top, bsp_n(low, fill_wh(draw)))) +} + +pub fn iter (items: impl Iterator>) -> impl Draw { + todo!() +} + +/// Split screen between two items, or layer them atop each other. +/// +/// ``` +/// use tengri::Direction::*; +/// let _ = tengri::draw::bsp(Above, (), ()); +/// let _ = tengri::draw::bsp(Below, (), ()); +/// let _ = tengri::draw::bsp(North, (), ()); +/// let _ = tengri::draw::bsp(South, (), ()); +/// let _ = tengri::draw::bsp(East, (), ()); +/// let _ = tengri::draw::bsp(West, (), ()); +/// ``` +pub fn bsp (dir: Direction, a: impl Draw, b: impl Draw) -> impl Draw { + move|to|{ + //fn split_cardinal ( + //direction: Direction, area: XYWH, a: N + //) -> (XYWH, XYWH) { + //let XYWH(x, y, w, h) = area; + //match direction { + //North => (XYWH(x, y.plus(h).minus(a), w, a), XYWH(x, y, w, h.minus(a))), + //South => (XYWH(x, y, w, a), XYWH(x, y.plus(a), w, h.minus(a))), + //East => (XYWH(x, y, a, h), XYWH(x.plus(a), y, w.minus(a), h)), + //West => (XYWH(x.plus(w).minus(a), y, a, h), XYWH(x, y, w.minus(a), h)), + //Above | Below => (area, area) + //} + //} + //fn bsp_areas , B: TwoD> ( + //area: [N;4], + //direction: Direction, + //a: &A, + //b: &B, + //) -> [XYWH;3] { + //let XYWH(x, y, w, h) = area; + //let WH(aw, ah) = a.layout(area).wh(); + //let WH(bw, bh) = b.layout(match direction { + //South => XYWH(x, y + ah, w, h.minus(ah)), + //North => XYWH(x, y, w, h.minus(ah)), + //East => XYWH(x + aw, y, w.minus(aw), h), + //West => XYWH(x, y, w.minus(aw), h), + //Above => area, + //Below => area, + //}).wh(); + //match direction { + //Above | Below => { + //let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah.max(bh)]); + //let a = XYWH((x + w/2.into()).minus(aw/2.into()), (y + h/2.into()).minus(ah/2.into()), aw, ah); + //let b = XYWH((x + w/2.into()).minus(bw/2.into()), (y + h/2.into()).minus(bh/2.into()), bw, bh); + //[a.into(), b.into(), XYWH(x, y, w, h)] + //}, + //South => { + //let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); + //let a = XYWH((x + w/2.into()).minus(aw/2.into()), y, aw, ah); + //let b = XYWH((x + w/2.into()).minus(bw/2.into()), y + ah, bw, bh); + //[a.into(), b.into(), XYWH(x, y, w, h)] + //}, + //North => { + //let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); + //let a = XYWH((x + (w/2.into())).minus(aw/2.into()), y + bh, aw, ah); + //let b = XYWH((x + (w/2.into())).minus(bw/2.into()), y, bw, bh); + //[a.into(), b.into(), XYWH(x, y, w, h)] + //}, + //East => { + //let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); + //let a = XYWH(x, (y + h/2.into()).minus(ah/2.into()), aw, ah); + //let b = XYWH(x + aw, (y + h/2.into()).minus(bh/2.into()), bw, bh); + //[a.into(), b.into(), XYWH(x, y, w, h)] + //}, + //West => { + //let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); + //let a = XYWH(x + bw, (y + h/2.into()).minus(ah/2.into()), aw, ah); + //let b = XYWH(x, (y + h/2.into()).minus(bh/2.into()), bw, bh); + //[a.into(), b.into(), XYWH(x, y, w, h)] + //}, + //} + //} + //pub fn iter + } +} + +/// 3-column layout with center priority. +/// +/// ``` +/// /// TODO +/// ``` +pub fn tryptich ( + top: boolean, [(w_a, ref a), (w_b, ref b), (w_c, ref c)]: [(N, impl Draw);3] +) -> impl Draw { + let Self { top, h, left: (w_a, ref a), middle: (w_b, ref b), right: (w_c, ref c) } = *self; + let a = exact_w(w_a, a); + let b = exact_w(w_b, align_w(b)); + let c = exact_w(w_c, c); + exact_h(h, if top { + bsp_above(fill_w(align_n(b)), bsp_above(fill_w(align_nw(a)), fill_w(align_ne(c)))) + } else { + bsp_above(fill_wh(align_c(b)), bsp_above(fill_wh(align_w(a)), fill_wh(align_e(c)))) + }) +} + +impl> Draw for Measure { + fn draw (&self, to: &mut O) { + // TODO: 🡘 🡙 ←🡙→ indicator to expand window when too small + self.x.store(to.area().w().into(), Relaxed); + self.y.store(to.area().h().into(), Relaxed); + } +} + +impl> Draw for Bounded { + fn draw (&self, to: &mut O) { + let area = to.area(); + *to.area_mut() = self.0; + self.1.draw(to); + *to.area_mut() = area; + } +} + +impl> Draw for Align { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), &self.1).draw(to) } +} + +impl> Draw for Pad { + fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } +} + +impl, Tail: Draw> Draw for Bsp { + fn draw (&self, to: &mut O) { + let [a, b, _] = bsp_areas(to.area(), self.0, &self.1, &self.2); + //panic!("{a:?} {b:?}"); + if self.0 == Below { + to.show(a, &self.1); + to.show(b, &self.2); + } else { + to.show(b, &self.2); + to.show(a, &self.1); + } + } +} + +/// Clear a pre-allocated buffer, then write into it. +#[macro_export] macro_rules! rewrite { + ($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } } +} + +/// FIXME: This macro should be some variant of `eval`, too. +/// But taking into account the different signatures (resolving them into 1?) +#[cfg(feature = "lang")] #[macro_export] macro_rules! draw { + ($State:ident: $Output:ident: $layers:expr) => { + impl Draw<$Output> for $State { + fn draw (&self, to: &mut $Output) { + for layer in $layers { layer(self, to) } + } + } + } +} + +/// FIXME: This is generic: should be called `eval` and be part of [dizzle]. +#[cfg(feature = "lang")] #[macro_export] macro_rules! view { + ($State:ident: $Output:ident: $namespaces:expr) => { + impl Understand<$Output, ()> for $State { + fn understand_expr <'a> (&'a self, to: &mut $Output, expr: &'a impl Expression) -> Usually<()> { + for namespace in $namespaces { if namespace(self, to, expr)? { return Ok(()) } } + Err(format!("{}::<{}, ()>::understand_expr: unexpected: {expr:?}", + stringify! { $State }, + stringify! { $Output }).into()) + } + } + } +} + +/// Stack things on top of each other, +#[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{ + let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)* bsp +}}); + +/// Stack southward. +#[macro_export] macro_rules! col (($($expr:expr),* $(,)?) => {{ + let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)* bsp +}}); + +/// Stack northward. +#[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{ + let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)* bsp +}}); + +/// Stack eastward. +#[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{ + let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)* bsp +}}); diff --git a/src/play.rs b/src/play.rs new file mode 100644 index 0000000..686ab86 --- /dev/null +++ b/src/play.rs @@ -0,0 +1,41 @@ +use crate::*; + +/// Define an enum containing commands, and implement [Command] trait for over given `State`. +#[macro_export] macro_rules! def_command ( + ($Command:ident: |$state:ident: $State:ty| { + // FIXME: support attrs (docstrings) + $($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)? + })=>{ + #[derive(Debug)] pub enum $Command { + // FIXME: support attrs (docstrings) + $($Variant $({ $($arg: $Arg),* })?),* + } + impl ::tengri::Command<$State> for $Command { + fn execute (&self, $state: &mut $State) -> Perhaps { + match self { + $(Self::$Variant $({ $($arg),* })? => $body,)* + _ => unimplemented!("Command<{}>: {self:?}", stringify!($State)), + } + } + } + }); + +/// Implement [Handle] for given `State` and `handler`. +#[macro_export] macro_rules! handle { + (|$self:ident:$State:ty,$input:ident|$handler:expr) => { + impl ::tengri::Handle for $State { + fn handle (&mut $self, $input: &E) -> Perhaps { + $handler + } + } + }; + ($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => { + impl ::tengri::Handle<$E> for $State { + fn handle (&mut $self, $input: &$E) -> + Perhaps<<$E as ::tengri::Input>::Handled> + { + $handler + } + } + } +} diff --git a/src/sing.rs b/src/sing.rs new file mode 100644 index 0000000..08c0bcb --- /dev/null +++ b/src/sing.rs @@ -0,0 +1,201 @@ +use crate::*; +use ::jack::*; +use JackState::*; + +/// Wraps [JackState], and through it [jack::Client] when connected. +/// +/// ``` +/// let jack = tengri::Jack::default(); +/// ``` +#[derive(Clone, Debug, Default)] pub struct Jack<'j> ( + pub(crate) Arc>> +); + +/// This is a connection which may be [Inactive], [Activating], or [Active]. +/// In the [Active] and [Inactive] states, [JackState::client] returns a +/// [jack::Client], which you can use to talk to the JACK API. +/// +/// ``` +/// let state = tengri::JackState::default(); +/// ``` +#[derive(Debug, Default)] pub enum JackState<'j> { + /// Unused + #[default] Inert, + /// Before activation. + Inactive(Client), + /// During activation. + Activating, + /// After activation. Must not be dropped for JACK thread to persist. + Active(DynamicAsyncClient<'j>), +} + +/// Event enum for JACK events. +/// +/// ``` +/// let event = tengri::JackEvent::XRun; +/// ``` +#[derive(Debug, Clone, PartialEq)] pub enum JackEvent { + ThreadInit, + Shutdown(ClientStatus, Arc), + Freewheel(bool), + SampleRate(Frames), + ClientRegistration(Arc, bool), + PortRegistration(PortId, bool), + PortRename(PortId, Arc, Arc), + PortsConnected(PortId, PortId, bool), + GraphReorder, + XRun, +} + +/// Generic notification handler that emits [JackEvent] +/// +/// ``` +/// let notify = tengri::JackNotify(|_|{}); +/// ``` +pub struct JackNotify(pub T); + +/// Running JACK [AsyncClient] with maximum type erasure. +/// +/// One [Box] contains function that handles [JackEvent]s. +/// +/// Another [Box] containing a function that handles realtime IO. +/// +/// That's all it knows about them. +pub type DynamicAsyncClient<'j> + = AsyncClient, DynamicAudioHandler<'j>>; + +/// Notification handler wrapper for [BoxedAudioHandler]. +pub type DynamicAudioHandler<'j> = + ::jack::contrib::ClosureProcessHandler<(), BoxedAudioHandler<'j>>; + +/// Boxed realtime callback. +pub type BoxedAudioHandler<'j> = + Box Control + Send + Sync + 'j>; + +/// Notification handler wrapper for [BoxedJackEventHandler]. +pub type DynamicNotifications<'j> = + JackNotify>; + +/// Boxed [JackEvent] callback. +pub type BoxedJackEventHandler<'j> = + Box; + +impl<'j> HasJack<'j> for Jack<'j> { fn jack (&self) -> &Jack<'j> { self } } + +impl<'j> HasJack<'j> for &Jack<'j> { fn jack (&self) -> &Jack<'j> { self } } + +/// Implement [Jack] constructor and methods +impl<'j> Jack<'j> { + /// Register new [Client] and wrap it for shared use. + pub fn new_run + Audio + Send + Sync + 'static> ( + name: &impl AsRef, + init: impl FnOnce(Jack<'j>)->Usually + ) -> Usually>> { + Jack::new(name)?.run(init) + } + + pub fn new (name: &impl AsRef) -> Usually { + let client = Client::new(name.as_ref(), ClientOptions::NO_START_SERVER)?.0; + Ok(Jack(Arc::new(RwLock::new(JackState::Inactive(client))))) + } + + pub fn run + Audio + Send + Sync + 'static> + (self, init: impl FnOnce(Self)->Usually) -> Usually>> + { + let client_state = self.0.clone(); + let app: Arc> = Arc::new(RwLock::new(init(self)?)); + let mut state = Activating; + std::mem::swap(&mut*client_state.write().unwrap(), &mut state); + if let Inactive(client) = state { + // This is the misc notifications handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [JackEvent], which is + // one of the available misc notifications. + let notify = JackNotify(Box::new({ + let app = app.clone(); + move|event|(&mut*app.write().unwrap()).handle(event) + }) as BoxedJackEventHandler); + // This is the main processing handler. It's a struct that wraps a [Box] + // which performs type erasure on a callback that takes [Client] and [ProcessScope] + // and passes them down to the `app`'s `process` callback, which in turn + // implements audio and MIDI input and output on a realtime basis. + let process = ::jack::contrib::ClosureProcessHandler::new(Box::new({ + let app = app.clone(); + move|c: &_, s: &_|if let Ok(mut app) = app.write() { + app.process(c, s) + } else { + Control::Quit + } + }) as BoxedAudioHandler); + // Launch a client with the two handlers. + *client_state.write().unwrap() = Active( + client.activate_async(notify, process)? + ); + } else { + unreachable!(); + } + Ok(app) + } + + /// Run something with the client. + pub fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { + match &*self.0.read().unwrap() { + Inert => panic!("jack client not activated"), + Inactive(client) => op(client), + Activating => panic!("jack client has not finished activation"), + Active(client) => op(client.as_client()), + } + } +} + +impl NotificationHandler for JackNotify { + fn thread_init(&self, _: &Client) { + self.0(JackEvent::ThreadInit); + } + unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { + self.0(JackEvent::Shutdown(status, reason.into())); + } + fn freewheel(&mut self, _: &Client, enabled: bool) { + self.0(JackEvent::Freewheel(enabled)); + } + fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { + self.0(JackEvent::SampleRate(frames)); + Control::Quit + } + fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { + self.0(JackEvent::ClientRegistration(name.into(), reg)); + } + fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { + self.0(JackEvent::PortRegistration(id, reg)); + } + fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { + self.0(JackEvent::PortRename(id, old.into(), new.into())); + Control::Continue + } + fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { + self.0(JackEvent::PortsConnected(a, b, are)); + } + fn graph_reorder(&mut self, _: &Client) -> Control { + self.0(JackEvent::GraphReorder); + Control::Continue + } + fn xrun(&mut self, _: &Client) -> Control { + self.0(JackEvent::XRun); + Control::Continue + } +} + +impl JackPerfModel for PerfModel { + fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope) { + 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( + scope.cycle_times().unwrap().period_usecs as f64, + Relaxed, + ); + } + } +} diff --git a/src/tengri.rs b/src/tengri.rs index 7c1968b..5e9e25e 100644 --- a/src/tengri.rs +++ b/src/tengri.rs @@ -9,502 +9,56 @@ #![feature(trait_alias)] #![feature(type_alias_impl_trait)] #![feature(type_changing_struct_update)] +pub extern crate atomic_float; +pub extern crate palette; +pub extern crate better_panic; +pub extern crate unicode_width; +mod tengri_macros; mod tengri_struct; pub use self::tengri_struct::*; mod tengri_trait; pub use self::tengri_trait::*; mod tengri_impl; pub use self::tengri_impl::*; +mod tengri_fns; pub use self::tengri_fns::*; + #[cfg(test)] pub(crate) use proptest_derive::Arbitrary; -pub extern crate dizzle; pub use dizzle::*; -pub extern crate atomic_float; pub(crate) use atomic_float::AtomicF64; -pub extern crate palette; pub(crate) use ::palette::{*, convert::*, okhsl::*}; -pub extern crate better_panic; pub(crate) use better_panic::{Settings, Verbosity}; -pub extern crate unicode_width; pub(crate) use unicode_width::*; -#[cfg(feature = "jack")] pub extern crate jack; -#[cfg(feature = "jack")] pub use jack::{*, contrib::{*, ClosureProcessHandler}}; + +pub(crate) use ::{ + atomic_float::AtomicF64, + palette::{*, convert::*, okhsl::*}, + better_panic::{Settings, Verbosity}, + unicode_width::*, + std::{ + io::{stdout, Stdout, Write}, + sync::{Arc, Weak, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}}, + fmt::{Debug, Display}, + ops::{Add, Sub, Mul, Div}, + marker::PhantomData, + time::Duration, + thread::{spawn, JoinHandle} + } +}; + +#[cfg(feature = "lang")] extern crate dizzle; +#[cfg(feature = "lang")] pub use ::dizzle::self as lang; + +#[cfg(feature = "draw")] pub mod draw; + #[cfg(feature = "tui")] pub extern crate ratatui; -#[cfg(feature = "tui")] pub(crate) use ::ratatui::{ - prelude::{Color, Style, Buffer, Position}, - style::{Stylize, Modifier, Color::*}, - backend::{Backend, CrosstermBackend, ClearType}, - layout::{Size, Rect}, - buffer::Cell -}; #[cfg(feature = "tui")] pub extern crate crossterm; -#[cfg(feature = "tui")] pub(crate) use ::crossterm::{ - ExecutableCommand, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, - event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, -}; -pub(crate) use ::std::{ - io::{stdout, Stdout, Write}, - sync::{Arc, Weak, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}}, - fmt::{Debug, Display}, - ops::{Add, Sub, Mul, Div}, - marker::PhantomData, - time::Duration, - thread::{spawn, JoinHandle} +#[cfg(feature = "tui")] pub(crate) use ::{ + ratatui::{ + prelude::{Color, Style, Buffer, Position}, + style::{Stylize, Modifier, Color::*}, + backend::{Backend, CrosstermBackend, ClearType}, + layout::{Size, Rect}, + buffer::Cell + }, + crossterm::{ + ExecutableCommand, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, + event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, + } }; -// Define macros first, so that private macros are available in private modules: - -/// Clear a pre-allocated buffer, then write into it. -#[macro_export] macro_rules! rewrite { - ($buf:ident, $($rest:tt)*) => { |$buf,_,_|{ $buf.clear(); write!($buf, $($rest)*) } } -} - -/// FIXME: This macro should be some variant of `eval`, too. -/// But taking into account the different signatures (resolving them into 1?) -#[cfg(feature = "dsl")] #[macro_export] macro_rules! draw { - ($State:ident: $Output:ident: $layers:expr) => { - impl Draw<$Output> for $State { - fn draw (&self, to: &mut $Output) { - for layer in $layers { layer(self, to) } - } - } - } -} - -/// FIXME: This is generic: should be called `eval` and be part of [dizzle]. -#[cfg(feature = "dsl")] #[macro_export] macro_rules! view { - ($State:ident: $Output:ident: $namespaces:expr) => { - impl Understand<$Output, ()> for $State { - fn understand_expr <'a> (&'a self, to: &mut $Output, expr: &'a impl Expression) -> Usually<()> { - for namespace in $namespaces { if namespace(self, to, expr)? { return Ok(()) } } - Err(format!("{}::<{}, ()>::understand_expr: unexpected: {expr:?}", - stringify! { $State }, - stringify! { $Output }).into()) - } - } - } -} - -/// Stack things on top of each other, -#[macro_export] macro_rules! lay (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = Bsp::b(bsp, $expr);)* bsp -}}); - -/// Stack southward. -#[macro_export] macro_rules! col (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = Bsp::s(bsp, $expr);)* bsp -}}); - -/// Stack northward. -#[macro_export] macro_rules! col_up (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = Bsp::n(bsp, $expr);)* bsp -}}); - -/// Stack eastward. -#[macro_export] macro_rules! row (($($expr:expr),* $(,)?) => {{ - let bsp = (); $(let bsp = Bsp::e(bsp, $expr);)* bsp -}}); - -/// Define an enum containing commands, and implement [Command] trait for over given `State`. -#[macro_export] macro_rules! def_command ( - ($Command:ident: |$state:ident: $State:ty| { - // FIXME: support attrs (docstrings) - $($Variant:ident$({$($arg:ident:$Arg:ty),+ $(,)?})?=>$body:expr),* $(,)? - })=>{ - #[derive(Debug)] pub enum $Command { - // FIXME: support attrs (docstrings) - $($Variant $({ $($arg: $Arg),* })?),* - } - impl ::tengri::Command<$State> for $Command { - fn execute (&self, $state: &mut $State) -> Perhaps { - match self { - $(Self::$Variant $({ $($arg),* })? => $body,)* - _ => unimplemented!("Command<{}>: {self:?}", stringify!($State)), - } - } - } - }); - -/// Implement [Handle] for given `State` and `handler`. -#[macro_export] macro_rules! handle { - (|$self:ident:$State:ty,$input:ident|$handler:expr) => { - impl ::tengri::Handle for $State { - fn handle (&mut $self, $input: &E) -> Perhaps { - $handler - } - } - }; - ($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => { - impl ::tengri::Handle<$E> for $State { - fn handle (&mut $self, $input: &$E) -> - Perhaps<<$E as ::tengri::Input>::Handled> - { - $handler - } - } - } -} - -#[macro_export] macro_rules! tui_main { - ($expr:expr) => { - fn main () -> Usually<()> { - let engine = ::tengri::Tui::new(Box::new(stdout()))?; - let state = ::std::sync::Arc::new(std::sync::RwLock::new($expr)); - engine.run(true, &state)?; - Ok(()) - } - }; -} - -#[macro_export] macro_rules! has_color { - (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { - impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? { - fn color (&$self) -> ItemColor { $cb } - } - } -} - -/// Define layout operation. -/// -/// ``` -/// # use tengri::*; -/// struct Target { xywh: XYWH /*platform-specific*/} -/// impl tengri::Out for Target { -/// type Unit = u16; -/// fn area (&self) -> XYWH { self.xywh } -/// fn area_mut (&mut self) -> &mut XYWH { &mut self.xywh } -/// fn place_at <'t, T: Draw + ?Sized> (&mut self, area: XYWH, content: &'t T) {} -/// } -/// -/// struct State {/*app-specific*/} -/// impl<'b> Namespace<'b, bool> for State {} -/// impl<'b> Namespace<'b, u16> for State {} -/// impl Understand for State {} -/// -/// # fn main () -> tengri::Usually<()> { -/// let state = State {}; -/// let mut target = Target { xywh: Default::default() }; -/// evaluate_output_expression(&state, &mut target, &"")?; -/// evaluate_output_expression(&state, &mut target, &"(when true (text hello))")?; -/// evaluate_output_expression(&state, &mut target, &"(either true (text hello) (text world))")?; -/// // TODO test all -/// # Ok(()) } -/// ``` -#[cfg(feature = "dsl")] pub fn evaluate_output_expression <'a, O: Out + 'a, S> ( - state: &S, output: &mut O, expr: &'a impl Expression -) -> Usually where - S: Understand - + for<'b>Namespace<'b, bool> - + for<'b>Namespace<'b, O::Unit> -{ - // First element of expression is used for dispatch. - // Dispatch is proto-namespaced using separator character - let head = expr.head()?; - let mut frags = head.src()?.unwrap_or_default().split("/"); - // The rest of the tokens in the expr are arguments. - // Their meanings depend on the dispatched operation - let args = expr.tail(); - let arg0 = args.head(); - let tail0 = args.tail(); - let arg1 = tail0.head(); - let tail1 = tail0.tail(); - let arg2 = tail1.head(); - // And we also have to do the above binding dance - // so that the Perhapss remain in scope. - match frags.next() { - - Some("when") => output.place(&When::new( - state.namespace(arg0?)?.unwrap(), - Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()) - )), - - Some("either") => output.place(&Either::new( - state.namespace(arg0?)?.unwrap(), - Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()), - Thunk::new(move|output: &mut O|state.understand(output, &arg2).unwrap()) - )), - - Some("bsp") => output.place(&{ - let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); - let b = Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()); - match frags.next() { - Some("n") => Bsp::n(a, b), - Some("s") => Bsp::s(a, b), - Some("e") => Bsp::e(a, b), - Some("w") => Bsp::w(a, b), - Some("a") => Bsp::a(a, b), - Some("b") => Bsp::b(a, b), - frag => unimplemented!("bsp/{frag:?}") - } - }), - - Some("align") => output.place(&{ - let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); - match frags.next() { - Some("n") => Align::n(a), - Some("s") => Align::s(a), - Some("e") => Align::e(a), - Some("w") => Align::w(a), - Some("x") => Align::x(a), - Some("y") => Align::y(a), - Some("c") => Align::c(a), - frag => unimplemented!("align/{frag:?}") - } - }), - - Some("fill") => output.place(&{ - let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); - match frags.next() { - Some("xy") | None => Fill::XY(a), - Some("x") => Fill::X(a), - Some("y") => Fill::Y(a), - frag => unimplemented!("fill/{frag:?}") - } - }), - - Some("fixed") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Fixed::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Fixed::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Fixed::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})", - head.src()?.unwrap_or_default().split("/").next()) - } - }), - - Some("min") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Min::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Min::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Min::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("min/{frag:?}") - } - }), - - Some("max") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Max::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Max::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Max::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("max/{frag:?}") - } - }), - - Some("push") => output.place(&{ - let axis = frags.next(); - let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; - let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); - match axis { - Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), - Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb), - Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb), - frag => unimplemented!("push/{frag:?}") - } - }), - - _ => return Ok(false) - - }; - Ok(true) -} - - -/// Trim string with [unicode_width]. -pub fn trim_string (max_width: usize, input: impl AsRef) -> 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() -} - -pub(crate) fn width_chars_max (max: u16, text: impl AsRef) -> 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 -} - -#[inline] pub fn map_south( - item_offset: O::Unit, - item_height: O::Unit, - item: impl Content -) -> impl Content { - Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item))) -} - -#[inline] pub fn map_south_west( - item_offset: O::Unit, - item_height: O::Unit, - item: impl Content -) -> impl Content { - Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item)))) -} - -#[inline] pub fn map_east( - item_offset: O::Unit, - item_width: O::Unit, - item: impl Content -) -> impl Content { - Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item)))) -} - -#[cfg(test)] mod test { - use proptest::{prelude::*, option::of}; - use proptest_derive::Arbitrary; - use crate::*; - use Direction::*; - - proptest! { - #[test] fn proptest_direction ( - d in prop_oneof![ - Just(North), Just(South), - Just(East), Just(West), - Just(Above), Just(Below) - ], - x in u16::MIN..u16::MAX, - y in u16::MIN..u16::MAX, - w in u16::MIN..u16::MAX, - h in u16::MIN..u16::MAX, - a in u16::MIN..u16::MAX, - ) { - let _ = d.split_fixed(XYWH(x, y, w, h), a); - } - } - - proptest! { - #[test] fn proptest_area ( - x in u16::MIN..u16::MAX, - y in u16::MIN..u16::MAX, - w in u16::MIN..u16::MAX, - h in u16::MIN..u16::MAX, - a in u16::MIN..u16::MAX, - b in u16::MIN..u16::MAX, - ) { - let _: XYWH = XYWH::zero(); - //let _: XYWH = XYWH::from_position([a, b]); - //let _: XYWH = XYWH::from_size([a, b]); - let area: XYWH = XYWH(x, y, w, h); - //let _ = area.expect_min(a, b); - let _ = area.xy(); - let _ = area.wh(); - //let _ = area.xywh(); - let _ = area.clipped_h(a); - let _ = area.clipped_w(b); - let _ = area.clipped(WH(a, b)); - //let _ = area.set_w(a); - //let _ = area.set_h(b); - let _ = area.x2(); - let _ = area.y2(); - let _ = area.lrtb(); - let _ = area.center(); - let _ = area.centered(); - let _ = area.centered_x(a); - let _ = area.centered_y(b); - let _ = area.centered_xy([a, b]); - } - } - - proptest! { - #[test] fn proptest_size ( - x in u16::MIN..u16::MAX, - y in u16::MIN..u16::MAX, - a in u16::MIN..u16::MAX, - b in u16::MIN..u16::MAX, - ) { - let size = WH(x, y); - let _ = size.w(); - let _ = size.h(); - let _ = size.wh(); - let _ = size.clip_w(a); - let _ = size.clip_h(b); - //let _ = size.expect_min(a, b); - //let _ = size.to_area_pos(); - //let _ = size.to_area_size(); - } - } - - macro_rules! test_op_transform { - ($fn:ident, $Op:ident) => { - proptest! { - #[test] fn $fn ( - op_x in of(u16::MIN..u16::MAX), - op_y in of(u16::MIN..u16::MAX), - content in "\\PC*", - x in u16::MIN..u16::MAX, - y in u16::MIN..u16::MAX, - w in u16::MIN..u16::MAX, - h in u16::MIN..u16::MAX, - ) { - if let Some(op) = match (op_x, op_y) { - (Some(x), Some(y)) => Some($Op::XY(x, y, content)), - (Some(x), None) => Some($Op::X(x, content)), - (None, Some(y)) => Some($Op::Y(y, content)), - _ => None - } { - //assert_eq!(Content::layout(&op, [x, y, w, h]), - //Draw::layout(&op, [x, y, w, h])); - } - } - } - } - } - test_op_transform!(proptest_op_fixed, Fixed); - test_op_transform!(proptest_op_min, Min); - test_op_transform!(proptest_op_max, Max); - test_op_transform!(proptest_op_push, Push); - test_op_transform!(proptest_op_pull, Pull); - test_op_transform!(proptest_op_shrink, Shrink); - test_op_transform!(proptest_op_expand, Expand); - test_op_transform!(proptest_op_padding, Pad); - - proptest! { - #[test] fn proptest_op_bsp ( - d in prop_oneof![ - Just(North), Just(South), - Just(East), Just(West), - Just(Above), Just(Below) - ], - a in "\\PC*", - b in "\\PC*", - x in u16::MIN..u16::MAX, - y in u16::MIN..u16::MAX, - w in u16::MIN..u16::MAX, - h in u16::MIN..u16::MAX, - ) { - let bsp = Bsp(d, a, b); - //assert_eq!( - //Content::layout(&bsp, [x, y, w, h]), - //Draw::layout(&bsp, [x, y, w, h]), - //); - } - } - - //#[test] fn test_tui_engine () -> Usually<()> { - ////use std::sync::{Arc, RwLock}; - //struct TestComponent(String); - //impl Draw for TestComponent { - //fn draw (&self, _to: &mut TuiOut) {} - //} - //impl Handle for TestComponent { - //fn handle (&mut self, _from: &TuiIn) -> Perhaps { Ok(None) } - //} - //let engine = Tui::new(Box::<&mut Vec>::new(vec![0u8;0].as_mut()))?; - //let state = engine.run(false, &Arc::new(RwLock::new(TestComponent("hello world".into()))))?; - //state.read().unwrap().exited.store(true, std::sync::atomic::Ordering::Relaxed); - //Ok(()) - //} -} +#[cfg(feature = "sing")] pub extern crate jack; +#[cfg(feature = "sing")] pub mod sing; +#[cfg(feature = "sing")] pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; diff --git a/src/tengri_fns.rs b/src/tengri_fns.rs new file mode 100644 index 0000000..7a4e6d4 --- /dev/null +++ b/src/tengri_fns.rs @@ -0,0 +1,182 @@ +use crate::*; + +/// Interpret layout operation. +/// +/// ``` +/// # use tengri::*; +/// struct Target { xywh: XYWH /*platform-specific*/} +/// impl tengri::Out for Target { +/// type Unit = u16; +/// fn area (&self) -> XYWH { self.xywh } +/// fn area_mut (&mut self) -> &mut XYWH { &mut self.xywh } +/// fn show <'t, T: Draw + ?Sized> (&mut self, area: XYWH, content: &'t T) {} +/// } +/// +/// struct State {/*app-specific*/} +/// impl<'b> Namespace<'b, bool> for State {} +/// impl<'b> Namespace<'b, u16> for State {} +/// impl Understand for State {} +/// +/// # fn main () -> tengri::Usually<()> { +/// let state = State {}; +/// let mut target = Target { xywh: Default::default() }; +/// eval_view(&state, &mut target, &"")?; +/// eval_view(&state, &mut target, &"(when true (text hello))")?; +/// eval_view(&state, &mut target, &"(either true (text hello) (text world))")?; +/// // TODO test all +/// # Ok(()) } +/// ``` +#[cfg(feature = "dsl")] pub fn eval_view <'a, O: Screen + 'a, S> ( + state: &S, output: &mut O, expr: &'a impl Expression +) -> Usually where + S: Understand + + for<'b>Namespace<'b, bool> + + for<'b>Namespace<'b, O::Unit> +{ + // First element of expression is used for dispatch. + // Dispatch is proto-namespaced using separator character + let head = expr.head()?; + let mut frags = head.src()?.unwrap_or_default().split("/"); + // The rest of the tokens in the expr are arguments. + // Their meanings depend on the dispatched operation + let args = expr.tail(); + let arg0 = args.head(); + let tail0 = args.tail(); + let arg1 = tail0.head(); + let tail1 = tail0.tail(); + let arg2 = tail1.head(); + // And we also have to do the above binding dance + // so that the Perhapss remain in scope. + match frags.next() { + + Some("when") => output.place(&When::new( + state.namespace(arg0?)?.unwrap(), + Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()) + )), + + Some("either") => output.place(&Either::new( + state.namespace(arg0?)?.unwrap(), + Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()), + Thunk::new(move|output: &mut O|state.understand(output, &arg2).unwrap()) + )), + + Some("bsp") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); + let b = Thunk::new(move|output: &mut O|state.understand(output, &arg1).unwrap()); + match frags.next() { + Some("n") => Bsp::n(a, b), + Some("s") => Bsp::s(a, b), + Some("e") => Bsp::e(a, b), + Some("w") => Bsp::w(a, b), + Some("a") => Bsp::a(a, b), + Some("b") => Bsp::b(a, b), + frag => unimplemented!("bsp/{frag:?}") + } + }), + + Some("align") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); + match frags.next() { + Some("n") => Align::n(a), + Some("s") => Align::s(a), + Some("e") => Align::e(a), + Some("w") => Align::w(a), + Some("x") => Align::x(a), + Some("y") => Align::y(a), + Some("c") => Align::c(a), + frag => unimplemented!("align/{frag:?}") + } + }), + + Some("fill") => output.place(&{ + let a = Thunk::new(move|output: &mut O|state.understand(output, &arg0).unwrap()); + match frags.next() { + Some("xy") | None => Fill::XY(a), + Some("x") => Fill::X(a), + Some("y") => Fill::Y(a), + frag => unimplemented!("fill/{frag:?}") + } + }), + + Some("fixed") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Fixed::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Fixed::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Fixed::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("fixed/{frag:?} ({expr:?}) ({head:?}) ({:?})", + head.src()?.unwrap_or_default().split("/").next()) + } + }), + + Some("min") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Min::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Min::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Min::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("min/{frag:?}") + } + }), + + Some("max") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Max::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Max::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Max::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("max/{frag:?}") + } + }), + + Some("push") => output.place(&{ + let axis = frags.next(); + let arg = match axis { Some("x") | Some("y") => arg1, Some("xy") | None => arg2, _ => panic!("fixed: unsupported axis {axis:?}") }; + let cb = Thunk::new(move|output: &mut O|state.understand(output, &arg).unwrap()); + match axis { + Some("xy") | None => Push::XY(state.namespace(arg0?)?.unwrap(), state.namespace(arg1?)?.unwrap(), cb), + Some("x") => Push::X(state.namespace(arg0?)?.unwrap(), cb), + Some("y") => Push::Y(state.namespace(arg0?)?.unwrap(), cb), + frag => unimplemented!("push/{frag:?}") + } + }), + + _ => return Ok(false) + + }; + Ok(true) +} + +/// Trim string with [unicode_width]. +pub fn trim_string (max_width: usize, input: impl AsRef) -> 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() +} + +pub(crate) fn width_chars_max (max: u16, text: impl AsRef) -> 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 +} diff --git a/src/tengri_impl.rs b/src/tengri_impl.rs index 0a865c2..476c657 100644 --- a/src/tengri_impl.rs +++ b/src/tengri_impl.rs @@ -1,65 +1,9 @@ use crate::*; -use Alignment::*; use Direction::*; use rand::{thread_rng, distributions::uniform::UniformSampler}; -impl + Layout> Content for T {} -impl<'a, O: Out> AsRef + 'a> for dyn Content + 'a { - fn as_ref (&self) -> &(dyn Draw + 'a) { self } -} -impl<'a, O: Out> AsRef + 'a> for dyn Content + 'a { - fn as_ref (&self) -> &(dyn Layout + 'a) { self } -} -impl Draw for () { - fn draw (&self, _: &mut O) {} -} -impl Draw for fn(&mut O) { - fn draw (&self, to: &mut O) { (*self)(to) } -} -impl Draw for Box> { - fn draw (&self, to: &mut O) { (**self).draw(to) } -} -impl> Draw for &D { - fn draw (&self, to: &mut O) { (*self).draw(to) } -} -impl> Draw for &mut D { - fn draw (&self, to: &mut O) { (**self).draw(to) } -} -impl> Draw for Arc { - fn draw (&self, to: &mut O) { (**self).draw(to) } -} -impl> Draw for RwLock { - fn draw (&self, to: &mut O) { self.read().unwrap().draw(to) } -} -impl> Draw for Option { - fn draw (&self, to: &mut O) { if let Some(draw) = self { draw.draw(to) } } -} impl<'a, T: AsRef> TrimString { fn as_ref (&self) -> TrimStringRef<'_, T> { TrimStringRef(self.0, &self.1) } } -impl> ErrorBoundary { - pub fn new (content: Perhaps) -> Self { Self(content, Default::default()) } -} -impl, V: Content> HasContent for FieldH { - fn content (&self) -> impl Content { Bsp::e(&self.1, &self.2) } -} -impl, V: Content> Layout for FieldH { - fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } -} -impl, V: Content> Draw for FieldH { - fn draw (&self, to: &mut O) { self.content().draw(to) } -} -impl, V: Content> HasContent for FieldV { - fn content (&self) -> impl Content { Bsp::s(&self.1, &self.2) } -} -impl, V: Content> Layout for FieldV { - fn layout (&self, to: XYWH) -> XYWH { self.content().layout(to) } -} -impl, V: Content> Draw for FieldV { - fn draw (&self, to: &mut O) { self.content().draw(to) } -} -impl> Layout for Border { - fn layout (&self, area: XYWH) -> XYWH { self.1.layout(area) } -} impl Field { pub fn new (direction: Direction) -> Field { Field:: { @@ -79,32 +23,9 @@ impl Field { Field:: { value, value_fg: fg, value_bg: bg, value_align: align, ..self } } } -impl> Command for Option { - fn execute (&self, _: &mut S) -> Perhaps { - Ok(None) - } - fn delegate (&self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps - where Self: Sized - { - Ok(None) - } -} -impl Layout for Measure {} -impl>> Measured for T { - fn measure (&self) -> &Measure { self.as_ref() } -} -impl Clone for Measure { - fn clone (&self) -> Self { - Self { __: Default::default(), x: self.x.clone(), y: self.y.clone(), } - } -} -impl Thunk { - pub const fn new (draw: F) -> Self { Self(draw, PhantomData) } -} -impl Draw for Thunk { - fn draw (&self, to: &mut O) { (self.0)(to) } -} -impl Layout for Thunk {} +impl> Action for Option { fn action (&self, _: &mut S) -> Perhaps { Ok(None) } } +impl Clone for Measure { fn clone (&self) -> Self { Self { __: Default::default(), x: self.x.clone(), y: self.y.clone(), } } } +impl Thunk { pub const fn new (draw: F) -> Self { Self(draw, PhantomData) } } impl Memo { pub fn new (value: T, view: U) -> Self { Self { value, view: Arc::new(view.into()) } @@ -118,37 +39,11 @@ impl Memo { None } } -impl Direction { - pub fn split_fixed (self, area: XYWH, a: N) -> (XYWH, XYWH) { - let XYWH(x, y, w, h) = area; - match self { - North => (XYWH(x, y.plus(h).minus(a), w, a), XYWH(x, y, w, h.minus(a))), - South => (XYWH(x, y, w, a), XYWH(x, y.plus(a), w, h.minus(a))), - East => (XYWH(x, y, a, h), XYWH(x.plus(a), y, w.minus(a), h)), - West => (XYWH(x.plus(w).minus(a), y, a, h), XYWH(x, y, w.minus(a), h)), - Above | Below => (area, area) - } - } -} -impl PartialEq for Measure { - fn eq (&self, other: &Self) -> bool { - self.x.load(Relaxed) == other.x.load(Relaxed) && - self.y.load(Relaxed) == other.y.load(Relaxed) - } -} -// TODO: 🡘 🡙 ←🡙→ indicator to expand window when too small -impl Draw for Measure { - fn draw (&self, to: &mut O) { - self.x.store(to.area().w().into(), Relaxed); - self.y.store(to.area().h().into(), Relaxed); - } -} -impl Debug for Measure { - fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - f.debug_struct("Measure").field("width", &self.x).field("height", &self.y).finish() - } -} -impl Measure { +impl PartialEq for Measure { fn eq (&self, other: &Self) -> bool { self.w() == other.w() && self.h() == other.h() } } +impl W for Measure { fn w (&self) -> O::Unit { (self.x.load(Relaxed) as u16).into() } } +impl H for Measure { fn h (&self) -> O::Unit { (self.y.load(Relaxed) as u16).into() } } +impl Debug for Measure { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { f.debug_struct("Measure").field("width", &self.x).field("height", &self.y).finish() } } +impl Measure { pub fn set_w (&self, w: impl Into) -> &Self { self.x.store(w.into(), Relaxed); self } pub fn set_h (&self, h: impl Into) -> &Self { self.y.store(h.into(), Relaxed); self } pub fn set_wh (&self, w: impl Into, h: impl Into) -> &Self { self.set_w(w); self.set_h(h); self } @@ -158,142 +53,7 @@ impl Measure { Self { __: PhantomData::default(), x: Arc::new(x.atomic()), y: Arc::new(y.atomic()), } } } -impl From> for Measure { - fn from (WH(x, y): WH) -> Self { Self::new(x, y) } -} -impl HasWH for Measure { /// FIXME don't convert to u16 specifically - fn w (&self) -> O::Unit { (self.x.load(Relaxed) as u16).into() } - fn h (&self) -> O::Unit { (self.y.load(Relaxed) as u16).into() } -} -impl Layout for () { - fn layout_x (&self, a: XYWH) -> O::Unit { a.x() } - fn layout_y (&self, a: XYWH) -> O::Unit { a.y() } - fn layout_w (&self, _: XYWH) -> O::Unit { 0.into() } - fn layout_w_min (&self, _: XYWH) -> O::Unit { 0.into() } - fn layout_w_max (&self, _: XYWH) -> O::Unit { 0.into() } - fn layout_h (&self, _: XYWH) -> O::Unit { 0.into() } - fn layout_h_min (&self, _: XYWH) -> O::Unit { 0.into() } - fn layout_h_max (&self, _: XYWH) -> O::Unit { 0.into() } - fn layout (&self, a: XYWH) -> XYWH { XYWH(a.x(), a.y(), 0.into(), 0.into()) } -} -impl> Layout for &L { - fn layout_x (&self, a: XYWH) -> O::Unit { (*self).layout_x(a) } - fn layout_y (&self, a: XYWH) -> O::Unit { (*self).layout_y(a) } - fn layout_w (&self, a: XYWH) -> O::Unit { (*self).layout_w(a) } - fn layout_w_min (&self, a: XYWH) -> O::Unit { (*self).layout_w_min(a) } - fn layout_w_max (&self, a: XYWH) -> O::Unit { (*self).layout_w_max(a) } - fn layout_h (&self, a: XYWH) -> O::Unit { (*self).layout_h(a) } - fn layout_h_min (&self, a: XYWH) -> O::Unit { (*self).layout_h_min(a) } - fn layout_h_max (&self, a: XYWH) -> O::Unit { (*self).layout_h_max(a) } - fn layout (&self, a: XYWH) -> XYWH { (*self).layout(a) } -} - -impl> Layout for &mut L { - fn layout_x (&self, a: XYWH) -> O::Unit { (**self).layout_x(a) } - fn layout_y (&self, a: XYWH) -> O::Unit { (**self).layout_y(a) } - fn layout_w (&self, a: XYWH) -> O::Unit { (**self).layout_w(a) } - fn layout_w_min (&self, a: XYWH) -> O::Unit { (**self).layout_w_min(a) } - fn layout_w_max (&self, a: XYWH) -> O::Unit { (**self).layout_w_max(a) } - fn layout_h (&self, a: XYWH) -> O::Unit { (**self).layout_h(a) } - fn layout_h_min (&self, a: XYWH) -> O::Unit { (**self).layout_h_min(a) } - fn layout_h_max (&self, a: XYWH) -> O::Unit { (**self).layout_h_max(a) } - fn layout (&self, a: XYWH) -> XYWH { (**self).layout(a) } -} - -impl> Layout for Arc { - fn layout_x (&self, a: XYWH) -> O::Unit { (**self).layout_x(a) } - fn layout_y (&self, a: XYWH) -> O::Unit { (**self).layout_y(a) } - fn layout_w (&self, a: XYWH) -> O::Unit { (**self).layout_w(a) } - fn layout_w_min (&self, a: XYWH) -> O::Unit { (**self).layout_w_min(a) } - fn layout_w_max (&self, a: XYWH) -> O::Unit { (**self).layout_w_max(a) } - fn layout_h (&self, a: XYWH) -> O::Unit { (**self).layout_h(a) } - fn layout_h_min (&self, a: XYWH) -> O::Unit { (**self).layout_h_min(a) } - fn layout_h_max (&self, a: XYWH) -> O::Unit { (**self).layout_h_max(a) } - fn layout (&self, a: XYWH) -> XYWH { (**self).layout(a) } -} -impl Layout for Box> { - fn layout_x (&self, a: XYWH) -> O::Unit { (**self).layout_x(a) } - fn layout_y (&self, a: XYWH) -> O::Unit { (**self).layout_y(a) } - fn layout_w (&self, a: XYWH) -> O::Unit { (**self).layout_w(a) } - fn layout_w_min (&self, a: XYWH) -> O::Unit { (**self).layout_w_min(a) } - fn layout_w_max (&self, a: XYWH) -> O::Unit { (**self).layout_w_max(a) } - fn layout_h (&self, a: XYWH) -> O::Unit { (**self).layout_h(a) } - fn layout_h_min (&self, a: XYWH) -> O::Unit { (**self).layout_h_min(a) } - fn layout_h_max (&self, a: XYWH) -> O::Unit { (**self).layout_h_max(a) } - fn layout (&self, a: XYWH) -> XYWH { (**self).layout(a) } -} -impl> Layout for RwLock { - fn layout_x (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_x(a) } - fn layout_y (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_y(a) } - fn layout_w (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_w(a) } - fn layout_w_min (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_w_min(a) } - fn layout_w_max (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_w_max(a) } - fn layout_h (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_h(a) } - fn layout_h_min (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_h_min(a) } - fn layout_h_max (&self, a: XYWH) -> O::Unit { self.read().unwrap().layout_h_max(a) } - fn layout (&self, a: XYWH) -> XYWH { self.read().unwrap().layout(a) } -} -impl> Layout for Option { - fn layout_x (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_x(to)).unwrap_or(to.x()) } - fn layout_y (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_y(to)).unwrap_or(to.y()) } - fn layout_w_min (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_w_min(to)).unwrap_or(0.into()) } - fn layout_w_max (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_w_max(to)).unwrap_or(0.into()) } - fn layout_w (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_w(to)).unwrap_or(0.into()) } - fn layout_h_min (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_h_min(to)).unwrap_or(0.into()) } - fn layout_h_max (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_h_max(to)).unwrap_or(0.into()) } - fn layout_h (&self, to: XYWH) -> O::Unit { self.as_ref().map(|c|c.layout_h(to)).unwrap_or(0.into()) } - fn layout (&self, to: XYWH) -> XYWH { - let xywh = XYWH(self.layout_x(to), self.layout_y(to), self.layout_w(to), self.layout_h(to)); - self.as_ref().map(|c|c.layout(xywh)).unwrap_or(XYWH(to.x(), to.y(), 0.into(), 0.into())) - } -} -impl> HasContent for Bounded { - fn content (&self) -> impl Content { &self.1 } -} -impl> Draw for Bounded { - fn draw (&self, to: &mut O) { - let area = to.area(); - *to.area_mut() = self.0; - self.1.draw(to); - *to.area_mut() = area; - } -} -impl> When { - /// Create a binary condition. - pub const fn new (c: bool, a: T) -> Self { Self(c, a, PhantomData) } -} -impl> Layout for When { - fn layout (&self, to: XYWH) -> XYWH { - let Self(cond, item, ..) = self; - if *cond { item.layout(to) } else { XYWH::::zero().into() } - } -} -impl> Draw for When { - fn draw (&self, to: &mut O) { - let Self(cond, item, ..) = self; - if *cond { Bounded(self.layout(to.area()), item).draw(to) } - } -} -impl, B: Content> Either { - /// Create a ternary view condition. - pub const fn new (c: bool, a: A, b: B) -> Self { - Self(c, a, b, PhantomData) - } -} -impl, B: Layout> Layout for Either { - fn layout (&self, to: XYWH) -> XYWH { - let Self(cond, a, b, ..) = self; - if *cond { a.layout(to) } else { b.layout(to) } - } -} -impl, B: Content> Draw for Either { - fn draw (&self, to: &mut E) { - let Self(cond, a, b, ..) = self; - let area = self.layout(to.area()); - if *cond { Bounded(area, a).draw(to) } else { Bounded(area, b).draw(to) } - } -} - +impl From> for Measure { fn from (WH(x, y): WH) -> Self { Self::new(x, y) } } macro_rules! layout_op_xy ( // Variant for layout ops that take no coordinates (0: $T: ident) => { @@ -302,7 +62,7 @@ macro_rules! layout_op_xy ( match self { Self::X(c) | Self::Y(c) | Self::XY(c) => c } } } - impl> Draw for $T { + impl> Draw for $T { fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } } }; @@ -313,7 +73,7 @@ macro_rules! layout_op_xy ( match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, } } } - impl> Draw for $T { + impl> Draw for $T { fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } } impl $T { @@ -331,7 +91,7 @@ macro_rules! layout_op_xy ( match self { Self::X(_, c) | Self::Y(_, c) | Self::XY(_, _, c) => c, } } } - impl> Draw for $T { + impl> Draw for $T { fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } } impl $T { @@ -347,7 +107,7 @@ macro_rules! layout_op_xy ( // Implement layout op that increments X and/or Y by fixed amount. macro_rules! push_pull(($T:ident: $method: ident)=>{ layout_op_xy!(1: $T); - impl> Layout for $T { + impl> Layout for $T { fn layout_x (&self, area: XYWH) -> O::Unit { area.x().$method(self.dx()) } fn layout_y (&self, area: XYWH) -> O::Unit { area.y().$method(self.dy()) } } @@ -357,7 +117,7 @@ push_pull!(Pull: minus); layout_op_xy!(0: Fill); -impl> Layout for Fill { +impl> Layout for Fill { fn layout_x (&self, area: XYWH) -> O::Unit { if self.dx() { area.x() } else { self.inner().layout_x(area) } } fn layout_y (&self, area: XYWH) -> O::Unit { if self.dy() { area.y() } else { self.inner().layout_y(area) } } fn layout_w (&self, area: XYWH) -> O::Unit { if self.dx() { area.w() } else { self.inner().layout_w(area) } } @@ -375,7 +135,7 @@ impl Fill { layout_op_xy!(1 opt: Fixed); -impl> Layout for Fixed { +impl> Layout for Fixed { fn layout_w (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w(area)) } fn layout_w_min (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w_min(area)) } fn layout_w_max (&self, area: XYWH) -> O::Unit { self.dx().unwrap_or(self.inner().layout_w_max(area)) } @@ -386,7 +146,7 @@ impl> Layout for Fixed { layout_op_xy!(1 opt: Max); -impl> Layout for Max { +impl> Layout for Max { fn layout (&self, area: XYWH) -> XYWH { let XYWH(x, y, w, h) = self.inner().layout(area); match self { @@ -399,7 +159,7 @@ impl> Layout for Max { layout_op_xy!(1 opt: Min); -impl> Layout for Min { +impl> Layout for Min { fn layout (&self, area: XYWH) -> XYWH { let XYWH(x, y, w, h) = self.inner().layout(area); match self { @@ -412,7 +172,7 @@ impl> Layout for Min { layout_op_xy!(1 opt: Expand); -impl> Layout for Expand { +impl> Layout for Expand { fn layout_w (&self, to: XYWH) -> O::Unit { self.inner().layout_w(to).plus(self.dx().unwrap_or_default()) } @@ -425,7 +185,7 @@ impl> Layout for Expand { layout_op_xy!(1 opt: Shrink); -impl> Layout for Shrink { +impl> Layout for Shrink { fn layout (&self, to: XYWH) -> XYWH { let area = self.inner().layout(to); let dx = self.dx().unwrap_or_default(); @@ -448,12 +208,9 @@ impl Align { #[inline] pub const fn se (a: T) -> Self { Self(Alignment::SE, a) } } -impl> Draw for Align { - fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), &self.1).draw(to) } -} - -impl> Layout for Align { +impl> Layout for Align { fn layout_x (&self, to: XYWH) -> O::Unit { + use Alignment::*; match self.0 { NW | W | SW => to.x(), N | Center | S => to.x().plus(to.w() / 2.into()).minus(self.1.layout_w(to) / 2.into()), @@ -462,6 +219,7 @@ impl> Layout for Align { } } fn layout_y (&self, to: XYWH) -> O::Unit { + use Alignment::*; match self.0 { NW | N | NE => to.y(), W | Center | E => to.y().plus(to.h() / 2.into()).minus(self.1.layout_h(to) / 2.into()), @@ -489,11 +247,7 @@ impl Pad { } } -impl> Draw for Pad { - fn draw (&self, to: &mut O) { Bounded(self.layout(to.area()), self.inner()).draw(to) } -} - -impl> Layout for Pad { +impl> Layout for Pad { fn layout_x (&self, area: XYWH) -> O::Unit { area.x().plus(self.dx()) } fn layout_y (&self, area: XYWH) -> O::Unit { area.x().plus(self.dx()) } fn layout_w (&self, area: XYWH) -> O::Unit { area.w().minus(self.dx() * 2.into()) } @@ -509,21 +263,7 @@ impl Bsp { #[inline] pub const fn b (a: Head, b: Tail) -> Self { Self(Below, a, b) } } -impl, Tail: Content> Draw for Bsp { - fn draw (&self, to: &mut O) { - let [a, b, _] = bsp_areas(to.area(), self.0, &self.1, &self.2); - //panic!("{a:?} {b:?}"); - if self.0 == Below { - to.place_at(a, &self.1); - to.place_at(b, &self.2); - } else { - to.place_at(b, &self.2); - to.place_at(a, &self.1); - } - } -} - -impl, Tail: Layout> Layout for Bsp { +impl, Tail: Layout> Layout for Bsp { fn layout_w (&self, area: XYWH) -> O::Unit { match self.0 { Above | Below | North | South => self.1.layout_w(area).max(self.2.layout_w(area)), @@ -565,56 +305,6 @@ impl, Tail: Layout> Layout for Bsp { } } -fn bsp_areas , B: Layout> ( - area: XYWH, - direction: Direction, - a: &A, - b: &B, -) -> [XYWH;3] { - let XYWH(x, y, w, h) = area; - let WH(aw, ah) = a.layout(area).wh(); - let WH(bw, bh) = b.layout(match direction { - South => XYWH(x, y + ah, w, h.minus(ah)), - North => XYWH(x, y, w, h.minus(ah)), - East => XYWH(x + aw, y, w.minus(aw), h), - West => XYWH(x, y, w.minus(aw), h), - Above => area, - Below => area, - }).wh(); - match direction { - Above | Below => { - let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah.max(bh)]); - let a = XYWH((x + w/2.into()).minus(aw/2.into()), (y + h/2.into()).minus(ah/2.into()), aw, ah); - let b = XYWH((x + w/2.into()).minus(bw/2.into()), (y + h/2.into()).minus(bh/2.into()), bw, bh); - [a.into(), b.into(), XYWH(x, y, w, h)] - }, - South => { - let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); - let a = XYWH((x + w/2.into()).minus(aw/2.into()), y, aw, ah); - let b = XYWH((x + w/2.into()).minus(bw/2.into()), y + ah, bw, bh); - [a.into(), b.into(), XYWH(x, y, w, h)] - }, - North => { - let XYWH(x, y, w, h) = area.centered_xy([aw.max(bw), ah + bh]); - let a = XYWH((x + (w/2.into())).minus(aw/2.into()), y + bh, aw, ah); - let b = XYWH((x + (w/2.into())).minus(bw/2.into()), y, bw, bh); - [a.into(), b.into(), XYWH(x, y, w, h)] - }, - East => { - let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); - let a = XYWH(x, (y + h/2.into()).minus(ah/2.into()), aw, ah); - let b = XYWH(x + aw, (y + h/2.into()).minus(bh/2.into()), bw, bh); - [a.into(), b.into(), XYWH(x, y, w, h)] - }, - West => { - let XYWH(x, y, w, h) = area.centered_xy([aw + bw, ah.max(bh)]); - let a = XYWH(x + bw, (y + h/2.into()).minus(ah/2.into()), aw, ah); - let b = XYWH(x, (y + h/2.into()).minus(bh/2.into()), bw, bh); - [a.into(), b.into(), XYWH(x, y, w, h)] - }, - } -} - impl<'a, O, A, B, I, F, G> Map where I: Iterator + Send + Sync + 'a, F: Fn() -> I + Send + Sync + 'a, @@ -626,13 +316,28 @@ impl<'a, O, A, B, I, F, G> Map where get_item } } + const fn to_south ( + item_offset: S::Unit, item_height: S::Unit, item: impl Draw + ) -> impl Draw { + Push::Y(item_offset, Fixed::Y(item_height, Fill::X(item))) + } + const fn to_south_west ( + item_offset: S::Unit, item_height: S::Unit, item: impl Draw + ) -> impl Draw { + Push::Y(item_offset, Align::nw(Fixed::Y(item_height, Fill::X(item)))) + } + const fn to_east ( + item_offset: S::Unit, item_width: S::Unit, item: impl Draw + ) -> impl Draw { + Push::X(item_offset, Align::w(Fixed::X(item_width, Fill::Y(item)))) + } } macro_rules! impl_map_direction (($name:ident, $axis:ident, $align:ident)=>{ impl<'a, O, A, B, I, F> Map< O, A, Push>>>, I, F, fn(A, usize)->B > where - O: Out, + O: Screen, B: Draw, I: Iterator + Send + Sync + 'a, F: Fn() -> I + Send + Sync + 'a @@ -668,7 +373,7 @@ impl_map_direction!(west, X, e); impl_map_direction!(north, Y, s); impl<'a, O, A, B, I, F, G> Layout for Map where - O: Out, + O: Screen, B: Layout, I: Iterator + Send + Sync + 'a, F: Fn() -> I + Send + Sync + 'a, @@ -695,8 +400,8 @@ impl<'a, O, A, B, I, F, G> Layout for Map where } impl<'a, O, A, B, I, F, G> Draw for Map where - O: Out, - B: Content, + O: Screen, + B: Draw, I: Iterator + Send + Sync + 'a, F: Fn() -> I + Send + Sync + 'a, G: Fn(A, usize)->B + Send + Sync @@ -707,8 +412,8 @@ impl<'a, O, A, B, I, F, G> Draw for Map where let area = self.layout(to.area()); for item in get_iter() { let item = get_item(item, index); - //to.place_at(area.into(), &item); - to.place_at(item.layout(area), &item); + //to.show(area.into(), &item); + to.show(item.layout(area), &item); index += 1; } } @@ -760,34 +465,19 @@ impl PerfModel { mod xywh { use crate::*; - impl HasXY for XY { - fn x (&self) -> N { self.0 } - fn y (&self) -> N { self.1 } - } - impl HasXY for XYWH { - fn x (&self) -> N { self.0 } - fn y (&self) -> N { self.1 } - } - impl HasXY for O { - // X coordinate of output area - #[inline] fn x (&self) -> O::Unit { self.area().x() } - // Y coordinate of output area - #[inline] fn y (&self) -> O::Unit { self.area().y() } - } - impl HasWH for WH { - fn w (&self) -> N { self.0 } - fn h (&self) -> N { self.1 } - } - impl HasWH for XYWH { - fn w (&self) -> N { self.2 } - fn h (&self) -> N { self.3 } - } - impl HasWH for O { - // Width of output area - #[inline] fn w (&self) -> O::Unit { self.area().w() } - // Height of output area - #[inline] fn h (&self) -> O::Unit { self.area().h() } - } + impl X for XY { fn x (&self) -> N { self.0 } } + impl Y for XY { fn y (&self) -> N { self.1 } } + impl W for WH { fn w (&self) -> N { self.0 } } + impl H for WH { fn h (&self) -> N { self.1 } } + impl X for XYWH { fn x (&self) -> N { self.0 } } + impl Y for XYWH { fn y (&self) -> N { self.1 } } + impl W for XYWH { fn w (&self) -> N { self.2 } } + impl H for XYWH { fn h (&self) -> N { self.3 } } + impl X for O { fn x (&self) -> O::Unit { self.area().x() } } + impl Y for O { fn y (&self) -> O::Unit { self.area().y() } } + impl W for O { fn w (&self) -> O::Unit { self.area().w() } } + impl H for O { fn h (&self) -> O::Unit { self.area().h() } } + impl WH { pub fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w), self.h()] } pub fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h)] } @@ -857,6 +547,48 @@ mod xywh { } } +impl Thread { + /// Spawn a TUI thread that runs `callt least one, then repeats until `exit`. + pub fn new (exit: Arc, call: F) -> Result + where F: Fn(&PerfModel)->() + Send + Sync + 'static + { + let perf = Arc::new(PerfModel::default()); + Ok(Self { + exit: exit.clone(), + perf: perf.clone(), + join: std::thread::Builder::new().name("tengri tui output".into()).spawn(move || { + while !exit.fetch_and(true, Relaxed) { + let _ = perf.cycle(&call); + } + })?.into() + }) + } + + /// Spawn a TUI thread that runs `callt least one, then repeats + /// until `exit`, sleeping for `time` msec after every iteration. + pub fn new_sleep ( + exit: Arc, time: Duration, call: F + ) -> Result + where F: Fn(&PerfModel)->() + Send + Sync + 'static + { + Self::new(exit, move |perf| { let _ = call(perf); std::thread::sleep(time); }) + } + + /// Spawn a TUI thread that runs `callt least one, then repeats + /// until `exit`, using polling to run every `time` msec. + pub fn new_poll ( + exit: Arc, time: Duration, call: F + ) -> Result + where F: Fn(&PerfModel)->() + Send + Sync + 'static + { + Self::new(exit, move |perf| { if poll(time).is_ok() { let _ = call(perf); } }) + } + + pub fn join (self) -> Result<(), Box> { + self.join.join() + } +} + #[cfg(feature = "tui")] pub use self::tui_impls::*; #[cfg(feature = "tui")] mod tui_impls { use crate::*; @@ -869,6 +601,12 @@ mod xywh { impl_from!(ItemColor: |rgb: Color| Self { rgb, okhsl: rgb_to_okhsl(rgb) }); impl_from!(ItemColor: |okhsl: Okhsl| Self { okhsl, rgb: okhsl_to_rgb(okhsl) }); impl_debug!(BigBuffer |self, f| { write!(f, "[BB {}x{} ({})]", self.width, self.height, self.content.len()) }); + impl Screen for Buffer { type Unit = u16; } + impl TuiOut for Buffer { fn tui_out (&mut self) -> &mut Buffer { self } } + impl TuiOut for Buffer { fn tui_out (&mut self) -> &mut Buffer { self } } + + //impl> TuiOut for T { fn tui_out (&mut self) -> &mut Buffer { self.as_mut() } } + impl Tui { /// True if done pub fn exited (&self) -> bool { self.exited.fetch_and(true, Relaxed) } @@ -877,11 +615,7 @@ mod xywh { /// Clean up after run pub fn teardown (&self) -> Usually<()> { tui_teardown(&mut*self.backend.write().unwrap()) } /// Apply changes to the display buffer. - pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { - tui_resized(&mut*self.backend.write().unwrap(), &mut*self.buffer.write().unwrap(), size); - tui_redrawn(&mut*self.backend.write().unwrap(), &mut*self.buffer.write().unwrap(), &mut buffer); - buffer - } + pub fn flip (&mut self, mut buffer: Buffer, size: ratatui::prelude::Rect) -> Buffer { tui_flip(self, self.buffer, buffer, size) } /// Create the engine. pub fn new (output: Box) -> Usually { let backend = CrosstermBackend::new(output); @@ -896,15 +630,17 @@ mod xywh { error: None, }) } - /// Run an amm in the engine. + /// Run an app in the engine. pub fn run (mut self, join: bool, state: &Arc>) -> Usually> where - T: Handle + Draw + Send + Sync + 'static + T: Act + Draw + Send + Sync + 'static { self.setup()?; - let tui = Arc::new(self); - let _input_thread = tui_input(&tui, state, Duration::from_millis(100))?; - let render_thread = tui_output(&tui, state, Duration::from_millis(10))?; - if join { + let tui = Arc::new(self); + let input_poll = Duration::from_millis(100); + let output_sleep = Duration::from_millis(10); // == 1/MAXFPS (?) + let _input_thread = tui_input(&tui, state, input_poll)?; + let render_thread = tui_output(&tui, state, output_sleep)?; + if join { // Run until render thread ends: let result = render_thread.join(); tui.teardown()?; match result { @@ -916,77 +652,14 @@ mod xywh { } } - /// Spawn the input thread. - pub fn tui_input + Send + Sync + 'static> ( - engine: &Arc, state: &Arc>, poll: Duration - ) -> Result { - let state = state.clone(); - let engine = engine.clone(); - TuiThread::new_poll(engine.exited.clone(), poll, move |_| { - let event = read().unwrap(); - match event { - Event::Key(KeyEvent { - modifiers: KeyModifiers::CONTROL, - code: KeyCode::Char('c'), - kind: KeyEventKind::Press, - state: KeyEventState::NONE - }) => { - engine.exited.store(true, Relaxed); - }, - _ => { - let event = TuiEvent::from_crossterm(event); - if let Err(e) = state.write().unwrap().handle(&engine) { - panic!("{e}") - } - } - } - }) - } - /// Spawn the output thread. - pub fn tui_output + Send + Sync + 'static> ( - engine: &Arc, state: &Arc>, sleep: Duration - ) -> Result { - let state = state.clone(); - let mut engine = engine.clone(); - let WH(width, height) = tui_wh(&engine); - let mut buffer = Buffer::empty(Rect { x: 0, y: 0, width, height }); - TuiThread::new_sleep(engine.exited.clone(), sleep, move |perf| { - let WH(width, height) = tui_wh(&engine); - if let Ok(state) = state.try_read() { - let size = Rect { x: 0, y: 0, width, height }; - if buffer.area != size { - engine.backend.write().unwrap() - .clear_region(ClearType::All).expect("pre-frame clear region failed"); - buffer.resize(size); - buffer.reset(); - } - state.draw(&engine); - buffer = engine.flip(*engine.buffer.write().unwrap(), size); - } - let timer = format!("{:>3.3}ms", perf.used.load(Relaxed)); - buffer.set_string(0, 0, &timer, Style::default()); - }) - } - - fn tui_wh (engine: &Tui) -> WH { - let Size { width, height } = engine.backend.read().unwrap().size().expect("get size failed"); - WH(width, height) - } - - impl Input for Tui { - type Event = TuiEvent; - type Handled = bool; - fn event (&self) -> &TuiEvent { - self.event.as_ref().expect("input.event called outside of input handler") + impl TuiEvent { + pub fn from_crossterm (event: Event) -> Self { Self(event) } + #[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Perhaps { + Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self)) } } - impl Done for Tui { - fn done (&self) { self.exited.store(true, Relaxed); } - fn is_done (&self) -> bool { self.exited.fetch_and(true, Relaxed) } - } - impl Ord for TuiEvent { fn cmp (&self, other: &Self) -> std::cmp::Ordering { self.partial_cmp(other) @@ -994,13 +667,6 @@ mod xywh { } } - impl TuiEvent { - pub fn from_crossterm (event: Event) -> Self { Self(event) } - #[cfg(feature = "dsl")] pub fn from_dsl (dsl: impl Language) -> Perhaps { - Ok(TuiKey::from_dsl(dsl)?.to_crossterm().map(Self)) - } - } - impl From for TuiEvent { fn from (e: Event) -> Self { Self(e) @@ -1061,86 +727,6 @@ mod xywh { } } - impl Out for Tui { - type Unit = u16; - #[inline] fn area (&self) -> XYWH { self.area } - #[inline] fn area_mut (&mut self) -> &mut XYWH { &mut self.area } - #[inline] fn place_at <'t, T: Draw + ?Sized> (&mut self, area: XYWH, content: &'t T) { - let last = self.area(); - *self.area_mut() = area; - content.draw(self); - *self.area_mut() = last; - } - } - - impl Tui { - #[inline] pub fn with_rect (&mut self, area: XYWH) -> &mut Self { self.area = area; self } - pub fn update (&mut self, area: XYWH, callback: &impl Fn(&mut Cell, u16, u16)) { - tui_update(&mut*self.buffer.write().unwrap(), area, callback); - } - pub fn fill_char (&mut self, area: XYWH, c: char) { self.update(area, &|cell,_,_|{cell.set_char(c);}) } - pub fn fill_bg (&mut self, area: XYWH, color: Color) { self.update(area, &|cell,_,_|{cell.set_bg(color);}) } - pub fn fill_fg (&mut self, area: XYWH, color: Color) { self.update(area, &|cell,_,_|{cell.set_fg(color);}) } - pub fn fill_mod (&mut self, area: XYWH, on: bool, modifier: Modifier) { - if on { - self.update(area, &|cell,_,_|cell.modifier.insert(modifier)) - } else { - self.update(area, &|cell,_,_|cell.modifier.remove(modifier)) - } - } - pub fn fill_bold (&mut self, area: XYWH, on: bool) { self.fill_mod(area, on, Modifier::BOLD) } - pub fn fill_reversed (&mut self, area: XYWH, on: bool) { self.fill_mod(area, on, Modifier::REVERSED) } - pub fn fill_crossed_out (&mut self, area: XYWH, on: bool) { self.fill_mod(area, on, Modifier::CROSSED_OUT) } - pub fn fill_ul (&mut self, area: XYWH, color: Option) { - if let Some(color) = color { - self.update(area, &|cell,_,_|{ - cell.modifier.insert(ratatui::prelude::Modifier::UNDERLINED); - cell.underline_color = color; - }) - } else { - self.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.write().unwrap().content.iter_mut() { - cell.fg = fg; - cell.bg = bg; - cell.modifier = modifier; - } - } - pub fn blit (&mut self, text: &impl AsRef, x: u16, y: u16, style: Option