From cc4a428143bc5ee90e5a190161217d4866d712aa Mon Sep 17 00:00:00 2001 From: facile pop culture reference Date: Mon, 3 Aug 2026 11:33:26 +0300 Subject: [PATCH 1/5] fold-in perf and time modules --- src/lib.rs | 1012 ++++++++++++++++++++++++++++++++++++++++++++++++++- src/sing.rs | 924 ---------------------------------------------- src/time.rs | 63 ---- 3 files changed, 996 insertions(+), 1003 deletions(-) delete mode 100644 src/sing.rs delete mode 100644 src/time.rs diff --git a/src/lib.rs b/src/lib.rs index 3a5342e..a2371a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,17 +30,6 @@ pub(crate) use ::{ std::marker::PhantomData }; -macro_rules! features { - ($($feature:literal: [ $($module:ident),* ]),*) => { - $( - $( - #[cfg(feature = $feature)] pub mod $module; - #[cfg(feature = $feature)] pub use $module::*; - )* - )* - } -} - macro_rules! fn_kw_layout { ($name:ident |$state:ident, $output: ident, $expr:ident| $body:block) => { pub fn $name ( @@ -71,11 +60,6 @@ macro_rules! fn_kw_layout_tui { #[cfg(feature = "lang")] pub use ::dizzle::{Usually, Perhaps}; #[cfg(feature = "lang")] use ::dizzle::*; -features! { - "time": [ time ], - "sing": [ sing ] -} - /// Define a trait an implement it for various mutation-enabled wrapper types. */ #[macro_export] macro_rules! flex_trait_mut ( ($Trait:ident $(<$($A:ident:$T:ident),+>)? { @@ -291,6 +275,1002 @@ macro_rules! eval_xy ( } } +#[cfg(feature = "time")] pub use self::time::*; +#[cfg(feature = "time")] mod time { + use ::std::sync::atomic::Ordering::*; + use ::atomic_float::AtomicF64; + + /// Performance counter + #[derive(Debug)] + pub struct PerfModel { + pub clock: quanta::Clock, + /// Measurement has a small cost. Disable it here. + pub enabled: bool, + // In nanoseconds. Time used by last iteration. + pub used: AtomicF64, + // In microseconds. Max prescribed time for iteration (frame, chunk...). + pub window: AtomicF64, + } + + impl_default!(PerfModel: Self { + enabled: true, + clock: quanta::Clock::new(), + used: Default::default(), + window: Default::default(), + }); + + impl PerfModel { + pub fn get_t0 (&self) -> Option { + if self.enabled { + Some(self.clock.raw()) + } else { + None + } + } + pub fn get_t1 (&self, t0: Option) -> Option { + if let Some(t0) = t0 { + if self.enabled { + Some(self.clock.delta(t0, self.clock.raw())) + } else { + None + } + } else { + None + } + } + pub fn update (&self, t0: Option, microseconds: f64) { + if let Some(t0) = t0 { + let t1 = self.clock.raw(); + self.used.store(self.clock.delta_as_nanos(t0, t1) as f64, Relaxed); + self.window.store(microseconds, Relaxed,); + } + } + pub fn percentage (&self) -> Option { + let window = self.window.load(Relaxed) * 1000.0; + if window > 0.0 { + let used = self.used.load(Relaxed); + Some(100.0 * used / window) + } else { + None + } + } + pub fn cycle T, T> (&self, call: &mut F) -> T { + let t0 = self.get_t0(); + let result = call(self); + let _t1 = self.get_t1(t0).unwrap(); + result + } + } +} + +#[cfg(feature = "sing")] pub use self::sing::*; +#[cfg(feature = "sing")] mod sing { + use crate::{*, time::PerfModel}; + pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; + pub use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; + use ConnectName::*; + use ConnectScope::*; + use ConnectStatus::*; + 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>), + } + + /// 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))))) + } + /// 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()), + } + } + 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) + } + } + + 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 + } + } + + impl<'j, T: HasJack<'j>> HasJack<'j> for Arc { + fn jack (&self) -> &Jack<'j> { + (&**self).jack() + } + } + + /// Event enum for JACK events. + /// + /// ``` + /// let event = tengri::JackEvent::XRun; // kerpop + /// ``` + #[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); + + /// Notification handler wrapper for [BoxedJackEventHandler]. + pub type DynamicNotifications<'j> = + JackNotify>; + + /// Boxed [JackEvent] callback. + pub type BoxedJackEventHandler<'j> = + Box; + + 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 + } + } + + pub trait JackPerfModel { + fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); + } + + 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, + ); + } + } + } + + /// Trait for thing that has a JACK process callback. + pub trait Audio { + /// Handle a JACK event. + fn handle (&mut self, _event: JackEvent) {} + /// Projecss a JACK chunk. + fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { + Control::Continue + } + /// The JACK process callback function passed to the server. + fn callback ( + state: &Arc>, client: &Client, scope: &ProcessScope + ) -> Control where Self: Sized { + if let Ok(mut state) = state.write() { + state.process(client, scope) + } else { + Control::Quit + } + } + } + + /// 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>; + + /// Things that can provide a [jack::Client] reference. + /// + /// ``` + /// use tengri::*; + /// + /// let jack: &Jack = Jacked::default().jack(); + /// + /// #[derive(Default)] struct Jacked<'j>(Jack<'j>); + /// + /// impl<'j> HasJack<'j> for Jacked<'j> { + /// fn jack (&self) -> &Jack<'j> { &self.0 } + /// } + /// ``` + pub trait HasJack<'j>: Send + Sync { + /// Return the internal [jack::Client] handle + /// that lets you call the JACK API. + fn jack (&self) -> &Jack<'j>; + fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { + self.jack().with_client(op) + } + fn port_by_name (&self, name: &str) -> Option> { + self.with_client(|client|client.port_by_name(name)) + } + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + fn register_port (&self, name: impl AsRef) -> Usually> { + self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?)) + } + fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->jack::contrib::Position) + -> Usually<()> + { + if enable { + self.with_client(|client|match client.register_timebase_callback(false, callback) { + Ok(_) => Ok(()), + Err(e) => Err(e) + })? + } + Ok(()) + } + fn sync_follow (&self, _enable: bool) -> Usually<()> { + // TODO: sync follow + Ok(()) + } + } + + /// Implement [Audio]: provide JACK callbacks. + #[macro_export] macro_rules! impl_audio { + (| + $self1:ident: + $Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident + |$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => { + impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { + #[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb } + $(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })? + } + }; + + ($Struct:ident: $process:ident, $handle:ident) => { + impl Audio for $Struct { + #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { + $process(self, c, s) + } + #[inline] fn handle (&mut self, e: JackEvent) { + $handle(self, e) + } + } + }; + + ($Struct:ident: $process:ident) => { + impl Audio for $Struct { + #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { + $process(self, c, s) + } + } + }; + } + + pub trait JackPorts: HasJack<'static> { + /// Register a MIDI input port. + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register a MIDI output port. + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio input port. + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + /// Register an audio output port. + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; + } + + impl> JackPorts for J { + fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiInput::new(self.jack(), name, connect) + } + fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + MidiOutput::new(self.jack(), name, connect) + } + fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioInput::new(self.jack(), name, connect) + } + fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { + AudioOutput::new(self.jack(), name, connect) + } + } + + pub trait JackPort: HasJack<'static> { + const KIND: &'static str = "Port"; + type Port: PortSpec + Default; + type Pair: PortSpec + Default; + + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized; + + fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { + jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) + .map_err(|e|e.into()) + } + + fn close (self) -> Usually<()> where Self: Sized { + let jack = self.jack().clone(); + Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) + } + + fn into_port (self) -> Port where Self: Sized; + fn port_name (&self) -> &Arc; + fn port (&self) -> &Port; + fn port_mut (&mut self) -> &mut Port; + fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { + self.with_client(|c|c.ports(re_name, re_type, flags)) + } + fn port_by_id (&self, id: u32) -> Option> { + self.with_client(|c|c.port_by_id(id)) + } + fn port_by_name (&self, name: impl AsRef) -> Option> { + self.with_client(|c|c.port_by_name(name.as_ref())) + } + + fn connections (&self) -> &[Connect]; + fn connect_to_matching <'k> (&'k self) -> Usually<()> { + for connect in self.connections().iter() { + match &connect.name { + Some(Exact(name)) => { + *connect.status.write().unwrap() = self.connect_exact(name)?; + }, + Some(RegExp(re)) => { + *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; + }, + _ => {}, + }; + } + Ok(()) + } + fn connect_exact <'k> (&'k self, name: &str) -> + Usually, Arc, ConnectStatus)>> + { + self.with_client(move|c|{ + let mut status = vec![]; + for port in c.ports(None, None, PortFlags::empty()).iter() { + if port.as_str() == &*name { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected { + break + } + } + } + } + Ok(status) + }) + } + fn connect_regexp <'k> ( + &'k self, re: &str, scope: Option + ) -> Usually, Arc, ConnectStatus)>> { + self.with_client(move|c|{ + let mut status = vec![]; + let ports = c.ports(Some(&re), None, PortFlags::empty()); + for port in ports.iter() { + if let Some(port) = c.port_by_name(port.as_str()) { + let port_status = self.connect_to_unowned(&port)?; + let name = port.name()?.into(); + status.push((port, name, port_status)); + if port_status == Connected && scope == Some(One) { + break + } + } + } + Ok(status) + }) + } + /** Connect to a matching port by name. */ + fn connect_to_name (&self, name: impl AsRef) -> Usually { + self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { + self.connect_to_unowned(port) + } else { + Ok(Missing) + }) + } + /** Connect to a matching port by reference. */ + fn connect_to_unowned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + /** Connect to an owned matching port by reference. */ + fn connect_to_owned (&self, port: &Port) -> Usually { + self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { + Connected + } else if let Ok(_) = c.connect_ports(port, self.port()) { + Connected + } else { + Mismatch + })) + } + } + + /// Audio input port. + #[derive(Debug)] pub struct AudioInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, + } + + /// Audio output port. + #[derive(Debug)] pub struct AudioOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of ports to connect to. + pub connections: Vec, + } + + /// MIDI input port. + #[derive(Debug)] pub struct MidiInput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, + } + + /// MIDI output port. + #[derive(Debug)] pub struct MidiOutput { + /// Handle to JACK client, for receiving reconnect events. + pub jack: Jack<'static>, + /// Port name + pub name: Arc, + /// Port handle. + pub port: Port, + /// List of currently held notes. + pub held: Arc>, + /// List of ports to connect to. + pub connections: Vec, + /// Buffer + pub note_buffer: Vec, + /// Buffer + pub output_buffer: Vec>>, + } + + macro_rules! jack_port { + ($($Struct:ty = ($Port:ty => $Pair:ty) $({ $($tt:tt)* })?),*) => { + $( + impl HasJack<'static> for $Struct { + fn jack (&self) -> &Jack<'static> { &self.jack } + } + impl JackPort for $Struct { + type Port = $Port; + type Pair = $Pair; + fn port_name (&self) -> &Arc { + &self.name + } + fn port (&self) -> &Port { + &self.port + } + fn port_mut (&mut self) -> &mut Port { + &mut self.port + } + fn into_port (self) -> Port { + self.port + } + fn connections (&self) -> &[Connect] { + self.connections.as_slice() + } + $($($tt)*)? + } + )* + }; + } + + jack_port!( + AudioInput = (AudioIn => AudioOut) { + const KIND: &'static str = "Audio In"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + }; + port.connect_to_matching()?; + Ok(port) + } + }, + + AudioOutput = (AudioOut => AudioIn) { + const KIND: &'static str = "Audio Out"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + }; + port.connect_to_matching()?; + Ok(port) + } + }, + + MidiInput = (MidiIn => MidiOut) { + const KIND: &'static str = "MIDI In"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self { + port: Self::register(jack, name)?, + jack: jack.clone(), + name: name.as_ref().into(), + connections: connect.to_vec(), + held: Arc::new(RwLock::new([false;128])) + }; + port.connect_to_matching()?; + Ok(port) + } + }, + + MidiOutput = (MidiOut => MidiIn) { + const KIND: &'static str = "MIDI Out"; + fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) + -> Usually where Self: Sized + { + let port = Self::register(jack, name)?; + let jack = jack.clone(); + let name = name.as_ref().into(); + let connections = connect.to_vec(); + let port = Self { + jack, + port, + name, + connections, + held: Arc::new([false;128].into()), + note_buffer: vec![0;8], + output_buffer: vec![vec![];65536], + }; + port.connect_to_matching()?; + Ok(port) + } + } + ); + + pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; + + /// Trait for thing that may receive MIDI. + pub trait HasMidiIns { + fn midi_ins (&self) -> &Vec; + fn midi_ins_mut (&mut self) -> &mut Vec; + /// Collect MIDI input from app ports (TODO preallocate large buffers) + fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { + self.midi_ins().iter() + .map(|port|port.port().iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>() + } + fn midi_ins_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_ins().iter().enumerate().map(move|(i, input)|{ + let height = 1 + input.connections().len(); + let data = (i, input.port_name(), input.connections(), y, y + height); + y += height; + data + }) + } + } + /// Trait for thing that may output MIDI. + pub trait HasMidiOuts { + fn midi_outs (&self) -> &Vec; + fn midi_outs_mut (&mut self) -> &mut Vec; + fn midi_outs_with_sizes <'a> (&'a self) -> + impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a + { + let mut y = 0; + self.midi_outs().iter().enumerate().map(move|(i, output)|{ + let height = 1 + output.connections().len(); + let data = (i, output.port_name(), output.connections(), y, y + height); + y += height; + data + }) + } + fn midi_outs_emit (&mut self, scope: &ProcessScope) { + for port in self.midi_outs_mut().iter_mut() { + port.buffer_emit(scope) + } + } + } + + impl MidiOutput { + /// Clear the section of the output buffer that we will be using, + /// emitting "all notes off" at start of buffer if requested. + pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { + let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); + for frame in &mut self.output_buffer[0..n_frames] { + frame.clear(); + } + if reset { + all_notes_off(&mut self.output_buffer); + } + } + /// Write a note to the output buffer + pub fn buffer_write <'a> ( + &'a mut self, + sample: usize, + event: LiveEvent, + ) { + self.note_buffer.fill(0); + event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); + self.output_buffer[sample].push(self.note_buffer.clone()); + // Update the list of currently held notes. + if let LiveEvent::Midi { ref message, .. } = event { + update_keys(&mut*self.held.write().unwrap(), message); + } + } + /// Write a chunk of MIDI data from the output buffer to the output port. + pub fn buffer_emit (&mut self, scope: &ProcessScope) { + let samples = scope.n_frames() as usize; + let mut writer = self.port.writer(scope); + for (time, events) in self.output_buffer.iter().enumerate().take(samples) { + for bytes in events.iter() { + writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ + panic!("Failed to write MIDI data: {bytes:?}"); + }); + } + } + } + } + + impl MidiInput { + pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { + parse_midi_input(self.port().iter(scope)) + } + } + + /// Return boxed iterator of MIDI events + pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) + -> Box, &'a [u8])> + 'a> + { + Box::new(input.map(|::jack::RawMidi { time, bytes }|( + time as usize, + LiveEvent::parse(bytes).unwrap(), + bytes + ))) + } + + /// Add "all notes off" to the start of a buffer. + pub fn all_notes_off (output: &mut [Vec>]) { + let mut buf = vec![]; + let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; + let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; + evt.write(&mut buf).unwrap(); + output[0].push(buf); + } + + /// Update notes_in array + pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { + match message { + MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } + MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, + _ => {} + } + } + + impl> + AsMut>> HasMidiIns for T { + fn midi_ins (&self) -> &Vec { self.as_ref() } + fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } + } + + impl> + AsMut>> HasMidiOuts for T { + fn midi_outs (&self) -> &Vec { self.as_ref() } + fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } + } + + impl> AddMidiIn for T { + fn midi_in_add (&mut self) -> Usually<()> { + let index = self.midi_ins().len(); + let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; + self.midi_ins_mut().push(port); + Ok(()) + } + } + + /// Trail for thing that may gain new MIDI ports. + impl> AddMidiOut for T { + fn midi_out_add (&mut self) -> Usually<()> { + let index = self.midi_outs().len(); + let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; + self.midi_outs_mut().push(port); + Ok(()) + } + } + + /// May create new MIDI input ports. + pub trait AddMidiIn { + fn midi_in_add (&mut self) -> Usually<()>; + } + + /// May create new MIDI output ports. + pub trait AddMidiOut { + fn midi_out_add (&mut self) -> Usually<()>; + } + + #[derive(Clone, Debug, PartialEq)] pub enum ConnectName { + /** Exact match */ + Exact(Arc), + /** Match regular expression */ + RegExp(Arc), + } + + #[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { + One, + All + } + + #[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { + Missing, + Disconnected, + Connected, + Mismatch, + } + + /// Port connection manager. + /// + /// ``` + /// let connect = tengri::Connect::default(); + /// ``` + #[derive(Clone, Debug, Default)] + pub struct Connect { + pub name: Option, + pub scope: Option, + pub status: Arc, Arc, ConnectStatus)>>>, + pub info: Arc, + } + + impl Connect { + pub fn new > ( + exact: Option>, + re: Option>, + re_all: Option>, + ) -> Vec { + let mut connections = vec![]; + if let Some(exact ) = exact { for port in exact { connections.push(Self::exact(port)) } } + if let Some(regexp) = re { for port in regexp { connections.push(Self::regexp(port)) } } + if let Some(re_all) = re_all { for port in re_all { connections.push(Self::regexp_all(port)) } } + connections + } + + /// Connect to this exact port + pub fn exact (name: impl AsRef) -> Self { + let info = format!("=:{}", name.as_ref()).into(); + let name = Some(Exact(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + + pub fn regexp (name: impl AsRef) -> Self { + let info = format!("~:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } + } + + pub fn regexp_all (name: impl AsRef) -> Self { + let info = format!("+:{}", name.as_ref()).into(); + let name = Some(RegExp(name.as_ref().into())); + Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } + } + + pub fn info (&self) -> Arc { + format!(" ({}) {} {}", { + let status = self.status.read().unwrap(); + let mut ok = 0; + for (_, _, state) in status.iter() { + if *state == Connected { + ok += 1 + } + } + format!("{ok}/{}", status.len()) + }, match self.scope { + None => "x", + Some(One) => " ", + Some(All) => "*", + }, match &self.name { + None => format!("x"), + Some(Exact(name)) => format!("= {name}"), + Some(RegExp(name)) => format!("~ {name}"), + }).into() + } + } + + pub fn connect_midi_ins > ( + jack: &Jack<'static>, + name: &T, + midi_from: &[T], + midi_from_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(midi_from.into_iter()), + Some([].into_iter()), + midi_from_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.midi_in(&format!("{}/{index}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } + + pub fn connect_midi_outs > ( + jack: &Jack<'static>, + name: &T, + midi_to: &[T], + midi_to_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(midi_to.into_iter()), + Some([].into_iter()), + midi_to_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.midi_out(&format!("{index}/{}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } + + pub fn connect_audio_ins > ( + jack: &Jack<'static>, + name: &T, + audio_from: &[T], + audio_from_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(audio_from.into_iter()), + Some([].into_iter()), + audio_from_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.audio_in(&format!("{}/{index}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } + + pub fn connect_audio_outs > ( + jack: &Jack<'static>, + name: &T, + audio_to: &[T], + audio_to_re: Option<&[T]>, + ) -> Usually> { + Ok(Connect::new( + Some(audio_to.into_iter()), + Some([].into_iter()), + audio_to_re.map(|x|x.into_iter())).iter().enumerate() + .map(|(index, connect)|jack.audio_out(&format!("{index}/{}", name.as_ref()), &[connect.clone()])) + .collect::>()?) + } +} + #[cfg(feature = "play")] pub use self::task::*; #[cfg(feature = "play")] mod task { use std::{ diff --git a/src/sing.rs b/src/sing.rs deleted file mode 100644 index d7d4469..0000000 --- a/src/sing.rs +++ /dev/null @@ -1,924 +0,0 @@ -use crate::{*, time::PerfModel}; -pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; -pub use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; -use ConnectName::*; -use ConnectScope::*; -use ConnectStatus::*; -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>), -} - -/// 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))))) - } - /// 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()), - } - } - 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) - } -} - -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 - } -} - -impl<'j, T: HasJack<'j>> HasJack<'j> for Arc { - fn jack (&self) -> &Jack<'j> { - (&**self).jack() - } -} - -/// Event enum for JACK events. -/// -/// ``` -/// let event = tengri::JackEvent::XRun; // kerpop -/// ``` -#[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); - -/// Notification handler wrapper for [BoxedJackEventHandler]. -pub type DynamicNotifications<'j> = - JackNotify>; - -/// Boxed [JackEvent] callback. -pub type BoxedJackEventHandler<'j> = - Box; - -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 - } -} - -pub trait JackPerfModel { - fn update_from_jack_scope (&self, t0: Option, scope: &ProcessScope); -} - -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, - ); - } - } -} - -/// Trait for thing that has a JACK process callback. -pub trait Audio { - /// Handle a JACK event. - fn handle (&mut self, _event: JackEvent) {} - /// Projecss a JACK chunk. - fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { - Control::Continue - } - /// The JACK process callback function passed to the server. - fn callback ( - state: &Arc>, client: &Client, scope: &ProcessScope - ) -> Control where Self: Sized { - if let Ok(mut state) = state.write() { - state.process(client, scope) - } else { - Control::Quit - } - } -} - -/// 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>; - -/// Things that can provide a [jack::Client] reference. -/// -/// ``` -/// use tengri::*; -/// -/// let jack: &Jack = Jacked::default().jack(); -/// -/// #[derive(Default)] struct Jacked<'j>(Jack<'j>); -/// -/// impl<'j> HasJack<'j> for Jacked<'j> { -/// fn jack (&self) -> &Jack<'j> { &self.0 } -/// } -/// ``` -pub trait HasJack<'j>: Send + Sync { - /// Return the internal [jack::Client] handle - /// that lets you call the JACK API. - fn jack (&self) -> &Jack<'j>; - fn with_client (&self, op: impl FnOnce(&Client)->T) -> T { - self.jack().with_client(op) - } - fn port_by_name (&self, name: &str) -> Option> { - self.with_client(|client|client.port_by_name(name)) - } - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - fn register_port (&self, name: impl AsRef) -> Usually> { - self.with_client(|client|Ok(client.register_port(name.as_ref(), PS::default())?)) - } - fn sync_lead (&self, enable: bool, callback: impl Fn(TimebaseInfo)->jack::contrib::Position) - -> Usually<()> - { - if enable { - self.with_client(|client|match client.register_timebase_callback(false, callback) { - Ok(_) => Ok(()), - Err(e) => Err(e) - })? - } - Ok(()) - } - fn sync_follow (&self, _enable: bool) -> Usually<()> { - // TODO: sync follow - Ok(()) - } -} - -/// Implement [Audio]: provide JACK callbacks. -#[macro_export] macro_rules! impl_audio { - (| - $self1:ident: - $Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident - |$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => { - impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { - #[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb } - $(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })? - } - }; - - ($Struct:ident: $process:ident, $handle:ident) => { - impl Audio for $Struct { - #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { - $process(self, c, s) - } - #[inline] fn handle (&mut self, e: JackEvent) { - $handle(self, e) - } - } - }; - - ($Struct:ident: $process:ident) => { - impl Audio for $Struct { - #[inline] fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { - $process(self, c, s) - } - } - }; -} - -pub trait JackPorts: HasJack<'static> { - /// Register a MIDI input port. - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register a MIDI output port. - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio input port. - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; - /// Register an audio output port. - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually; -} - -impl> JackPorts for J { - fn midi_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiInput::new(self.jack(), name, connect) - } - fn midi_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - MidiOutput::new(self.jack(), name, connect) - } - fn audio_in (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioInput::new(self.jack(), name, connect) - } - fn audio_out (&self, name: &impl AsRef, connect: &[Connect]) -> Usually { - AudioOutput::new(self.jack(), name, connect) - } -} - -pub trait JackPort: HasJack<'static> { - const KIND: &'static str = "Port"; - type Port: PortSpec + Default; - type Pair: PortSpec + Default; - - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized; - - fn register (jack: &Jack<'static>, name: &impl AsRef) -> Usually> { - jack.with_client(|c|c.register_port::(name.as_ref(), Default::default())) - .map_err(|e|e.into()) - } - - fn close (self) -> Usually<()> where Self: Sized { - let jack = self.jack().clone(); - Ok(jack.with_client(|c|c.unregister_port(self.into_port()))?) - } - - fn into_port (self) -> Port where Self: Sized; - fn port_name (&self) -> &Arc; - fn port (&self) -> &Port; - fn port_mut (&mut self) -> &mut Port; - fn ports (&self, re_name: Option<&str>, re_type: Option<&str>, flags: PortFlags) -> Vec { - self.with_client(|c|c.ports(re_name, re_type, flags)) - } - fn port_by_id (&self, id: u32) -> Option> { - self.with_client(|c|c.port_by_id(id)) - } - fn port_by_name (&self, name: impl AsRef) -> Option> { - self.with_client(|c|c.port_by_name(name.as_ref())) - } - - fn connections (&self) -> &[Connect]; - fn connect_to_matching <'k> (&'k self) -> Usually<()> { - for connect in self.connections().iter() { - match &connect.name { - Some(Exact(name)) => { - *connect.status.write().unwrap() = self.connect_exact(name)?; - }, - Some(RegExp(re)) => { - *connect.status.write().unwrap() = self.connect_regexp(re, connect.scope)?; - }, - _ => {}, - }; - } - Ok(()) - } - fn connect_exact <'k> (&'k self, name: &str) -> - Usually, Arc, ConnectStatus)>> - { - self.with_client(move|c|{ - let mut status = vec![]; - for port in c.ports(None, None, PortFlags::empty()).iter() { - if port.as_str() == &*name { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected { - break - } - } - } - } - Ok(status) - }) - } - fn connect_regexp <'k> ( - &'k self, re: &str, scope: Option - ) -> Usually, Arc, ConnectStatus)>> { - self.with_client(move|c|{ - let mut status = vec![]; - let ports = c.ports(Some(&re), None, PortFlags::empty()); - for port in ports.iter() { - if let Some(port) = c.port_by_name(port.as_str()) { - let port_status = self.connect_to_unowned(&port)?; - let name = port.name()?.into(); - status.push((port, name, port_status)); - if port_status == Connected && scope == Some(One) { - break - } - } - } - Ok(status) - }) - } - /** Connect to a matching port by name. */ - fn connect_to_name (&self, name: impl AsRef) -> Usually { - self.with_client(|c|if let Some(ref port) = c.port_by_name(name.as_ref()) { - self.connect_to_unowned(port) - } else { - Ok(Missing) - }) - } - /** Connect to a matching port by reference. */ - fn connect_to_unowned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } - /** Connect to an owned matching port by reference. */ - fn connect_to_owned (&self, port: &Port) -> Usually { - self.with_client(|c|Ok(if let Ok(_) = c.connect_ports(self.port(), port) { - Connected - } else if let Ok(_) = c.connect_ports(port, self.port()) { - Connected - } else { - Mismatch - })) - } -} - -/// Audio input port. -#[derive(Debug)] pub struct AudioInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// Audio output port. -#[derive(Debug)] pub struct AudioOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI input port. -#[derive(Debug)] pub struct MidiInput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, -} - -/// MIDI output port. -#[derive(Debug)] pub struct MidiOutput { - /// Handle to JACK client, for receiving reconnect events. - pub jack: Jack<'static>, - /// Port name - pub name: Arc, - /// Port handle. - pub port: Port, - /// List of currently held notes. - pub held: Arc>, - /// List of ports to connect to. - pub connections: Vec, - /// Buffer - pub note_buffer: Vec, - /// Buffer - pub output_buffer: Vec>>, -} - -macro_rules! jack_port { - ($($Struct:ty = ($Port:ty => $Pair:ty) $({ $($tt:tt)* })?),*) => { - $( - impl HasJack<'static> for $Struct { - fn jack (&self) -> &Jack<'static> { &self.jack } - } - impl JackPort for $Struct { - type Port = $Port; - type Pair = $Pair; - fn port_name (&self) -> &Arc { - &self.name - } - fn port (&self) -> &Port { - &self.port - } - fn port_mut (&mut self) -> &mut Port { - &mut self.port - } - fn into_port (self) -> Port { - self.port - } - fn connections (&self) -> &[Connect] { - self.connections.as_slice() - } - $($($tt)*)? - } - )* - }; -} - -jack_port!( - AudioInput = (AudioIn => AudioOut) { - const KIND: &'static str = "Audio In"; - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - }; - port.connect_to_matching()?; - Ok(port) - } - }, - - AudioOutput = (AudioOut => AudioIn) { - const KIND: &'static str = "Audio Out"; - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - }; - port.connect_to_matching()?; - Ok(port) - } - }, - - MidiInput = (MidiIn => MidiOut) { - const KIND: &'static str = "MIDI In"; - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self { - port: Self::register(jack, name)?, - jack: jack.clone(), - name: name.as_ref().into(), - connections: connect.to_vec(), - held: Arc::new(RwLock::new([false;128])) - }; - port.connect_to_matching()?; - Ok(port) - } - }, - - MidiOutput = (MidiOut => MidiIn) { - const KIND: &'static str = "MIDI Out"; - fn new (jack: &Jack<'static>, name: &impl AsRef, connect: &[Connect]) - -> Usually where Self: Sized - { - let port = Self::register(jack, name)?; - let jack = jack.clone(); - let name = name.as_ref().into(); - let connections = connect.to_vec(); - let port = Self { - jack, - port, - name, - connections, - held: Arc::new([false;128].into()), - note_buffer: vec![0;8], - output_buffer: vec![vec![];65536], - }; - port.connect_to_matching()?; - Ok(port) - } - } -); - -pub type CollectedMidiInput<'a> = Vec, MidiError>)>>; - -/// Trait for thing that may receive MIDI. -pub trait HasMidiIns { - fn midi_ins (&self) -> &Vec; - fn midi_ins_mut (&mut self) -> &mut Vec; - /// Collect MIDI input from app ports (TODO preallocate large buffers) - fn midi_input_collect <'a> (&'a self, scope: &'a ProcessScope) -> CollectedMidiInput<'a> { - self.midi_ins().iter() - .map(|port|port.port().iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>() - } - fn midi_ins_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_ins().iter().enumerate().map(move|(i, input)|{ - let height = 1 + input.connections().len(); - let data = (i, input.port_name(), input.connections(), y, y + height); - y += height; - data - }) - } -} -/// Trait for thing that may output MIDI. -pub trait HasMidiOuts { - fn midi_outs (&self) -> &Vec; - fn midi_outs_mut (&mut self) -> &mut Vec; - fn midi_outs_with_sizes <'a> (&'a self) -> - impl Iterator, &'a [Connect], usize, usize)> + Send + Sync + 'a - { - let mut y = 0; - self.midi_outs().iter().enumerate().map(move|(i, output)|{ - let height = 1 + output.connections().len(); - let data = (i, output.port_name(), output.connections(), y, y + height); - y += height; - data - }) - } - fn midi_outs_emit (&mut self, scope: &ProcessScope) { - for port in self.midi_outs_mut().iter_mut() { - port.buffer_emit(scope) - } - } -} - -impl MidiOutput { - /// Clear the section of the output buffer that we will be using, - /// emitting "all notes off" at start of buffer if requested. - pub fn buffer_clear (&mut self, scope: &ProcessScope, reset: bool) { - let n_frames = (scope.n_frames() as usize).min(self.output_buffer.len()); - for frame in &mut self.output_buffer[0..n_frames] { - frame.clear(); - } - if reset { - all_notes_off(&mut self.output_buffer); - } - } - /// Write a note to the output buffer - pub fn buffer_write <'a> ( - &'a mut self, - sample: usize, - event: LiveEvent, - ) { - self.note_buffer.fill(0); - event.write(&mut self.note_buffer).expect("failed to serialize MIDI event"); - self.output_buffer[sample].push(self.note_buffer.clone()); - // Update the list of currently held notes. - if let LiveEvent::Midi { ref message, .. } = event { - update_keys(&mut*self.held.write().unwrap(), message); - } - } - /// Write a chunk of MIDI data from the output buffer to the output port. - pub fn buffer_emit (&mut self, scope: &ProcessScope) { - let samples = scope.n_frames() as usize; - let mut writer = self.port.writer(scope); - for (time, events) in self.output_buffer.iter().enumerate().take(samples) { - for bytes in events.iter() { - writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{ - panic!("Failed to write MIDI data: {bytes:?}"); - }); - } - } - } -} - -impl MidiInput { - pub fn parsed <'a> (&'a self, scope: &'a ProcessScope) -> impl Iterator, &'a [u8])> { - parse_midi_input(self.port().iter(scope)) - } -} - -/// Return boxed iterator of MIDI events -pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) - -> Box, &'a [u8])> + 'a> -{ - Box::new(input.map(|::jack::RawMidi { time, bytes }|( - time as usize, - LiveEvent::parse(bytes).unwrap(), - bytes - ))) -} - -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut [Vec>]) { - let mut buf = vec![]; - let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; - let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; - evt.write(&mut buf).unwrap(); - output[0].push(buf); -} - -/// Update notes_in array -pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) { - match message { - MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; } - MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; }, - _ => {} - } -} - -impl> + AsMut>> HasMidiIns for T { - fn midi_ins (&self) -> &Vec { self.as_ref() } - fn midi_ins_mut (&mut self) -> &mut Vec { self.as_mut() } -} - -impl> + AsMut>> HasMidiOuts for T { - fn midi_outs (&self) -> &Vec { self.as_ref() } - fn midi_outs_mut (&mut self) -> &mut Vec { self.as_mut() } -} - -impl> AddMidiIn for T { - fn midi_in_add (&mut self) -> Usually<()> { - let index = self.midi_ins().len(); - let port = MidiInput::new(self.jack(), &format!("M/{index}"), &[])?; - self.midi_ins_mut().push(port); - Ok(()) - } -} - -/// Trail for thing that may gain new MIDI ports. -impl> AddMidiOut for T { - fn midi_out_add (&mut self) -> Usually<()> { - let index = self.midi_outs().len(); - let port = MidiOutput::new(self.jack(), &format!("{index}/M"), &[])?; - self.midi_outs_mut().push(port); - Ok(()) - } -} - -/// May create new MIDI input ports. -pub trait AddMidiIn { - fn midi_in_add (&mut self) -> Usually<()>; -} - -/// May create new MIDI output ports. -pub trait AddMidiOut { - fn midi_out_add (&mut self) -> Usually<()>; -} - -#[derive(Clone, Debug, PartialEq)] pub enum ConnectName { - /** Exact match */ - Exact(Arc), - /** Match regular expression */ - RegExp(Arc), -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectScope { - One, - All -} - -#[derive(Clone, Copy, Debug, PartialEq)] pub enum ConnectStatus { - Missing, - Disconnected, - Connected, - Mismatch, -} - -/// Port connection manager. -/// -/// ``` -/// let connect = tengri::Connect::default(); -/// ``` -#[derive(Clone, Debug, Default)] -pub struct Connect { - pub name: Option, - pub scope: Option, - pub status: Arc, Arc, ConnectStatus)>>>, - pub info: Arc, -} - -impl Connect { - pub fn new > ( - exact: Option>, - re: Option>, - re_all: Option>, - ) -> Vec { - let mut connections = vec![]; - if let Some(exact ) = exact { for port in exact { connections.push(Self::exact(port)) } } - if let Some(regexp) = re { for port in regexp { connections.push(Self::regexp(port)) } } - if let Some(re_all) = re_all { for port in re_all { connections.push(Self::regexp_all(port)) } } - connections - } - - /// Connect to this exact port - pub fn exact (name: impl AsRef) -> Self { - let info = format!("=:{}", name.as_ref()).into(); - let name = Some(Exact(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - - pub fn regexp (name: impl AsRef) -> Self { - let info = format!("~:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(One), status: Arc::new(RwLock::new(vec![])), info } - } - - pub fn regexp_all (name: impl AsRef) -> Self { - let info = format!("+:{}", name.as_ref()).into(); - let name = Some(RegExp(name.as_ref().into())); - Self { name, scope: Some(All), status: Arc::new(RwLock::new(vec![])), info } - } - - pub fn info (&self) -> Arc { - format!(" ({}) {} {}", { - let status = self.status.read().unwrap(); - let mut ok = 0; - for (_, _, state) in status.iter() { - if *state == Connected { - ok += 1 - } - } - format!("{ok}/{}", status.len()) - }, match self.scope { - None => "x", - Some(One) => " ", - Some(All) => "*", - }, match &self.name { - None => format!("x"), - Some(Exact(name)) => format!("= {name}"), - Some(RegExp(name)) => format!("~ {name}"), - }).into() - } -} - -pub fn connect_midi_ins > ( - jack: &Jack<'static>, - name: &T, - midi_from: &[T], - midi_from_re: Option<&[T]>, -) -> Usually> { - Ok(Connect::new( - Some(midi_from.into_iter()), - Some([].into_iter()), - midi_from_re.map(|x|x.into_iter())).iter().enumerate() - .map(|(index, connect)|jack.midi_in(&format!("{}/{index}", name.as_ref()), &[connect.clone()])) - .collect::>()?) -} - -pub fn connect_midi_outs > ( - jack: &Jack<'static>, - name: &T, - midi_to: &[T], - midi_to_re: Option<&[T]>, -) -> Usually> { - Ok(Connect::new( - Some(midi_to.into_iter()), - Some([].into_iter()), - midi_to_re.map(|x|x.into_iter())).iter().enumerate() - .map(|(index, connect)|jack.midi_out(&format!("{index}/{}", name.as_ref()), &[connect.clone()])) - .collect::>()?) -} - -pub fn connect_audio_ins > ( - jack: &Jack<'static>, - name: &T, - audio_from: &[T], - audio_from_re: Option<&[T]>, -) -> Usually> { - Ok(Connect::new( - Some(audio_from.into_iter()), - Some([].into_iter()), - audio_from_re.map(|x|x.into_iter())).iter().enumerate() - .map(|(index, connect)|jack.audio_in(&format!("{}/{index}", name.as_ref()), &[connect.clone()])) - .collect::>()?) -} - -pub fn connect_audio_outs > ( - jack: &Jack<'static>, - name: &T, - audio_to: &[T], - audio_to_re: Option<&[T]>, -) -> Usually> { - Ok(Connect::new( - Some(audio_to.into_iter()), - Some([].into_iter()), - audio_to_re.map(|x|x.into_iter())).iter().enumerate() - .map(|(index, connect)|jack.audio_out(&format!("{index}/{}", name.as_ref()), &[connect.clone()])) - .collect::>()?) -} diff --git a/src/time.rs b/src/time.rs deleted file mode 100644 index 93b5a1f..0000000 --- a/src/time.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::*; - -/// Performance counter -#[derive(Debug)] -pub struct PerfModel { - pub clock: quanta::Clock, - /// Measurement has a small cost. Disable it here. - pub enabled: bool, - // In nanoseconds. Time used by last iteration. - pub used: AtomicF64, - // In microseconds. Max prescribed time for iteration (frame, chunk...). - pub window: AtomicF64, -} - -impl_default!(PerfModel: Self { - enabled: true, - clock: quanta::Clock::new(), - used: Default::default(), - window: Default::default(), -}); - -impl PerfModel { - pub fn get_t0 (&self) -> Option { - if self.enabled { - Some(self.clock.raw()) - } else { - None - } - } - pub fn get_t1 (&self, t0: Option) -> Option { - if let Some(t0) = t0 { - if self.enabled { - Some(self.clock.delta(t0, self.clock.raw())) - } else { - None - } - } else { - None - } - } - pub fn update (&self, t0: Option, microseconds: f64) { - if let Some(t0) = t0 { - let t1 = self.clock.raw(); - self.used.store(self.clock.delta_as_nanos(t0, t1) as f64, Relaxed); - self.window.store(microseconds, Relaxed,); - } - } - pub fn percentage (&self) -> Option { - let window = self.window.load(Relaxed) * 1000.0; - if window > 0.0 { - let used = self.used.load(Relaxed); - Some(100.0 * used / window) - } else { - None - } - } - pub fn cycle T, T> (&self, call: &mut F) -> T { - let t0 = self.get_t0(); - let result = call(self); - let _t1 = self.get_t1(t0).unwrap(); - result - } -} From 4c25dcc702e764a7a82a3f68e6c44fe9c8b8193c Mon Sep 17 00:00:00 2001 From: facile pop culture reference Date: Mon, 3 Aug 2026 11:40:43 +0300 Subject: [PATCH 2/5] make Draw::clip take drawable instead of fn --- src/layout/align.rs | 9 +++++---- src/layout/area.rs | 3 ++- src/layout/exact.rs | 2 +- src/layout/full.rs | 6 +++--- src/layout/max.rs | 2 +- src/layout/min.rs | 6 +++--- src/layout/push.rs | 6 +++--- src/layout/split.rs | 8 ++++---- src/lib.rs | 26 +++++++++++++------------- 9 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/layout/align.rs b/src/layout/align.rs index 1e1c961..b94788e 100644 --- a/src/layout/align.rs +++ b/src/layout/align.rs @@ -11,10 +11,11 @@ impl> Draw for Align { Ok(align::(area, self.1.layout(area)?, self.0)) } fn draw (self, to: &mut S) -> Perhaps> { - Ok(self.layout(to.area())? - .map(|area|to.clip(area, |to|self.1.draw(to))) - .transpose()? - .flatten()) + Ok(if let Some(area) = self.layout(to.area())? { + to.clip(area, self.1)? + } else { + None + }) } } diff --git a/src/layout/area.rs b/src/layout/area.rs index b6d68da..6533d35 100644 --- a/src/layout/area.rs +++ b/src/layout/area.rs @@ -16,6 +16,7 @@ pub struct Area>( pub Option>, pub T ); + impl_draw!(,>|self: Area, to: S|{ - to.clip(self.0, |to|self.1.draw(to)) + to.clip(self.0, self.1) }); diff --git a/src/layout/exact.rs b/src/layout/exact.rs index e94f51d..ef51dc2 100644 --- a/src/layout/exact.rs +++ b/src/layout/exact.rs @@ -22,7 +22,7 @@ impl , X: Into> + Copy> Draw for Exact< fn draw (self, to: &mut S) -> Perhaps> { let area = layout_exact(&self, to.area()); let item = match self { Self::W(i, ..) => i, Self::H(i, ..) => i, Self::WH(i, ..) => i, _ => unreachable!() }; - to.clip(area, |to|item.draw(to)) + to.clip(area, item) } } diff --git a/src/layout/full.rs b/src/layout/full.rs index 5dbd40f..7da3c3f 100644 --- a/src/layout/full.rs +++ b/src/layout/full.rs @@ -22,17 +22,17 @@ impl_draw!(,>|self: Full, to: T|{ let XYWH(x0, y0, w0, h0) = to.area(); match self { Self::W(item) => if let Some(XYWH(_, y, _, h)) = item.layout(to.area())? { - to.clip(XYWH(x0, y, w0, h), |to|item.draw(to)) + to.clip(XYWH(x0, y, w0, h), item) } else { Ok(None) }, Self::H(item) => if let Some(XYWH(x, _, w, _)) = item.layout(to.area())? { - to.clip(XYWH(x, y0, w, h0), |to|item.draw(to)) + to.clip(XYWH(x, y0, w, h0), item) } else { Ok(None) }, Self::WH(item) => if let Some(XYWH(..)) = item.layout(to.area())? { - to.clip(XYWH(x0, y0, w0, h0), |to|item.draw(to)) + to.clip(XYWH(x0, y0, w0, h0), item) } else { Ok(None) }, diff --git a/src/layout/max.rs b/src/layout/max.rs index 9fc1c40..9b3f40a 100644 --- a/src/layout/max.rs +++ b/src/layout/max.rs @@ -50,6 +50,6 @@ impl, X: Into> + Copy> Draw for Max return Ok(None) }; - to.clip(area, |to|item.draw(to)) + to.clip(area, item) } } diff --git a/src/layout/min.rs b/src/layout/min.rs index d747253..70c6549 100644 --- a/src/layout/min.rs +++ b/src/layout/min.rs @@ -24,16 +24,16 @@ impl_draw!(, X: Into>,>|self: Min Self::__(_) => unreachable!(), Self::W(item, w1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { let w = w1.into().map(|w1|w.max(w1)).unwrap_or(w); - to.clip(XYWH(x, y, w, h), |to|item.draw(to)) + to.clip(XYWH(x, y, w, h), item) }, Self::H(item, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { let h = h1.into().map(|h1|h.max(h1)).unwrap_or(h); - to.clip(XYWH(x, y, w, h), |to|item.draw(to)) + to.clip(XYWH(x, y, w, h), item) }, Self::WH(item, w1, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { let w = w1.into().map(|w1|w.max(w1)).unwrap_or(w); let h = h1.into().map(|h1|h.max(h1)).unwrap_or(h); - to.clip(XYWH(x, y, w, h), |to|item.draw(to)) + to.clip(XYWH(x, y, w, h), item) }, _ => Ok(None) } diff --git a/src/layout/push.rs b/src/layout/push.rs index 7d1114d..0907ad1 100644 --- a/src/layout/push.rs +++ b/src/layout/push.rs @@ -25,17 +25,17 @@ impl_draw!(, X: Into>,>|self: Push { to.clip(XYWH( x + x1.into().unwrap_or_default(), y, w, h - ), |to|item.draw(to)) + ), item) }, Self::Y(item, y1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { to.clip(XYWH( x, y + y1.into().unwrap_or_default(), w, h - ), |to|item.draw(to)) + ), item) }, Self::XY(item, x1, y1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { to.clip(XYWH( x + x1.into().unwrap_or_default(), y + y1.into().unwrap_or_default(), w, h - ), |to|item.draw(to)) + ), item) }, _ => Ok(None) } diff --git a/src/layout/split.rs b/src/layout/split.rs index 555c75a..77120c3 100644 --- a/src/layout/split.rs +++ b/src/layout/split.rs @@ -122,14 +122,14 @@ fn draw_stacks ( origin_b: impl Into>, ) -> Usually<(Option>, Option>)> { let draw_a = |to: &mut S|Ok::<_, Box>(if let Some(origin_a) = origin_a.into() { - to.clip(area_a.into(), |to|a.align(origin_a).draw(to))? + to.clip(area_a.into(), a.align(origin_a))? } else { - to.clip(area_a.into(), |to|a.draw(to))? + to.clip(area_a.into(), a)? }); let draw_b = |to: &mut S|Ok::<_, Box>(if let Some(origin_b) = origin_b.into() { - to.clip(area_b.into(), |to|b.align(origin_b).draw(to))? + to.clip(area_b.into(), b.align(origin_b))? } else { - to.clip(area_b.into(), |to|b.draw(to))? + to.clip(area_b.into(), b)? }); Ok(if matches!(split, Split::Below) { let drawn_b = draw_b(to)?; diff --git a/src/lib.rs b/src/lib.rs index a2371a8..6d92fe2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1366,12 +1366,12 @@ macro_rules! eval_xy ( /// fn area (&self) -> XYWH { /// Default::default() /// } - /// fn clip ( + /// fn clip ( /// &mut self, /// area: impl Into>>, - /// draw: impl FnOnce(&mut Self)->T - /// ) -> T { - /// draw(self) + /// draw: impl Draw + /// ) -> Perhaps> { + /// draw.to(self) /// } /// } /// @@ -1387,11 +1387,11 @@ macro_rules! eval_xy ( /// Get current clipping area fn area (&self) -> XYWH; /// Set clipping area - fn clip ( + fn clip ( &mut self, area: impl Into>>, - draw: impl FnOnce(&mut Self)->T - ) -> T; + draw: impl Draw + ) -> Perhaps>; } /// Implement the [Draw] trait for a particular drawable and [Screen]. @@ -2645,16 +2645,16 @@ macro_rules! eval_xy ( } } - fn clip ( + fn clip ( &mut self, area: impl Into>>, - draw: impl FnOnce(&mut Self)->T - ) -> T { + draw: impl Draw + ) -> Perhaps> { let prev = self.area(); if let Some(area) = area.into() { *self.area_mut() = area.into(); } - let result = draw(self); + let result = draw.draw(self); *self.area_mut() = prev; result } @@ -2815,10 +2815,10 @@ macro_rules! eval_xy ( self.1.layout(area) } fn draw (self, to: &mut Tui) -> Drawn { - self.layout(to.area()).map(|area|to.clip(area, |to|{ + self.layout(to.area()).map(|area|to.clip(area, draw(|to: &mut Tui|{ to.update(&|cell,_,_|{ cell.set_bg(self.0); }); self.1.draw(to) - }))? + })))? } } From 083ce8ba76bfe4bad3d2b63a6a664db1c2b091d7 Mon Sep 17 00:00:00 2001 From: facile pop culture reference Date: Mon, 3 Aug 2026 11:51:58 +0300 Subject: [PATCH 3/5] remove Screen::show --- src/layout/align.rs | 3 +++ src/layout/full.rs | 1 + src/lib.rs | 50 ++++++++++++++++++--------------------------- 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/layout/align.rs b/src/layout/align.rs index b94788e..f7779fd 100644 --- a/src/layout/align.rs +++ b/src/layout/align.rs @@ -40,3 +40,6 @@ fn align ( } }) } + +#[cfg(test)] #[test] fn test_align () { +} diff --git a/src/layout/full.rs b/src/layout/full.rs index 7da3c3f..5c2a717 100644 --- a/src/layout/full.rs +++ b/src/layout/full.rs @@ -18,6 +18,7 @@ pub enum Full> { H(I), WH(I), } + impl_draw!(,>|self: Full, to: T|{ let XYWH(x0, y0, w0, h0) = to.area(); match self { diff --git a/src/lib.rs b/src/lib.rs index 6d92fe2..c706bc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1359,10 +1359,6 @@ macro_rules! eval_xy ( /// } /// impl Screen for TestOut { /// type Unit = u16; - /// fn show (&mut self, _: impl Draw) -> Perhaps> { - /// println!("placed"); - /// Ok(None) - /// } /// fn area (&self) -> XYWH { /// Default::default() /// } @@ -1382,8 +1378,6 @@ macro_rules! eval_xy ( /// ``` pub trait Screen: Xy + Wh + Send + Sync + Sized { type Unit: Coord; - /// Render drawable in subarea specified by `area` - fn show (&mut self, content: impl Draw) -> Perhaps>; /// Get current clipping area fn area (&self) -> XYWH; /// Set clipping area @@ -2612,37 +2606,15 @@ macro_rules! eval_xy ( /// Get mutable reference to current clipping area fn area_mut (&mut self) -> &mut XYWH { - match self { - Self::Draw(_, area) => area, - Self::Layout(area) => area, - } + self.into() } } impl Screen for Tui { type Unit = u16; - /// Render drawable in subarea specified by `area` - fn show (&mut self, content: impl Draw) -> Perhaps> { - let previous_area = self.area(); - Ok(if let Some(area) = content.layout(self.area())? { - *self.area_mut() = area; - if let Some(result_area) = content.draw(self)? { - *self.area_mut() = previous_area; - Some(result_area) - } else { - None - } - } else { - None - }) - } - /// Get current clipping area fn area (&self) -> XYWH { - match self { - Self::Draw(_, area) => *area, - Self::Layout(area) => *area, - } + self.into() } fn clip ( @@ -2660,6 +2632,24 @@ macro_rules! eval_xy ( } } + impl From<&Tui> for XYWH { + fn from (screen: &Tui) -> XYWH { + match screen { + Tui::Draw(_, area) => *area, + Tui::Layout(area) => *area, + } + } + } + + impl<'a: 'b, 'b> From<&'a mut Tui> for &'b mut XYWH { + fn from (screen: &'a mut Tui) -> &'b mut XYWH { + match screen { + Tui::Draw(_, area) => area, + Tui::Layout(area) => area, + } + } + } + pub const fn fill_char (c: char) -> impl Draw { draw(move|to: &mut Tui|Ok(Some(to.update(&|cell,_,_|{ cell.set_char(c); From 7f9b7091e2d94ceef6bf7f7ad5820e9a3fe39c04 Mon Sep 17 00:00:00 2001 From: facile pop culture reference Date: Tue, 4 Aug 2026 07:59:20 +0300 Subject: [PATCH 4/5] support multipass layout, barely --- examples/mode_02.rs | 1 - src/layout.rs | 103 ++++ src/layout/align.rs | 45 -- src/layout/area.rs | 2 +- src/layout/axis.rs | 245 +++++++++ src/layout/azimuth.rs | 455 ++++++++++++++++ src/layout/cond.rs | 46 ++ src/layout/exact.rs | 49 -- src/layout/full.rs | 42 -- src/layout/max.rs | 55 -- src/layout/min.rs | 40 -- src/layout/origin.rs | 21 - src/layout/pad.rs | 37 -- src/layout/pull.rs | 24 - src/layout/push.rs | 42 -- src/layout/split.rs | 315 ----------- src/lib.rs | 1165 ++--------------------------------------- src/term.rs | 517 ++++++++++++++++++ src/term/border.rs | 4 +- src/term/modify.rs | 156 ++++++ 20 files changed, 1581 insertions(+), 1783 deletions(-) create mode 100644 src/layout.rs delete mode 100644 src/layout/align.rs create mode 100644 src/layout/axis.rs create mode 100644 src/layout/azimuth.rs create mode 100644 src/layout/cond.rs delete mode 100644 src/layout/exact.rs delete mode 100644 src/layout/full.rs delete mode 100644 src/layout/max.rs delete mode 100644 src/layout/min.rs delete mode 100644 src/layout/origin.rs delete mode 100644 src/layout/pad.rs delete mode 100644 src/layout/pull.rs delete mode 100644 src/layout/push.rs delete mode 100644 src/layout/split.rs create mode 100644 src/term.rs create mode 100644 src/term/modify.rs diff --git a/examples/mode_02.rs b/examples/mode_02.rs index a2106b3..1ec2069 100644 --- a/examples/mode_02.rs +++ b/examples/mode_02.rs @@ -37,7 +37,6 @@ impl_keywords!(Tui, XYWH, State [ kw_split, kw_align, kw_exact, - kw_fixed, kw_min, kw_max, kw_push, diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000..9ef91cc --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,103 @@ +use crate::*; + +impl> Layout for T {} + +pub trait Layout: Draw + Sized { + fn full_w (self) -> impl Draw { + Full::W(self) + } + fn full_h (self) -> impl Draw { + Full::H(self) + } + fn full_wh (self) -> impl Draw { + Full::WH(self) + } + + fn origin (self, azimuth: impl Into>) -> impl Draw { + Origin(azimuth.into(), self) + } + fn origin_c (self) -> impl Draw { + Origin(Some(Azimuth::C), self) + } + fn origin_x (self) -> impl Draw { + Origin(Some(Azimuth::X), self) + } + fn origin_y (self) -> impl Draw { + Origin(Some(Azimuth::Y), self) + } + + fn origin_n (self) -> impl Draw { + Origin(Some(Azimuth::N), self) + } + fn origin_s (self) -> impl Draw { + Origin(Some(Azimuth::S), self) + } + fn origin_e (self) -> impl Draw { + Origin(Some(Azimuth::E), self) + } + fn origin_w (self) -> impl Draw { + Origin(Some(Azimuth::W), self) + } + + fn origin_ne (self) -> impl Draw { + Origin(Some(Azimuth::NE), self) + } + fn origin_se (self) -> impl Draw { + Origin(Some(Azimuth::SE), self) + } + fn origin_nw (self) -> impl Draw { + Origin(Some(Azimuth::NW), self) + } + fn origin_sw (self) -> impl Draw { + Origin(Some(Azimuth::SW), self) + } +} + +/// Uses [AtomicUsize] to measure size during\ +/// rendering (which is normally read-only). +#[derive(Default, Debug, Clone)] +pub struct Sizer( + /// Width + pub Arc, + /// Height + pub Arc, +); + +impl Xy for Sizer { + fn x (&self) -> u16 { + self.0.load(Relaxed) as u16 + } + fn y (&self) -> u16 { + self.1.load(Relaxed) as u16 + } +} +impl Wide for Sizer { + fn w (&self) -> u16 { + self.0.load(Relaxed) as u16 + } +} +impl Tall for Sizer { + fn h (&self) -> u16 { + self.1.load(Relaxed) as u16 + } +} +impl PartialEq for Sizer { + fn eq (&self, _: &Self) -> bool { todo!() } +} + +impl Sizer { + pub const fn of (&self, of: impl Draw) -> impl Draw { + draw(move|to: &mut T|{ + let area = of.draw(to)?; + self.0.store(area.map(|a|a.w()).unwrap_or(T::Unit::zero()).into(), Relaxed); + self.1.store(area.map(|a|a.h()).unwrap_or(T::Unit::zero()).into(), Relaxed); + Ok(area) + }) + } +} + +mod area; pub use self::area::*; +mod axis; pub use self::axis::*; +mod azimuth; pub use self::azimuth::*; +mod cond; pub use self::cond::*; +mod iter; pub use self::iter::*; diff --git a/src/layout/align.rs b/src/layout/align.rs deleted file mode 100644 index f7779fd..0000000 --- a/src/layout/align.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::*; -use Azimuth::*; - -pub struct Align( - pub(crate) Option, - pub(crate) T, -); - -impl> Draw for Align { - fn layout (&self, area: XYWH) -> Perhaps> { - Ok(align::(area, self.1.layout(area)?, self.0)) - } - fn draw (self, to: &mut S) -> Perhaps> { - Ok(if let Some(area) = self.layout(to.area())? { - to.clip(area, self.1)? - } else { - None - }) - } -} - -fn align ( - area0: XYWH, area: Option>, azimuth: Option -) -> Option> { - area.map(|XYWH(x, y, w, h)|{ - let XYWH(x0, y0, w0, h0) = area0; - match azimuth { - Some(NW) => XYWH(x0, y0, w, h), - Some(N) => XYWH(x0 + w0.minus(w) / 2.into(), y0, w, h), - Some(NE) => XYWH((x0 + w0).minus(w), y0, w, h), - Some(W) => XYWH(x0, y0 + h0.minus(h) / 2.into(), w, h), - Some(C) => XYWH(x0 + w0.minus(w) / 2.into(), y0 + h0.minus(h) / 2.into(), w, h), - Some(E) => XYWH((x0 + w0).minus(w), y0 + h0.minus(h) / 2.into(), w, h), - Some(SW) => XYWH(x0, (y0 + h0).minus(h), w, h), - Some(S) => XYWH(x0 + w0.minus(w) / 2.into(), (y0 + h0).minus(h), w, h), - Some(SE) => XYWH((x0 + w0).minus(w), (y0 + h0).minus(h), w, h), - Some(X) => XYWH(x0 + w0.minus(w) / 2.into(), y, w, h), - Some(Y) => XYWH(x, y0 + h0.minus(h) / 2.into(), w, h), - None => XYWH(x, y, w, h) - } - }) -} - -#[cfg(test)] #[test] fn test_align () { -} diff --git a/src/layout/area.rs b/src/layout/area.rs index 6533d35..a806c92 100644 --- a/src/layout/area.rs +++ b/src/layout/area.rs @@ -18,5 +18,5 @@ pub struct Area>( ); impl_draw!(,>|self: Area, to: S|{ - to.clip(self.0, self.1) + to.draw(self.0, &self.1) }); diff --git a/src/layout/axis.rs b/src/layout/axis.rs new file mode 100644 index 0000000..0cb59e9 --- /dev/null +++ b/src/layout/axis.rs @@ -0,0 +1,245 @@ +use crate::*; + +macro_rules! def_layout_modifier { + ( + $Trait:ident ($fn_x:ident $fn_y:ident $fn_xy:ident), + $Struct:ident { $X:ident $Y:ident $XY:ident } $kw_name:ident $name:literal + |$self:ident, $to:ident| $body:block + ) => { + impl> $Trait for T {} + + pub trait $Trait: Draw + Sized { + fn $fn_x > + Copy> (self, x: N) + -> $Struct { $Struct::$X(self, x) } + fn $fn_y > + Copy> (self, y: N) + -> $Struct { $Struct::$Y(self, y) } + fn $fn_xy > + Copy> (self, x: N, y: N) + -> $Struct { $Struct::$XY(self, x, y) } + } + + pub enum $Struct, X: Into>> { + __(PhantomData), $X(I, X), $Y(I, X), $XY(I, X, X), + } + + impl , X: Into> + Copy> Draw for $Struct { + fn draw (&$self, $to: &mut S) -> Perhaps> { + $body + } + } + + fn_kw_layout!($kw_name |state, output, expr| { + let head = expr.head()?; + let mut frags = head.src()?.unwrap_or_default().split("/"); + Ok(matches!(expr.head()?, Some("exact")).then(||{ + let args = expr.tail(); + let arg0 = args.head(); + let tail0 = args.tail(); + let arg1 = tail0.head(); + let tail1 = tail0.tail(); + let arg2 = tail1.head(); + eval_xy!( + $name => expr, head, output, state, frags.next(), + $fn_xy, $fn_x, $fn_y, + arg0, arg1, arg2, + ) + }).transpose()?.flatten()) + }); + } +} + +def_layout_modifier!( + CanExact (exact_w exact_h exact_wh), + Exact { W H WH } kw_exact "exact" |self, to| { + let XYWH(x, y, w0, h0) = to.area(); + let (item, w, h) = match self { + Self::W(item, w) => (item, (*w).into().unwrap_or(w0), h0), + Self::H(item, h) => (item, w0, (*h).into().unwrap_or(h0)), + Self::WH(item, w, h) => (item, (*w).into().unwrap_or(h0), (*h).into().unwrap_or(h0)), + _ => unreachable!() + }; + to.draw(XYWH(x, y, w0, h0), item) + } +); + +def_layout_modifier!( + CanMin (min_w min_h min_wh), + Min { W H WH } kw_min "min" |self, to| { + match self { + Self::__(_) => unreachable!(), + Self::W(item, w1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + let w: S::Unit = (*w1).into().map(|w1|w.max(w1)).unwrap_or(w); + to.draw(XYWH(x, y, w, h), item) + }, + Self::H(item, h1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + let h: S::Unit = (*h1).into().map(|h1|h.max(h1)).unwrap_or(h); + to.draw(XYWH(x, y, w, h), item) + }, + Self::WH(item, w1, h1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + let w: S::Unit = (*w1).into().map(|w1|w.max(w1)).unwrap_or(w); + let h: S::Unit = (*h1).into().map(|h1|h.max(h1)).unwrap_or(h); + to.draw(XYWH(x, y, w, h), item) + }, + _ => Ok(None) + } + } +); + +def_layout_modifier!( + CanMax (max_w max_h max_wh), + Max { W H WH } kw_max "max" |self, to| { + let area: XYWH = to.area(); + let (item, area) = match self { + Self::W(item, max_w) => (item, XYWH( + area.0, area.1, (*max_w).into().map(|max|max.min(area.2)).unwrap_or(area.2), + area.3 + )), + Self::H(item, max_h) => (item, XYWH( + area.0, area.1, area.2, + (*max_h).into().map(|max|max.min(area.3)).unwrap_or(area.3) + )), + Self::WH(item, max_w, max_h) => (item, XYWH( + area.0, area.1, (*max_w).into().map(|max|max.min(area.2)).unwrap_or(area.2), + (*max_h).into().map(|max|max.min(area.3)).unwrap_or(area.3), + )), + _ => return Ok(None) + }; + to.draw(area, item) + } +); + +def_layout_modifier!( + CanPad (pad_w pad_h pad_wh), + Pad { X Y XY } kw_pad "pad" |self, to| { + let area = to.area(); + let (item, area) = match self { + Self::X(item, w1) => { + let w1: S::Unit = (*w1).into().unwrap_or_default(); + (item, XYWH(area.0 + w1, area.1, area.2.minus(w1 + w1), area.3)) + }, + Self::Y(item, h1) => { + let h1: S::Unit = (*h1).into().unwrap_or_default(); + (item, XYWH(area.0, area.1 + h1, area.2, area.3.minus(h1 + h1))) + }, + Self::XY(item, w1, h1) => { + let w1: S::Unit = (*w1).into().unwrap_or_default(); + let h1: S::Unit = (*h1).into().unwrap_or_default(); + (item, XYWH(area.0 + w1, area.1 + h1, area.2.minus(w1 + w1), area.3.minus(h1 + h1))) + }, + _ => return Ok(None) + }; + item.draw(to) + } +); + +def_layout_modifier!( + CanPush (push_x push_y push_xy), + Push { X Y XY } kw_push "push" |self, to| { + match self { + Self::__(_) => unreachable!(), + Self::X(item, x1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + to.draw(XYWH(x + (*x1).into().unwrap_or_default(), y, w, h), item) + }, + Self::Y(item, y1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + to.draw(XYWH(x, y + (*y1).into().unwrap_or_default(), w, h), item) + }, + Self::XY(item, x1, y1) if let Some(XYWH(x, y, w, h)) = to.size(None, item)? => { + to.draw(XYWH(x + (*x1).into().unwrap_or_default(), y + (*y1).into().unwrap_or_default(), w, h), item) + }, + _ => Ok(None) + } + } +); + +def_layout_modifier!( + CanPull (pull_x pull_y pull_xy), + Pull { X Y XY } kw_pull "pull" |self, to| { + todo!() + } +); + +/// Use whole drawing area along one or both axes. +/// +/// ``` +/// # fn doctest_layout_full () -> Result<(), Box> { +/// use tengri::{Layout, Draw, XYWH}; +/// let area = XYWH(0u16, 0, 80, 25); +/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); +/// assert_eq!("1".full_w().layout(area)?, Some(XYWH(0u16, 0, 80, 1))); +/// assert_eq!("1".full_h().layout(area)?, Some(XYWH(0u16, 0, 1, 25))); +/// assert_eq!("1".full_wh().layout(area)?, Some(XYWH(0u16, 0, 80, 25))); +/// # Ok(()) } +/// ``` +pub enum Full> { + __(PhantomData), + W(I), + H(I), + WH(I), +} + +impl_draw!(,>|self: Full, to: T|{ + let XYWH(x0, y0, w0, h0) = to.area(); + match self { + Self::W(item) => if let Some(XYWH(_, y, _, h)) = to.size(None, item)? { + to.draw(XYWH(x0, y, w0, h), item) + } else { + Ok(None) + }, + Self::H(item) => if let Some(XYWH(x, _, w, _)) = to.size(None, item)? { + to.draw(XYWH(x, y0, w, h0), item) + } else { + Ok(None) + }, + Self::WH(item) => if let Some(XYWH(..)) = to.size(None, item)? { + to.draw(XYWH(x0, y0, w0, h0), item) + } else { + Ok(None) + }, + _ => unreachable!(), + } +}); + +/// Only draw content if area is above a certain size. +/// +/// ``` +/// # fn doctest_layout_min () -> Result<(), Box> { +/// use tengri::{Layout, Draw, XYWH}; +/// let area = XYWH(1u16, 1, 80, 25); +/// assert_eq!("1".min_w(5).layout(area)?, Some(XYWH(1u16, 1, 5, 1))); +/// assert_eq!("1".min_h(5).layout(area)?, Some(XYWH(1u16, 1, 1, 5))); +/// assert_eq!("1".min_wh(5, 5).layout(area)?, Some(XYWH(1u16, 1, 5, 5))); +/// assert_eq!("123456".min_w(5).layout(area)?, Some(XYWH(1u16, 1, 6, 1))); +/// # Ok(()) } +/// ``` +/// Move content in the negative direction of one or both axes. +/// +/// ``` +/// # fn doctest_layout_pull () -> Result<(), Box> { +/// use tengri::{Layout, Draw, XYWH}; +/// let area = XYWH(1u16, 1, 80, 25); +/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); +/// assert_eq!("1".pull_x(1).layout(area)?, Some(XYWH(0u16, 1, 1, 1))); +/// assert_eq!("1".pull_y(1).layout(area)?, Some(XYWH(1u16, 0, 1, 1))); +/// assert_eq!("1".pull_xy(1, 1).layout(area)?, Some(XYWH(0u16, 0, 1, 1))); +/// # Ok(()) } +/// ``` +/// Move content in the positive direction of one or both axes. +/// +/// ``` +/// # fn doctest_layout_push () -> Result<(), Box> { +/// use tengri::{Layout, Draw, XYWH}; +/// let area = XYWH(0u16, 0, 80, 25); +/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); +/// assert_eq!("1".push_x(1).layout(area)?, Some(XYWH(1u16, 0, 1, 1))); +/// assert_eq!("1".push_y(1).layout(area)?, Some(XYWH(0u16, 1, 1, 1))); +/// assert_eq!("1".push_xy(1, 1).layout(area)?, Some(XYWH(1u16, 1, 1, 1))); +/// # Ok(()) } +/// ``` +#[cfg(test)] #[test] fn test_exact () -> Usually<()> { + let mut screen = Tui::Layout(XYWH(0, 0, 80, 25)); + assert_eq!("FOOBAR\nKILROY".draw(&mut screen)?, Some(XYWH(0, 0, 6, 2))); + assert_eq!("FOOBAR\nKILROY".exact_w(3).draw(&mut screen)?, Some(XYWH(0, 0, 3, 2))); + assert_eq!("FOOBAR\nKILROY".exact_h(1).draw(&mut screen)?, Some(XYWH(0, 0, 6, 1))); + assert_eq!("FOOBAR\nKILROY".exact_wh(2, 1).draw(&mut screen)?, Some(XYWH(0, 0, 2, 1))); + Ok(()) +} + diff --git a/src/layout/azimuth.rs b/src/layout/azimuth.rs new file mode 100644 index 0000000..ce72b95 --- /dev/null +++ b/src/layout/azimuth.rs @@ -0,0 +1,455 @@ +use crate::*; +use Azimuth::*; + +/// Where is [0, 0] located? +/// +/// ``` +/// use tengri::*; +/// use Azimuth::*; +/// let _ = "".align(NW); +/// ``` +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Debug, Copy, Clone, Default)] pub enum Azimuth { + #[default] C, X, Y, NW, N, NE, E, SE, S, SW, W +} + +pub struct Origin( + pub(crate) Option, + pub(crate) T +); + +impl_draw!(,>|self: Origin, _to: S|{ + todo!() +}); + +/// Something that has `[0, 0]` at a particular point. +pub trait HasOrigin { + fn origin (&self) -> Azimuth; +} + +impl> HasOrigin for T { + fn origin (&self) -> Azimuth { + *self.as_ref() + } +} + +fn_kw_layout!(kw_align |state, output, expr| { + Ok(matches!(expr.head()?, Some("align")).then(||{ + draw(move|output: &mut O|{state.interpret(output, &expr.tail().head())}).align( + eval_enum!("align", output, state, + expr.head().src()?.unwrap_or_default().split("/").skip(1).next(), + expr.tail().head(), + Azimuth { + "c" => C, "x" => X, "y" => Y, + "n" => N, "s" => S, "e" => E, "w" => W, + "nw" => NW, "sw" => SW, "ne" => NE, "se" => SE, + }) + ).draw(output) + }).transpose()?.flatten()) +}); + +impl> CanAlign for T {} + +pub trait CanAlign: Draw + Sized { + fn align (self, azimuth: impl Into>) -> Align { + Align(azimuth.into(), self) + } + fn align_c (self) -> Align { + Align(Some(Azimuth::C), self) + } + fn align_x (self) -> Align { + Align(Some(Azimuth::X), self) + } + fn align_y (self) -> Align { + Align(Some(Azimuth::Y), self) + } + fn align_n (self) -> Align { + Align(Some(Azimuth::N), self) + } + fn align_s (self) -> Align { + Align(Some(Azimuth::S), self) + } + fn align_e (self) -> Align { + Align(Some(Azimuth::E), self) + } + fn align_w (self) -> Align { + Align(Some(Azimuth::W), self) + } + fn align_ne (self) -> Align { + Align(Some(Azimuth::NE), self) + } + fn align_se (self) -> Align { + Align(Some(Azimuth::SE), self) + } + fn align_nw (self) -> Align { + Align(Some(Azimuth::NW), self) + } + fn align_sw (self) -> Align { + Align(Some(Azimuth::SW), self) + } +} + +pub struct Align( + pub(crate) Option, + pub(crate) T, +); + +impl> Draw for Align { + fn draw (&self, to: &mut S) -> Perhaps> { + let area = to.area(); + Ok(if let Some(area) = align::(area, to.size(area, &self.1)?, self.0) { + to.draw(area, &self.1)? + } else { + None + }) + } +} + +fn align ( + area0: XYWH, area: Option>, azimuth: Option +) -> Option> { + area.map(|XYWH(x, y, w, h)|{ + let XYWH(x0, y0, w0, h0) = area0; + match azimuth { + Some(NW) => XYWH(x0, y0, w, h), + Some(N) => XYWH(x0 + w0.minus(w) / 2.into(), y0, w, h), + Some(NE) => XYWH((x0 + w0).minus(w), y0, w, h), + Some(W) => XYWH(x0, y0 + h0.minus(h) / 2.into(), w, h), + Some(C) => XYWH(x0 + w0.minus(w) / 2.into(), y0 + h0.minus(h) / 2.into(), w, h), + Some(E) => XYWH((x0 + w0).minus(w), y0 + h0.minus(h) / 2.into(), w, h), + Some(SW) => XYWH(x0, (y0 + h0).minus(h), w, h), + Some(S) => XYWH(x0 + w0.minus(w) / 2.into(), (y0 + h0).minus(h), w, h), + Some(SE) => XYWH((x0 + w0).minus(w), (y0 + h0).minus(h), w, h), + Some(X) => XYWH(x0 + w0.minus(w) / 2.into(), y, w, h), + Some(Y) => XYWH(x, y0 + h0.minus(h) / 2.into(), w, h), + None => XYWH(x, y, w, h) + } + }) +} + +fn_kw_layout!(kw_split |state, output, expr| { + let head = expr.head(); + let mut frags = head.src()?.unwrap_or_default().split("/"); + Ok(matches!(frags.next(), Some("bsp")).then(||{ + eval_enum!("bsp", output, state, frags.next(), expr.tail().head()?, Split { + "n" => North, + "s" => South, + "e" => East, + "w" => West, + "a" => Above, + "b" => Below + }).stack( + draw(move|output: &mut O|{ + state.interpret(output, &expr.tail().tail().head()?) + }), + draw(move|output: &mut O|{ + state.interpret(output, &expr.tail().tail().tail().head()?) + }), + ).draw(output) + }).transpose()?.flatten()) +}); + +/// Split along an axis. Direction determines order. +#[cfg_attr(test, derive(Arbitrary))] +#[derive(Copy, Clone, PartialEq, Debug, Default)] pub enum Split { + North, + South, + East, + West, + Above, + #[default] Below +} + +pub struct Pair, B: Draw>(Split, A, B, PhantomData); + +pub fn split , B: Draw> ( + split: Split, a: A, b: B +) -> Pair { + Pair(split, a, b, PhantomData) +} + +impl, B: Draw> Draw for Pair { + fn draw (&self, to: &mut S) -> Drawn { + let Self(split, a, b, ..) = self; + let (area_a, area_b) = stack_areas(split, to, a, b)?; + let (drawn_a, drawn_b) = draw_stacks(split, to, a, area_a, None, b, area_b, None)?; + Ok(stack_drawn(split, drawn_a, drawn_b)) + } +} + +impl Split { + + /// ``` + /// use tengri::*; + /// let _ = Split::Above.stack("", ""); + /// let _ = Split::Below.stack("", ""); + /// let _ = Split::North.stack("", ""); + /// let _ = Split::South.stack("", ""); + /// let _ = Split::East.stack("", ""); + /// let _ = Split::West.stack("", ""); + /// ``` + pub const fn stack , B: Draw> (&self, a: A, b: B) -> impl Draw { + Pair(*self, a, b, PhantomData) + } + + /// ``` + /// use tengri::*; + /// let _ = Split::Above.half("", ""); + /// let _ = Split::Below.half("", ""); + /// let _ = Split::North.half("", ""); + /// let _ = Split::South.half("", ""); + /// let _ = Split::East.half("", ""); + /// let _ = Split::West.half("", ""); + /// ``` + pub const fn half , B: Draw> (&self, a: &A, b: &B) -> impl Draw { + draw(move|to: &mut S|{ + let (area_a, area_b) = to.xywh().split_half(self); + let (origin_a, origin_b) = self.origins(); + let (drawn_a, drawn_b) = draw_stacks(self, to, a, area_a, origin_a, b, area_b, origin_b)?; + Ok(stack_drawn(self, drawn_a, drawn_b)) + }) + } + + /// Newly split areas begin at the center of the split + /// to maintain centeredness in the user's field of view. + /// + /// Use [align] to override that and always start + /// at the top, bottom, etc. + /// + /// ``` + /// /* + /// + /// Split east: Split south: + /// | | | | A | + /// | <-A|B-> | |---------| + /// | | | | B | + /// + /// */ + /// ``` + const fn origins (&self) -> (Azimuth, Azimuth) { + use Azimuth::*; + match self { + Self::South => (S, N), + Self::East => (E, W), + Self::North => (N, S), + Self::West => (W, E), + Self::Above => (C, C), + Self::Below => (C, C), + } + } + + ///// ``` + ///// use tengri::*; + ///// let _ = Split::Below.iter([ + ///// "Leftbar" + ///// .min_w(10).max_w(15).align(Azimuth::NW), + ///// "Rightbar" + ///// .min_w(10).max_w(12).align(Azimuth::NE), + ///// "Center" + ///// .min_w(20).max_w(40).align(Azimuth::C), + ///// ].iter()); + ///// ``` + //pub fn iter > (&self, _: impl Iterator) { + //todo!() + //} + +} + +fn draw_stacks ( + split: &Split, + to: &mut S, + a: impl Draw, + area_a: impl Into>>, + origin_a: impl Into>, + b: impl Draw, + area_b: impl Into>>, + origin_b: impl Into>, +) -> Usually<(Option>, Option>)> { + let draw_a = |to: &mut S|Ok::<_, Box>(if let Some(origin_a) = origin_a.into() { + to.draw(area_a.into(), a.align(origin_a))? + } else { + to.draw(area_a.into(), a)? + }); + let draw_b = |to: &mut S|Ok::<_, Box>(if let Some(origin_b) = origin_b.into() { + to.draw(area_b.into(), b.align(origin_b))? + } else { + to.draw(area_b.into(), b)? + }); + Ok(if matches!(split, Split::Below) { + let drawn_b = draw_b(to)?; + let drawn_a = draw_a(to)?; + (drawn_a, drawn_b) + } else { + (draw_a(to)?, draw_b(to)?) + }) +} + +pub fn stack_areas ( + split: &Split, + to: &mut S, + a: impl Draw, + b: impl Draw, +) -> Usually<(Option>, Option>)> { + let area_a = to.draw(None, a)?; + Ok(match split { + Split::South => ( + area_a, + area_a + .map(|used|to.size(XYWH(to.x(), to.y() + used.h(), to.w(), to.h().minus(used.h())), b)) + .transpose()? + .flatten() + ), + Split::East => ( + area_a, + area_a.map(|used|to.size(XYWH(to.x() + used.w(), to.y(), to.w().minus(used.w()), to.h()), b)) + .transpose()? + .flatten() + ), + Split::North => ( + area_a.map(|used|XYWH(used.x(), (to.y() + to.h()).minus(used.h()), used.w(), used.h())), + if let Some(used) = area_a { + to.size(XYWH(to.x(), to.y(), to.w(), to.h().minus(used.h())), b)? + } else { + to.size(None, b)? + .map(|area_b|XYWH(area_b.x(), area_b.y() + area_b.h(), area_b.w(), area_b.h())) + } + ), + Split::West => ( + area_a.map(|used|XYWH((to.x() + to.w()).minus(used.w()), used.y(), used.w(), used.h())), + if let Some(used) = area_a { + to.size(XYWH(to.x(), to.y(), to.w().minus(used.w()), to.h()), b)? + } else { + to.size(None, b)? + .map(|area_b|XYWH(area_b.x() + area_b.w(), area_b.y(), area_b.w(), area_b.h())) + } + ), + Split::Above | Split::Below => ( + area_a, + to.size(None, b)?, + ), + }) +} + +fn stack_drawn ( + split: &Split, + drawn_a: Option>, + drawn_b: Option>, +) -> Option> { + if let (Some(XYWH(xa, ya, wa, ha)), Some(XYWH(xb, yb, wb, hb))) = (drawn_a, drawn_b) { + match split { + Split::South => Some(XYWH(xa.min(xb), ya, wa.max(wb), ha + hb)), + Split::East => Some(XYWH(xa, ya.min(yb), wa + wb, ha.max(hb))), + Split::North => Some(XYWH(xa.min(xb), yb, wa.max(wb), ha + hb)), + Split::West => Some(XYWH(xb, ya.min(yb), wa + wb, ha.max(hb))), + Split::Above | Split::Below => + Some(XYWH(xa.min(xb), ya.min(yb), wa.max(wb), ha.max(hb))), + } + } else if let Some(a) = drawn_a { + Some(a) + } else if let Some(b) = drawn_b { + Some(b) + } else { + None + } +} + +#[macro_export] macro_rules! north { + ($head:expr $(,)?) => { $head }; + ($head:expr, $($tail:expr),* $(,)?) => { north($head, north!($($tail,)*)) }; +} + +#[macro_export] macro_rules! south { + ($head:expr $(,)?) => { $head }; + ($head:expr, $($tail:expr),* $(,)?) => { south($head, south!($($tail,)*)) }; +} + +#[macro_export] macro_rules! east { + ($head:expr $(,)?) => { $head }; + ($head:expr, $($tail:expr),* $(,)?) => { east($head, east!($($tail,)*)) }; +} + +#[macro_export] macro_rules! west { + ($head:expr $(,)?) => { $head }; + ($head:expr $(, $tail:expr)* $(,)?) => { west($head, west!($($tail,)*)) }; +} + +#[macro_export] macro_rules! above { + ($head:expr $(,)?) => { $head }; + ($head:expr $(, $tail:expr)* $(,)?) => { above($head, above!($($tail,)*)) }; +} + +#[macro_export] macro_rules! below { + ($head:expr $(,)?) => { $head }; + ($head:expr, $($tail:expr),* $(,)?) => { below($head, below!($($tail,)*)) }; +} + +pub const fn east , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::East, a, b, PhantomData) +} + +pub const fn north , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::North, a, b, PhantomData) +} + +pub const fn west , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::West, a, b, PhantomData) +} + +pub const fn south , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::South, a, b, PhantomData) +} + +pub const fn above , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::Above, a, b, PhantomData) +} + +pub const fn below , B: Draw> (a: A, b: B) -> impl Draw { + Pair(Split::Below, a, b, PhantomData) +} + +#[cfg(test)] #[test] fn test_stack_areas () -> Usually<()> { + let area = XYWH(0u16, 0, 80, 25); + + assert_eq!(stack_areas(&Split::East, &mut Tui::Layout(area), &"foo", &"bar")?, ( + Some(XYWH(0u16, 0, 3, 1)), + Some(XYWH(3u16, 0, 3, 1)), + )); + + assert_eq!(stack_areas(&Split::South, &mut Tui::Layout(area), &"foo", &"bar")?, ( + Some(XYWH(0u16, 0, 3, 1)), + Some(XYWH(0u16, 1, 3, 1)), + )); + + Ok(()) +} + +#[cfg(test)] #[test] fn test_split_stack () -> Usually<()> { + use Split::*; + fn size_of , B: Draw> (stack: &Pair) -> Perhaps> { + Tui::Layout(XYWH(0, 0, 80, 25)).size(None, stack) + } + assert_eq!(size_of(&split(East, "foo", "bar"))?, + Some(XYWH(0, 0, 6, 1))); + assert_eq!(size_of(&split(South, "foo", "bar"))?, + Some(XYWH(0, 0, 3, 2))); + assert_eq!(size_of(&split(South, split(East, "foo", "bar"), "baz"))?, + Some(XYWH(0, 0, 6, 2))); + assert_eq!(size_of(&split(East, split(South, "foo", "bar"), "baz"))?, + Some(XYWH(0, 0, 6, 2))); + return Ok(()); +} + +#[cfg(test)] #[test] fn test_align () -> Usually<()> { + let mut screen = Tui::Layout(XYWH(0, 0, 80, 25)); + assert_eq!("FOOBAR\nKILROY".draw(&mut screen)?, Some(XYWH(0, 0, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_nw().draw(&mut screen)?, Some(XYWH(0, 0, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_n().draw(&mut screen)?, Some(XYWH(37, 0, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_ne().draw(&mut screen)?, Some(XYWH(74, 0, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_w().draw(&mut screen)?, Some(XYWH(0, 11, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_c().draw(&mut screen)?, Some(XYWH(37, 11, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_e().draw(&mut screen)?, Some(XYWH(74, 11, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_sw().draw(&mut screen)?, Some(XYWH(0, 23, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_s().draw(&mut screen)?, Some(XYWH(37, 23, 6, 2))); + assert_eq!("FOOBAR\nKILROY".align_se().draw(&mut screen)?, Some(XYWH(74, 23, 6, 2))); + Ok(()) +} diff --git a/src/layout/cond.rs b/src/layout/cond.rs new file mode 100644 index 0000000..a95d8f1 --- /dev/null +++ b/src/layout/cond.rs @@ -0,0 +1,46 @@ +use crate::*; + +fn_kw_layout!(kw_when |state, output, expr| { + Ok(matches!(expr.head()?, Some("when")).then(||{ + when(state.namespace(expr.tail().head())?.unwrap(), + draw(move|output: &mut O|{state.interpret(output, &expr.tail().tail().head())}) + ).draw(output) + }).transpose()?.flatten()) +}); + +/// Only render when condition is true. +/// +/// ``` +/// # use tengri::*; +/// # fn test () -> impl Draw { +/// when(true, "Yes") +/// # } +/// ``` +pub const fn when (condition: bool, item: impl Draw) -> impl Draw { + draw(move|to: &mut T|if condition { item.draw(to) } else { Ok(Default::default()) }) +} + +fn_kw_layout!(kw_either |state, output, expr| { + Ok(matches!(expr.head()?, Some("either")).then(||{ + either(state.namespace(expr.tail().head()?)?.unwrap(), + draw(move|output: &mut O|{ + state.interpret(output, &expr.tail().tail().head()?) + }), + draw(move|output: &mut O|{ + state.interpret(output, &expr.tail().tail().tail().head()?) + }), + ).draw(output) + }).transpose()?.flatten()) +}); + +/// Render one thing if a condition is true and another false. +/// +/// ``` +/// # use tengri::*; +/// # fn test () -> impl Draw { +/// either(true, "Yes", "No") +/// # } +/// ``` +pub const fn either (condition: bool, a: impl Draw, b: impl Draw) -> impl Draw { + draw(move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) }) +} diff --git a/src/layout/exact.rs b/src/layout/exact.rs deleted file mode 100644 index ef51dc2..0000000 --- a/src/layout/exact.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::*; - -/// Set size of of drawing area. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".exact_w(1); -/// let _ = "".exact_h(1); -/// let _ = "".exact_wh(1, 1); -/// ``` -pub enum Exact, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl , X: Into> + Copy> Draw for Exact { - fn layout (&self, area: XYWH) -> Perhaps> { - Ok(Some(layout_exact(self, area))) - } - fn draw (self, to: &mut S) -> Perhaps> { - let area = layout_exact(&self, to.area()); - let item = match self { Self::W(i, ..) => i, Self::H(i, ..) => i, Self::WH(i, ..) => i, _ => unreachable!() }; - to.clip(area, item) - } -} - -fn layout_exact , X: Into> + Copy> ( - exact: &Exact, area: XYWH -) -> XYWH { - let (w, h): (S::Unit, S::Unit) = match exact { - Exact::W(_, w) => { - let w: Option = (*w).into(); - (w.unwrap_or(area.2), area.3) - }, - Exact::H(_, h) => { - let h: Option = (*h).into(); - (area.2, h.unwrap_or(area.3)) - }, - Exact::WH(_, w, h) => { - let w: Option = (*w).into(); - let h: Option = (*h).into(); - (w.unwrap_or(area.2), w.unwrap_or(area.3)) - }, - _ => unreachable!() - }; - XYWH(area.0, area.1, w, h) -} diff --git a/src/layout/full.rs b/src/layout/full.rs deleted file mode 100644 index 5c2a717..0000000 --- a/src/layout/full.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::*; - -/// Use whole drawing area along one or both axes. -/// -/// ``` -/// # fn doctest_layout_full () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(0u16, 0, 80, 25); -/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// assert_eq!("1".full_w().layout(area)?, Some(XYWH(0u16, 0, 80, 1))); -/// assert_eq!("1".full_h().layout(area)?, Some(XYWH(0u16, 0, 1, 25))); -/// assert_eq!("1".full_wh().layout(area)?, Some(XYWH(0u16, 0, 80, 25))); -/// # Ok(()) } -/// ``` -pub enum Full> { - __(PhantomData), - W(I), - H(I), - WH(I), -} - -impl_draw!(,>|self: Full, to: T|{ - let XYWH(x0, y0, w0, h0) = to.area(); - match self { - Self::W(item) => if let Some(XYWH(_, y, _, h)) = item.layout(to.area())? { - to.clip(XYWH(x0, y, w0, h), item) - } else { - Ok(None) - }, - Self::H(item) => if let Some(XYWH(x, _, w, _)) = item.layout(to.area())? { - to.clip(XYWH(x, y0, w, h0), item) - } else { - Ok(None) - }, - Self::WH(item) => if let Some(XYWH(..)) = item.layout(to.area())? { - to.clip(XYWH(x0, y0, w0, h0), item) - } else { - Ok(None) - }, - _ => unreachable!(), - } -}); diff --git a/src/layout/max.rs b/src/layout/max.rs deleted file mode 100644 index 9b3f40a..0000000 --- a/src/layout/max.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::*; - -/// Set maximum size of of drawing area. -/// -/// ``` -/// # fn doctest_layout_max () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(1u16, 1, 80, 25); -/// assert_eq!("12345".max_w(1).layout(area)?, Some(XYWH(1u16, 1, 1, 1))); -/// assert_eq!("12345".max_h(1).layout(area)?, Some(XYWH(1u16, 1, 1, 1))); -/// assert_eq!("12345".max_wh(1, 1).layout(area)?, Some(XYWH(1u16, 1, 5, 1))); -/// # Ok(()) } -/// ``` -pub enum Max, X: Into> + Copy> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl, X: Into> + Copy> Draw for Max { - fn layout (&self, area: XYWH) -> Perhaps> { - Ok(Some(match self { - Self::W(_, max_w) => XYWH( - area.0, area.1, (*max_w).into().map(|max|max.min(area.2)).unwrap_or(area.2), - area.3), - Self::H(_, max_h) => XYWH( - area.0, area.1, area.2, - (*max_h).into().map(|max|max.min(area.3)).unwrap_or(area.3)), - Self::WH(_, max_w, max_h) => XYWH( - area.0, area.1, (*max_w).into().map(|max|max.min(area.2)).unwrap_or(area.2), - (*max_h).into().map(|max|max.min(area.3)).unwrap_or(area.3)), - _ => return Ok(None) - })) - } - fn draw (self, to: &mut T) -> Perhaps> { - let area: XYWH = to.area(); - let (item, area) = match self { - Self::W(item, max_w) => (item, XYWH( - area.0, area.1, max_w.into().map(|max|max.min(area.2)).unwrap_or(area.2), - area.3 - )), - Self::H(item, max_h) => (item, XYWH( - area.0, area.1, area.2, - max_h.into().map(|max|max.min(area.3)).unwrap_or(area.3) - )), - Self::WH(item, max_w, max_h) => (item, XYWH( - area.0, area.1, max_w.into().map(|max|max.min(area.2)).unwrap_or(area.2), - max_h.into().map(|max|max.min(area.3)).unwrap_or(area.3), - )), - _ => return Ok(None) - }; - to.clip(area, item) - } -} diff --git a/src/layout/min.rs b/src/layout/min.rs deleted file mode 100644 index 70c6549..0000000 --- a/src/layout/min.rs +++ /dev/null @@ -1,40 +0,0 @@ -use crate::*; - -/// Only draw content if area is above a certain size. -/// -/// ``` -/// # fn doctest_layout_min () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(1u16, 1, 80, 25); -/// assert_eq!("1".min_w(5).layout(area)?, Some(XYWH(1u16, 1, 5, 1))); -/// assert_eq!("1".min_h(5).layout(area)?, Some(XYWH(1u16, 1, 1, 5))); -/// assert_eq!("1".min_wh(5, 5).layout(area)?, Some(XYWH(1u16, 1, 5, 5))); -/// assert_eq!("123456".min_w(5).layout(area)?, Some(XYWH(1u16, 1, 6, 1))); -/// # Ok(()) } -/// ``` -pub enum Min, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Min, to: T|{ - match self { - Self::__(_) => unreachable!(), - Self::W(item, w1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - let w = w1.into().map(|w1|w.max(w1)).unwrap_or(w); - to.clip(XYWH(x, y, w, h), item) - }, - Self::H(item, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - let h = h1.into().map(|h1|h.max(h1)).unwrap_or(h); - to.clip(XYWH(x, y, w, h), item) - }, - Self::WH(item, w1, h1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - let w = w1.into().map(|w1|w.max(w1)).unwrap_or(w); - let h = h1.into().map(|h1|h.max(h1)).unwrap_or(h); - to.clip(XYWH(x, y, w, h), item) - }, - _ => Ok(None) - } -}); diff --git a/src/layout/origin.rs b/src/layout/origin.rs deleted file mode 100644 index e5aaff9..0000000 --- a/src/layout/origin.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::*; - -pub struct Origin( - pub(crate) Option, - pub(crate) T -); - -impl_draw!(,>|self: Origin, _to: S|{ - todo!() -}); - -/// Something that has `[0, 0]` at a particular point. -pub trait HasOrigin { - fn origin (&self) -> Azimuth; -} - -impl> HasOrigin for T { - fn origin (&self) -> Azimuth { - *self.as_ref() - } -} diff --git a/src/layout/pad.rs b/src/layout/pad.rs deleted file mode 100644 index 6ac2b4a..0000000 --- a/src/layout/pad.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::*; - -/// Define inner drawing area. -/// -/// ``` -/// use tengri::Layout; -/// let _ = "".pad_w(1); -/// let _ = "".pad_h(1); -/// let _ = "".pad_wh(1, 1); -/// ``` -pub enum Pad, X: Into>> { - __(PhantomData), - W(I, X), - H(I, X), - WH(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Pad, to: T|{ - let area = to.area(); - let (item, area) = match self { - Self::W(item, w1) => { - let w1 = w1.into().unwrap_or(T::Unit::zero()); - (item, XYWH(area.0 + w1, area.1, area.2.minus(w1 + w1), area.3)) - }, - Self::H(item, h1) => { - let h1 = h1.into().unwrap_or(T::Unit::zero()); - (item, XYWH(area.0, area.1 + h1, area.2, area.3.minus(h1 + h1))) - }, - Self::WH(item, w1, h1) => { - let w1 = w1.into().unwrap_or(T::Unit::zero()); - let h1 = h1.into().unwrap_or(T::Unit::zero()); - (item, XYWH(area.0 + w1, area.1 + h1, area.2.minus(w1 + w1), area.3.minus(h1 + h1))) - }, - _ => return Ok(None) - }; - item.draw(to) -}); diff --git a/src/layout/pull.rs b/src/layout/pull.rs deleted file mode 100644 index 287a998..0000000 --- a/src/layout/pull.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::*; - -/// Move content in the negative direction of one or both axes. -/// -/// ``` -/// # fn doctest_layout_pull () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(1u16, 1, 80, 25); -/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// assert_eq!("1".pull_x(1).layout(area)?, Some(XYWH(0u16, 1, 1, 1))); -/// assert_eq!("1".pull_y(1).layout(area)?, Some(XYWH(1u16, 0, 1, 1))); -/// assert_eq!("1".pull_xy(1, 1).layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// # Ok(()) } -/// ``` -pub enum Pull, X: Into>> { - __(PhantomData), - X(I, X), - Y(I, X), - XY(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Pull, _to: T|{ - todo!() -}); diff --git a/src/layout/push.rs b/src/layout/push.rs deleted file mode 100644 index 0907ad1..0000000 --- a/src/layout/push.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::*; - -/// Move content in the positive direction of one or both axes. -/// -/// ``` -/// # fn doctest_layout_push () -> Result<(), Box> { -/// use tengri::{Layout, Draw, XYWH}; -/// let area = XYWH(0u16, 0, 80, 25); -/// assert_eq!("1".layout(area)?, Some(XYWH(0u16, 0, 1, 1))); -/// assert_eq!("1".push_x(1).layout(area)?, Some(XYWH(1u16, 0, 1, 1))); -/// assert_eq!("1".push_y(1).layout(area)?, Some(XYWH(0u16, 1, 1, 1))); -/// assert_eq!("1".push_xy(1, 1).layout(area)?, Some(XYWH(1u16, 1, 1, 1))); -/// # Ok(()) } -/// ``` -pub enum Push, X: Into>> { - __(PhantomData), - X(I, X), - Y(I, X), - XY(I, X, X), -} - -impl_draw!(, X: Into>,>|self: Push, to: T|{ - match self { - Self::__(_) => unreachable!(), - Self::X(item, x1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH( - x + x1.into().unwrap_or_default(), y, w, h - ), item) - }, - Self::Y(item, y1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH( - x, y + y1.into().unwrap_or_default(), w, h - ), item) - }, - Self::XY(item, x1, y1) if let Some(XYWH(x, y, w, h)) = item.layout(to.area())? => { - to.clip(XYWH( - x + x1.into().unwrap_or_default(), y + y1.into().unwrap_or_default(), w, h - ), item) - }, - _ => Ok(None) - } -}); diff --git a/src/layout/split.rs b/src/layout/split.rs deleted file mode 100644 index 77120c3..0000000 --- a/src/layout/split.rs +++ /dev/null @@ -1,315 +0,0 @@ -use super::*; - -/// Split along an axis. Direction determines order. -#[cfg_attr(test, derive(Arbitrary))] -#[derive(Copy, Clone, PartialEq, Debug, Default)] pub enum Split { - North, - South, - East, - West, - Above, - #[default] Below -} - -pub struct Pair, B: Draw>(Split, A, B, PhantomData); - -pub fn split , B: Draw> ( - split: Split, a: A, b: B -) -> Pair { - Pair(split, a, b, PhantomData) -} - -impl, B: Draw> Draw for Pair { - fn layout (&self, to: XYWH) -> Drawn { - let Self(split, a, b, ..) = self; - let (area_a, area_b) = stack_areas(split, to, a, b)?; - Ok(stack_drawn(split, area_a, area_b)) - } - fn draw (self, to: &mut S) -> Drawn { - let Self(ref split, a, b, ..) = self; - let (area_a, area_b) = stack_areas(split, to.area(), &a, &b)?; - let (drawn_a, drawn_b) = draw_stacks(split, to, a, area_a, None, b, area_b, None)?; - Ok(stack_drawn(split, drawn_a, drawn_b)) - } -} - -impl Split { - - /// ``` - /// use tengri::*; - /// let _ = Split::Above.stack("", ""); - /// let _ = Split::Below.stack("", ""); - /// let _ = Split::North.stack("", ""); - /// let _ = Split::South.stack("", ""); - /// let _ = Split::East.stack("", ""); - /// let _ = Split::West.stack("", ""); - /// ``` - pub const fn stack , B: Draw> (&self, a: A, b: B) -> impl Draw { - Pair(*self, a, b, PhantomData) - } - - /// ``` - /// use tengri::*; - /// let _ = Split::Above.half("", ""); - /// let _ = Split::Below.half("", ""); - /// let _ = Split::North.half("", ""); - /// let _ = Split::South.half("", ""); - /// let _ = Split::East.half("", ""); - /// let _ = Split::West.half("", ""); - /// ``` - pub const fn half , B: Draw> (&self, a: A, b: B) -> impl Draw { - draw(move|to: &mut S|{ - let (area_a, area_b) = to.xywh().split_half(self); - let (origin_a, origin_b) = self.origins(); - let (drawn_a, drawn_b) = draw_stacks(self, to, a, area_a, origin_a, b, area_b, origin_b)?; - Ok(stack_drawn(self, drawn_a, drawn_b)) - }) - } - - /// Newly split areas begin at the center of the split - /// to maintain centeredness in the user's field of view. - /// - /// Use [align] to override that and always start - /// at the top, bottom, etc. - /// - /// ``` - /// /* - /// - /// Split east: Split south: - /// | | | | A | - /// | <-A|B-> | |---------| - /// | | | | B | - /// - /// */ - /// ``` - const fn origins (&self) -> (Azimuth, Azimuth) { - use Azimuth::*; - match self { - Self::South => (S, N), - Self::East => (E, W), - Self::North => (N, S), - Self::West => (W, E), - Self::Above => (C, C), - Self::Below => (C, C), - } - } - - ///// ``` - ///// use tengri::*; - ///// let _ = Split::Below.iter([ - ///// "Leftbar" - ///// .min_w(10).max_w(15).align(Azimuth::NW), - ///// "Rightbar" - ///// .min_w(10).max_w(12).align(Azimuth::NE), - ///// "Center" - ///// .min_w(20).max_w(40).align(Azimuth::C), - ///// ].iter()); - ///// ``` - //pub fn iter > (&self, _: impl Iterator) { - //todo!() - //} - -} - -fn draw_stacks ( - split: &Split, - to: &mut S, - a: impl Draw, - area_a: impl Into>>, - origin_a: impl Into>, - b: impl Draw, - area_b: impl Into>>, - origin_b: impl Into>, -) -> Usually<(Option>, Option>)> { - let draw_a = |to: &mut S|Ok::<_, Box>(if let Some(origin_a) = origin_a.into() { - to.clip(area_a.into(), a.align(origin_a))? - } else { - to.clip(area_a.into(), a)? - }); - let draw_b = |to: &mut S|Ok::<_, Box>(if let Some(origin_b) = origin_b.into() { - to.clip(area_b.into(), b.align(origin_b))? - } else { - to.clip(area_b.into(), b)? - }); - Ok(if matches!(split, Split::Below) { - let drawn_b = draw_b(to)?; - let drawn_a = draw_a(to)?; - (drawn_a, drawn_b) - } else { - (draw_a(to)?, draw_b(to)?) - }) -} - -pub fn stack_areas ( - split: &Split, - area: XYWH, - a: &impl Draw, - b: &impl Draw, -) -> Usually<(Option>, Option>)> { - let area_a = a.layout(area)?; - Ok(match split { - Split::South => ( - area_a, - if let Some(used) = area_a { - b.layout(XYWH(area.x(), area.y() + used.h(), area.w(), area.h().minus(used.h())))? - } else { - None - } - ), - Split::East => ( - area_a, - if let Some(used) = area_a { - b.layout(XYWH(area.x() + used.w(), area.y(), area.w().minus(used.w()), area.h()))? - } else { - None - } - ), - Split::North => ( - if let Some(used) = area_a { - Some(XYWH(used.x(), (area.y() + area.h()).minus(used.h()), used.w(), used.h())) - } else { - None - }, - if let Some(used) = area_a { - b.layout(XYWH(area.x(), area.y(), area.w(), area.h().minus(used.h())))? - } else { - b.layout(area)?.map(|area_b|XYWH( - area_b.x(), area_b.y() + area_b.h(), area_b.w(), area_b.h() - )) - } - ), - Split::West => ( - if let Some(used) = area_a { - Some(XYWH((area.x() + area.w()).minus(used.w()), used.y(), used.w(), used.h())) - } else { - None - }, - if let Some(used) = area_a { - b.layout(XYWH(area.x(), area.y(), area.w().minus(used.w()), area.h()))? - } else { - b.layout(area)?.map(|area_b|XYWH( - area_b.x() + area_b.w(), area_b.y(), area_b.w(), area_b.h() - )) - } - ), - Split::Above | Split::Below => ( - area_a, - b.layout(area)?, - ), - }) -} - -fn stack_drawn ( - split: &Split, - drawn_a: Option>, - drawn_b: Option>, -) -> Option> { - if let (Some(XYWH(xa, ya, wa, ha)), Some(XYWH(xb, yb, wb, hb))) = (drawn_a, drawn_b) { - match split { - Split::South => Some(XYWH(xa.min(xb), ya, wa.max(wb), ha + hb)), - Split::East => Some(XYWH(xa, ya.min(yb), wa + wb, ha.max(hb))), - Split::North => Some(XYWH(xa.min(xb), yb, wa.max(wb), ha + hb)), - Split::West => Some(XYWH(xb, ya.min(yb), wa + wb, ha.max(hb))), - Split::Above | Split::Below => - Some(XYWH(xa.min(xb), ya.min(yb), wa.max(wb), ha.max(hb))), - } - } else if let Some(a) = drawn_a { - Some(a) - } else if let Some(b) = drawn_b { - Some(b) - } else { - None - } -} - -#[macro_export] macro_rules! north { - ($head:expr $(,)?) => { $head }; - ($head:expr, $($tail:expr),* $(,)?) => { north($head, north!($($tail,)*)) }; -} - -#[macro_export] macro_rules! south { - ($head:expr $(,)?) => { $head }; - ($head:expr, $($tail:expr),* $(,)?) => { south($head, south!($($tail,)*)) }; -} - -#[macro_export] macro_rules! east { - ($head:expr $(,)?) => { $head }; - ($head:expr, $($tail:expr),* $(,)?) => { east($head, east!($($tail,)*)) }; -} - -#[macro_export] macro_rules! west { - ($head:expr $(,)?) => { $head }; - ($head:expr $(, $tail:expr)* $(,)?) => { west($head, west!($($tail,)*)) }; -} - -#[macro_export] macro_rules! above { - ($head:expr $(,)?) => { $head }; - ($head:expr $(, $tail:expr)* $(,)?) => { above($head, above!($($tail,)*)) }; -} - -#[macro_export] macro_rules! below { - ($head:expr $(,)?) => { $head }; - ($head:expr, $($tail:expr),* $(,)?) => { below($head, below!($($tail,)*)) }; -} - -pub const fn east , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::East, a, b, PhantomData) -} - -pub const fn north , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::North, a, b, PhantomData) -} - -pub const fn west , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::West, a, b, PhantomData) -} - -pub const fn south , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::South, a, b, PhantomData) -} - -pub const fn above , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::Above, a, b, PhantomData) -} - -pub const fn below , B: Draw> (a: A, b: B) -> impl Draw { - Pair(Split::Below, a, b, PhantomData) -} - -#[cfg(test)] mod test { - use crate::*; - - #[test] fn test_stack_areas () -> Usually<()> { - let area = XYWH(0u16, 0, 80, 25); - - assert_eq!(stack_areas(&Split::East, area, &"foo", &"bar")?, ( - Some(XYWH(0u16, 0, 3, 1)), - Some(XYWH(3u16, 0, 3, 1)), - )); - - assert_eq!(stack_areas(&Split::South, area, &"foo", &"bar")?, ( - Some(XYWH(0u16, 0, 3, 1)), - Some(XYWH(0u16, 1, 3, 1)), - )); - - Ok(()) - } - - #[test] fn test_split_stack () -> Usually<()> { - use tengri::{*, Split::*}; - - let stack = split(East, "foo", "bar"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 6, 1))); - - let stack = split(South, "foo", "bar"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 3, 2))); - - let stack = split(South, split(East, "foo", "bar"), "baz"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 6, 2))); - - let stack = split(East, split(South, "foo", "bar"), "baz"); - assert_eq!(stack.layout(XYWH(0, 0, 80, 25))?, Some(XYWH(0, 0, 6, 2))); - - Ok(()) - } -} diff --git a/src/lib.rs b/src/lib.rs index c706bc5..036b573 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,6 @@ pub extern crate unicode_width; #[cfg(test)] pub(crate) use proptest_derive::Arbitrary; pub(crate) use ::{ - atomic_float::AtomicF64, std::fmt::{Debug, Display}, std::ops::{Add, Sub, Mul, Div}, std::sync::{Arc, RwLock}, @@ -30,8 +29,10 @@ pub(crate) use ::{ std::marker::PhantomData }; +#[cfg(all(feature = "draw", feature = "eval"))] macro_rules! fn_kw_layout { ($name:ident |$state:ident, $output: ident, $expr:ident| $body:block) => { + #[cfg(all(feature = "draw", feature = "eval"))] pub fn $name ( $state: &S, $output: &mut O, $expr: &str ) -> Perhaps> where @@ -43,8 +44,10 @@ macro_rules! fn_kw_layout { } } +#[cfg(all(feature = "draw", feature = "eval", feature = "term"))] macro_rules! fn_kw_layout_tui { ($name:ident |$state:ident, $output: ident, $expr:ident| $body:block) => { + #[cfg(all(feature = "draw", feature = "eval", feature = "term"))] pub fn $name ( $state: &S, $output: &mut Tui, $expr: &str ) -> Perhaps> where @@ -346,7 +349,7 @@ macro_rules! eval_xy ( #[cfg(feature = "sing")] pub use self::sing::*; #[cfg(feature = "sing")] mod sing { use crate::{*, time::PerfModel}; - pub use ::jack::{*, contrib::{*, ClosureProcessHandler}}; + pub use ::jack::{*, contrib::{TimebaseInfo, ClosureProcessHandler, Position as JackPosition}}; pub use ::midly::{Smf, TrackEventKind, MidiMessage, Error as MidiError, num::*, live::*}; use ConnectName::*; use ConnectScope::*; @@ -1362,12 +1365,12 @@ macro_rules! eval_xy ( /// fn area (&self) -> XYWH { /// Default::default() /// } - /// fn clip ( + /// fn draw ( /// &mut self, /// area: impl Into>>, - /// draw: impl Draw + /// draw: Draw /// ) -> Perhaps> { - /// draw.to(self) + /// draw.draw(self) /// } /// } /// @@ -1380,8 +1383,20 @@ macro_rules! eval_xy ( type Unit: Coord; /// Get current clipping area fn area (&self) -> XYWH; - /// Set clipping area - fn clip ( + /// Set current clipping area + fn clip ( + &mut self, + area: impl Into>>, + draw: &impl Fn(&mut Self)->T + ) -> T; + /// Determine used area without drawing + fn size ( + &mut self, + area: impl Into>>, + draw: impl Draw + ) -> Perhaps>; + /// Draw + fn draw ( &mut self, area: impl Into>>, draw: impl Draw @@ -1401,12 +1416,12 @@ macro_rules! eval_xy ( ($(<$($T:ident: $Trait:path,)+>)?| $self:ident:$Self:path, $to:ident:$To:ty |$draw:block)=>{ impl$(<$($T:$Trait),+>)? Draw<$To> for $Self { - fn draw ($self, $to: &mut $To) -> Perhaps::Unit>> $draw + fn draw (&$self, $to: &mut $To) -> Perhaps::Unit>> $draw } }; ($(<$($T:ident: $Trait:path,)+>)?| $self:ident:$Self:ty, $to:ident:$To:ty |$draw:block)=>{ impl$(<$($T:$Trait),+>)? Draw<$To> for $Self { - fn draw ($self, $to: &mut $To) -> Perhaps::Unit>> $draw + fn draw (&$self, $to: &mut $To) -> Perhaps::Unit>> $draw } } ); @@ -1426,16 +1441,13 @@ macro_rules! eval_xy ( /// use tengri::*; /// struct MyWidget(bool); /// impl Draw for MyWidget { - /// fn draw (self, to: &mut Tui) -> Perhaps> { + /// fn draw (&self, to: &mut Tui) -> Perhaps> { /// todo!("your draw logic") /// } /// } /// ``` pub trait Draw { - fn draw (self, to: &mut S) -> Drawn; - fn layout (&self, area: XYWH) -> Drawn { - Ok(Some(area)) - } + fn draw (&self, to: &mut S) -> Drawn; } /// Emit a [Draw]able. @@ -1474,9 +1486,9 @@ macro_rules! eval_xy ( /// Because we can't implement [Draw] for `F: FnOnce...` without conflicts. pub struct DrawThunk(pub F, std::marker::PhantomData); - implPerhaps>> Draw for DrawThunk { - fn draw (self, to: &mut T) -> Perhaps> { - (self.0)(to) + implPerhaps>> Draw for DrawThunk { + fn draw (&self, to: &mut T) -> Perhaps> { + to.clip(None, &self.0) } } @@ -1486,66 +1498,48 @@ macro_rules! eval_xy ( /// # use tengri::*; /// let _ = draw(|to: &mut Tui|Ok(Some(to.area()))); // draws nothing /// ``` - pub const fn draw Perhaps>> ( + pub const fn draw Perhaps>> ( item: F ) -> DrawThunk { DrawThunk(item, std::marker::PhantomData) } - /// Only render when condition is true. - /// - /// ``` - /// # use tengri::*; - /// # fn test () -> impl Draw { - /// when(true, "Yes") - /// # } - /// ``` - pub const fn when (condition: bool, item: impl Draw) -> impl Draw { - draw(move|to: &mut T|if condition { item.draw(to) } else { Ok(Default::default()) }) - } - - /// Render one thing if a condition is true and another false. - /// - /// ``` - /// # use tengri::*; - /// # fn test () -> impl Draw { - /// either(true, "Yes", "No") - /// # } - /// ``` - pub const fn either (condition: bool, a: impl Draw, b: impl Draw) -> impl Draw { - draw(move|to: &mut T|if condition { a.draw(to) } else { b.draw(to) }) - } - pub type Drawn = Perhaps>; impl Draw for () { - fn draw (self, _: &mut S) -> Drawn { + fn draw (&self, _: &mut S) -> Drawn { Ok(None) } } impl_draw!(,>|self: Option, to: S|{ - self.map(|it|it.draw(to)).transpose().map(Option::unwrap_or_default) + self.as_ref().map(|it|it.draw(to)).transpose().map(Option::unwrap_or_default) }); //impl> Draw for RwLock { - //fn draw (self, __: &mut S) -> Drawn { + //fn draw (&self, __: &mut S) -> Drawn { //todo!() //} //} //impl> Draw for Arc { - //fn draw (self, __: &mut T) -> Perhaps> { + //fn draw (&self, __: &mut T) -> Perhaps> { //todo!() //} //} - impl> Draw for &V { - fn draw (self, to: &mut T) -> Perhaps> { - self.view().draw(to) + impl> Draw for &V { + fn draw (&self, to: &mut T) -> Perhaps> { + (*self).draw(to) } } + //impl> Draw for &V { + //fn draw (&self, to: &mut T) -> Perhaps> { + //self.view().draw(to) + //} + //} + pub trait Xy { fn x (&self) -> N; fn y (&self) -> N; @@ -1750,232 +1744,7 @@ macro_rules! eval_xy ( } #[cfg(feature = "draw")] pub use self::layout::*; -#[cfg(feature = "draw")] mod layout { - use crate::*; - - impl> Layout for T {} - - pub trait Layout: Draw + Sized { - fn full_w (self) -> impl Draw { - Full::W(self) - } - fn full_h (self) -> impl Draw { - Full::H(self) - } - fn full_wh (self) -> impl Draw { - Full::WH(self) - } - - /// (bsp/e (exact/w 10 "Hello") "World") - fn exact_w >> (self, x: N) -> impl Draw { - Exact::W(self, x.into()) - } - /// (bsp/s (exact/h 10 "Hello") "World") - fn exact_h >> (self, y: N) -> impl Draw { - Exact::H(self, y.into()) - } - /// (exact/wh 10 2 "Hello World") - fn exact_wh >> (self, x: N, y: N) -> impl Draw { - Exact::WH(self, x.into(), y.into()) - } - - fn min_w >> (self, x: N) -> impl Draw { - Min::W(self, x.into()) - } - fn min_h >> (self, y: N) -> impl Draw { - Min::H(self, y.into()) - } - fn min_wh >> (self, x: N, y: N) -> impl Draw { - Min::WH(self, x.into(), y.into()) - } - - fn max_w >> (self, x: N) -> impl Draw { - Max::W(self, x.into()) - } - fn max_h >> (self, y: N) -> impl Draw { - Max::H(self, y.into()) - } - fn max_wh >> (self, x: N, y: N) -> impl Draw { - Max::WH(self, x.into(), y.into()) - } - - fn pad_w >> (self, x: N) -> impl Draw { - Pad::W(self, x.into()) - } - fn pad_h >> (self, y: N) -> impl Draw { - Pad::H(self, y.into()) - } - fn pad_wh >> (self, x: N, y: N) -> impl Draw { - Pad::WH(self, x.into(), y.into()) - } - - fn pull_x >> (self, x: N) -> impl Draw { - Pull::X(self, x.into()) - } - fn pull_y >> (self, y: N) -> impl Draw { - Pull::Y(self, y.into()) - } - fn pull_xy >> (self, x: N, y: N) -> impl Draw { - Pull::XY(self, x.into(), y.into()) - } - - fn push_x >> (self, x: N) -> impl Draw { - Push::X(self, x.into()) - } - fn push_y >> (self, y: N) -> impl Draw { - Push::Y(self, y.into()) - } - fn push_xy >> (self, x: N, y: N) -> impl Draw { - Push::XY(self, x.into(), y.into()) - } - - fn align (self, azimuth: impl Into>) -> Align { - Align(azimuth.into(), self) - } - fn align_c (self) -> Align { - Align(Some(Azimuth::C), self) - } - fn align_x (self) -> Align { - Align(Some(Azimuth::X), self) - } - fn align_y (self) -> Align { - Align(Some(Azimuth::Y), self) - } - - fn align_n (self) -> impl Draw { - Align(Some(Azimuth::N), self) - } - fn align_s (self) -> impl Draw { - Align(Some(Azimuth::S), self) - } - fn align_e (self) -> impl Draw { - Align(Some(Azimuth::E), self) - } - fn align_w (self) -> impl Draw { - Align(Some(Azimuth::W), self) - } - - fn align_ne (self) -> impl Draw { - Align(Some(Azimuth::NE), self) - } - fn align_se (self) -> impl Draw { - Align(Some(Azimuth::SE), self) - } - fn align_nw (self) -> impl Draw { - Align(Some(Azimuth::NW), self) - } - fn align_sw (self) -> impl Draw { - Align(Some(Azimuth::SW), self) - } - - fn origin (self, azimuth: impl Into>) -> impl Draw { - Origin(azimuth.into(), self) - } - fn origin_c (self) -> impl Draw { - Origin(Some(Azimuth::C), self) - } - fn origin_x (self) -> impl Draw { - Origin(Some(Azimuth::X), self) - } - fn origin_y (self) -> impl Draw { - Origin(Some(Azimuth::Y), self) - } - - fn origin_n (self) -> impl Draw { - Origin(Some(Azimuth::N), self) - } - fn origin_s (self) -> impl Draw { - Origin(Some(Azimuth::S), self) - } - fn origin_e (self) -> impl Draw { - Origin(Some(Azimuth::E), self) - } - fn origin_w (self) -> impl Draw { - Origin(Some(Azimuth::W), self) - } - - fn origin_ne (self) -> impl Draw { - Origin(Some(Azimuth::NE), self) - } - fn origin_se (self) -> impl Draw { - Origin(Some(Azimuth::SE), self) - } - fn origin_nw (self) -> impl Draw { - Origin(Some(Azimuth::NW), self) - } - fn origin_sw (self) -> impl Draw { - Origin(Some(Azimuth::SW), self) - } - } - - /// Where is [0, 0] located? - /// - /// ``` - /// use tengri::*; - /// use Azimuth::*; - /// let _ = "".align(NW); - /// ``` - #[cfg_attr(test, derive(Arbitrary))] - #[derive(Debug, Copy, Clone, Default)] pub enum Azimuth { - #[default] C, X, Y, NW, N, NE, E, SE, S, SW, W - } - - /// Uses [AtomicUsize] to measure size during\ - /// rendering (which is normally read-only). - #[derive(Default, Debug, Clone)] - pub struct Sizer( - /// Width - pub Arc, - /// Height - pub Arc, - ); - - impl Xy for Sizer { - fn x (&self) -> u16 { - self.0.load(Relaxed) as u16 - } - fn y (&self) -> u16 { - self.1.load(Relaxed) as u16 - } - } - impl Wide for Sizer { - fn w (&self) -> u16 { - self.0.load(Relaxed) as u16 - } - } - impl Tall for Sizer { - fn h (&self) -> u16 { - self.1.load(Relaxed) as u16 - } - } - impl PartialEq for Sizer { - fn eq (&self, _: &Self) -> bool { todo!() } - } - - impl Sizer { - pub const fn of (&self, of: impl Draw) -> impl Draw { - draw(move|to: &mut T|{ - let area = of.draw(to)?; - self.0.store(area.map(|a|a.w()).unwrap_or(T::Unit::zero()).into(), Relaxed); - self.1.store(area.map(|a|a.h()).unwrap_or(T::Unit::zero()).into(), Relaxed); - Ok(area) - }) - } - } - - mod align; pub use self::align::*; - mod area; pub use self::area::*; - mod exact; pub use self::exact::*; - mod full; pub use self::full::*; - mod iter; pub use self::iter::*; - mod max; pub use self::max::*; - mod min; pub use self::min::*; - mod origin; pub use self::origin::*; - mod pad; pub use self::pad::*; - mod pull; pub use self::pull::*; - mod push; pub use self::push::*; - mod split; pub use self::split::*; -} +#[cfg(feature = "draw")] mod layout; #[cfg(feature = "draw")] pub use self::color::*; #[cfg(feature = "draw")] mod color { @@ -2166,7 +1935,7 @@ macro_rules! eval_xy ( } } -#[cfg(feature = "draw")] pub use self::text::*; +#[cfg(feature = "text")] pub use self::text::*; #[cfg(feature = "text")] mod text { #![allow(unused)] @@ -2229,651 +1998,6 @@ macro_rules! eval_xy ( } } -#[cfg(feature = "term")] pub use self::term::*; -#[cfg(feature = "term")] mod term { - use crate::*; - use Color::*; - - #[macro_export] macro_rules! tui_app { - ($Struct:ident { $($fields:tt)* }) => { - #[dizzle::namespace(bool)] - #[dizzle::namespace(u8)] - #[dizzle::namespace(u16)] - #[dizzle::namespace(Option)] - #[dizzle::namespace(Color Tui::eval_color_expr)] - #[derive(Debug, Default)] - pub struct $Struct { $($fields)* } - tui_main!($Struct { ..Default::default() }); - } - } - - /// Implement standard [main] entrypoint for TUI apps. - #[macro_export] macro_rules! tui_main { - ($state:expr) => { - pub fn main () -> Usually<()> { - tengri::Tui::setup_panic(); - tengri::Tui::run_main( - ::std::sync::Arc::new(::std::sync::RwLock::new($state)) - ) - } - } - } - - /// Enable TUI output for state struct. - #[macro_export] macro_rules! tui_view { - ($self:ident: $State:ty $body:block) => { - impl tengri::View for $State { - fn view (&$self) -> impl tengri::Draw $body - } - } - } - - #[macro_export] macro_rules! tui_interpret { - ($self:ident: $State:ident, $to:pat, $pat:ident -> $Result:ty { $($body:tt)+ }) => { - impl dizzle::Interpret for $State { - - fn interpret_word <'a> ( - &'a $self, $to: &mut Tui, $pat: &'a impl dizzle::Symbol - ) -> Usually<$Result> { - $($body)+ - } - - fn interpret_expr <'a> ( - &'a self, to: &mut Tui, src: &'a impl tengri::Expression - ) -> Usually<$Result> { - Ok(Some(if let Some(area) = tengri::eval_view(self, to, src)? { - area - } else if let Some(area) = tengri::Tui::eval_view(self, to, src)? { - area - } else { - return Err(format!("App::interpret_expr: unexpected: {src:?}").into()) - })) - } - - } - }; - } - - /// Enable TUI keyboard input for main state struct. - #[macro_export] macro_rules! tui_keys { - ($self:ident:$State:ty,$input:ident $($body:tt)+) => { - impl dizzle::Apply> for $State { - fn apply (&mut $self, $input: &tengri::TuiEvent) -> Usually<()> $($body)+ - } - }; - } - - //use unicode_width::{UnicodeWidthStr, UnicodeWidthChar}; - //use rand::distributions::uniform::UniformSampler; - pub(crate) use ::{ - std::{ - io::{stdout, Write}, - time::Duration, - ops::{Deref, DerefMut}, - }, - ratatui::{ - prelude::{Style, Position, Backend, Color}, - style::{Modifier}, - backend::{CrosstermBackend, ClearType}, - layout::{Size, Rect}, - buffer::{Buffer, Cell}, - crossterm::{ - ExecutableCommand, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode}, - //event::{poll, read, Event, KeyEvent, KeyCode, KeyModifiers, KeyEventKind, KeyEventState}, - } - }, - crossterm::event::read, - }; - - //impl Deref for Tui { type Target = Buffer; fn deref (&self) -> &Buffer { &self.0 } } - //impl DerefMut for Tui { fn deref_mut (&mut self) -> &mut Buffer { &mut self.0 } } - //impl AsMut for Tui { fn as_mut (&mut self) -> &mut Buffer { &mut self.0 } } - impl AsRef> for Tui { - fn as_ref (&self) -> &XYWH { - match self { - Self::Draw(_, area) => area, - Self::Layout(area) => area - } - } - } - - impl Wide for Tui { - fn w (&self) -> u16 { self.as_ref().2 } - } - - impl Tall for Tui { - fn h (&self) -> u16 { self.as_ref().3 } - } - - impl Xy for Tui { - fn x (&self) -> u16 { self.as_ref().0 } - fn y (&self) -> u16 { self.as_ref().1 } - } - - impl HasOrigin for Tui { fn origin (&self) -> Azimuth { Azimuth::NW } } - - /// Terminal output. - pub enum Tui { - Draw ( - /// Ratatui buffer; area is screen size - Buffer, - /// Current draw area - XYWH - ), - Layout ( - XYWH - ) - } - - impl Tui { - pub fn setup_panic () { - use ::std::panic::{set_hook, PanicHookInfo}; - use ::better_panic::{Settings, Verbosity}; - let panic = Settings::auto() - .verbosity(Verbosity::Full) - .create_panic_handler(); - set_hook(Box::new(move |info: &PanicHookInfo|{ - let _ = Tui::teardown(&mut stdout()); - panic(info); - })); - } - pub fn run_main (state: Arc>) -> Usually<()> where - T: View + Apply> + Send + Sync + 'static - { - Exit::run(|exit|{ - let scan = Duration::from_millis(100); - let frame = Duration::from_millis(10); - let (_input, output) = Tui::io(exit.as_ref(), &state, scan, frame, std::io::stdout())?; - let _ = output.join(); - Tui::teardown(&mut stdout()) - }) - } - /// Spawn the TUI input and output threadsl. - pub fn io < - T: View + Apply> + Send + Sync + 'static, - W: Write + Send + Sync + 'static, - > ( - exited: &Arc, - state: &Arc>, - poll: Duration, - sleep: Duration, - output: W, - ) -> Result<(Task, Task), Box> { - Ok(( - Tui::input(exited, state, poll)?, - Tui::output(exited, state, sleep, output)?, - )) - } - /// Spawn the TUI input thread which reads keys from the terminal. - pub fn input > + Send + Sync + 'static> ( - exited: &Arc, state: &Arc>, poll: Duration - ) -> Result { - let exited = exited.clone(); - let state = state.clone(); - Task::new_poll(exited.clone(), poll, move |_| { - let event = read().unwrap(); - if Exit::is(&event) { - exited.store(true, Relaxed); - } else if let Err(e) = state.write().unwrap().apply(&TuiEvent(event)) { - panic!("{e}") - } - }) - } - pub fn teardown (backend: &mut W) -> Usually<()> { - use ::ratatui::backend::Backend; - stdout().execute(LeaveAlternateScreen)?; - CrosstermBackend::new(backend).show_cursor()?; - disable_raw_mode().map_err(Into::into) - } - pub fn new (width: u16, height: u16) -> Self { - Self::Draw(Buffer::empty(Rect { x: 0, y: 0, width, height }), XYWH(0, 0, width, height)) - } - pub fn resize (&mut self, back: &mut CrosstermBackend, width: u16, height: u16) { - let size = Rect { x: 0, y: 0, width, height }; - if let Self::Draw(buffer, ..) = self { - if buffer.area != size { - back.clear_region(ClearType::All).unwrap(); - buffer.resize(size); - buffer.reset(); - } - } - } - pub fn redraw <'b, W: Write> ( - &'b mut self, - back: &mut CrosstermBackend, - mut next: &'b mut Self - ) { - if let Self::Draw(prev, ..) = self && let Self::Draw(next, ..) = next { - let updates = prev.diff(&next); - back.draw(updates.into_iter()).expect("failed to render"); - Backend::flush(back).expect("failed to flush output new"); - std::mem::swap(prev, next); - next.reset(); - } - } - pub fn update (&mut self, callback: &impl Fn(&mut Cell, u16, u16)) -> XYWH { - let XYWH(x0, y0, w, h) = self.area(); - if let Self::Draw(buffer, ..) = self { - for row in 0..h { - let y = y0 + row; - for col in 0..w { - let x = x0 + col; - if x < buffer.area.width && y < buffer.area.height { - if let Some(cell) = buffer.cell_mut(Position { x, y }) { - callback(cell, col, row); - } - } - } - } - } - self.xywh() - } - pub fn blit (&mut self, text: &impl AsRef, x: u16, y: u16, style: Option