Compare commits

..

No commits in common. "e3e3c163da02165e77a259eb715749b7f0097498" and "2557a0d253dfe45eab001dcc08ebc66c2c6715d3" have entirely different histories.

20 changed files with 383 additions and 227 deletions

View file

@ -18,18 +18,10 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.14.0" version = "0.13.0"
edition = "2024" edition = "2024"
[workspace.dependencies] [workspace.dependencies]
tengri = { path = "./tengri" }
tengri_core = { path = "./core" }
tengri_input = { path = "./input" }
tengri_output = { path = "./output" }
tengri_tui = { path = "./tui" }
tengri_dsl = { path = "./dsl" }
tengri_proc = { path = "./proc" }
anyhow = { version = "1.0" } anyhow = { version = "1.0" }
atomic_float = { version = "1" } atomic_float = { version = "1" }
better-panic = { version = "0.3.0" } better-panic = { version = "0.3.0" }

View file

@ -1,25 +1,14 @@
[***dizzle***](https://codeberg.org/unspeaker/tengri/src/branch/main/dsl) # `ket`
is a means of adding a tiny interpreted domain-specific language to your programs.
dizzle currently provides an s-expression based syntax. **ket** is the configuration language of **tek**.
it's based on [edn](https://en.wikipedia.org/wiki/Clojure#Extensible_Data_Notation)
but without all the features.
dizzle parses source code by means of the `Dsl`, `DslExpr` and `DslWord` traits. ## usage
those are implemented for basic stringy types and their `Option` and `Result` wrapped analogs.
to customize parsing, define and use your own traits on top of the provided ones.
dizzle evaluates the parsed source code by means of the `DslNs` trait. the methods of ### with `tengri_output`
this trait match literals, words, and expressions, against pre-defined lists. the
`dsl_words` and `dsl_exprs` macros let you define those lists slightly less verbosely.
## goals this is a `tengri_output` view layout defined using ket:
* [x] const parse
* [ ] live reload
* [ ] serialize modified code back to original indentation
## examples
### in [`tengri_output`](../output)
```edn ```edn
(bsp/s (fixed/y 2 :toolbar) (bsp/s (fixed/y 2 :toolbar)
@ -27,7 +16,9 @@ this trait match literals, words, and expressions, against pre-defined lists. th
(bsp/s :outputs (bsp/s :inputs (bsp/s :tracks :scenes))))))) (bsp/s :outputs (bsp/s :inputs (bsp/s :tracks :scenes)))))))
``` ```
### in [`tengri_input`](../input) ### with `tengri_input`
this is a `tengri_input` keymap defined using ket:
```edn ```edn
(@u undo 1) (@u undo 1)
@ -38,6 +29,70 @@ this trait match literals, words, and expressions, against pre-defined lists. th
(@tab pool toggle) (@tab pool toggle)
``` ```
## tokens
ket has 4 "types", represented by variants of the `Value` enum:
* `Num` - numeric literal
* `Sym` - textual symbol
* `Key` - textual key
* `Exp` - parenthesized group of tokens
### numeric literal
numbers are passed through as is. only non-negative integers are supported.
```edn
0
123456
```
### keys
keys are the names of available operations. they look like this:
```edn
simple-key
multi-part/key
```
keys are implemented by the underlying subsystem:
* in `tengri_output`, keys are names of layout primitives
* in `tengri_input`, keys are names of commands
### symbols
symbols that start with `:` represent the names of variables
provided by the `Context` trait - such as command parameters,
or entire layout components:
```edn
:symbol-name
```
symbols that start with `@` represent keybindings.
they are parsed in `tengri_tui` and look like this:
```edn
@ctrl-alt-shift-space
```
### parenthesized groups
parenthesized groups represent things like expressions
or configuration statements, and look like this:
```edn
(some-key :symbol (some/other-key @another-symbol 123) 456)
```
## goals
* [ ] const parse
* [ ] live reload
* [ ] serialize modified code back to original indentation
## implementation notes ## implementation notes
### `DslExpr` trait behavior ### `DslExpr` trait behavior
@ -59,7 +114,9 @@ this is the trait which differentiates "a thing" from
* e2: Unexpected 'b' * e2: Unexpected 'b'
* e3: Unexpected 'd' * e3: Unexpected 'd'
### possible design for operator-based syntax ## todo
### operators
* replace: `(:= :name :value1 :valueN)` * replace: `(:= :name :value1 :valueN)`
* append: `(:+ :name :value2 :valueN)` * append: `(:+ :name :value2 :valueN)`

View file

@ -289,16 +289,22 @@ fn ok_flat <T> (x: Option<DslPerhaps<T>>) -> DslPerhaps<T> {
// Special form for numeric types // Special form for numeric types
(num |$state:ident : $State: ty| -> $Type:ty { $( $pat:tt => $body:expr ),* $(,)? }) => { (num |$state:ident : $State: ty| -> $Type:ty { $( $pat:tt => $body:expr ),* $(,)? }) => {
impl<'t> DslNs<'t, $Type> for $State { impl<'t> DslNs<'t, $Type> for $State {
const WORDS: DslNsMap<'t, fn (&$State)->Perhaps<$Type>> = const WORDS: DslNsMap<'t, fn (&'t $State)->Perhaps<$Type>> =
DslNsMap::new(&[$(dsl_ns!{@word ($state: $State) -> $Type { $pat => $body }}),*]); DslNsMap::new(&[$(dsl_ns!{@word ($state: $State) -> $Type { $pat => $body }}),*]);
const EXPRS: DslNsMap<'t, fn (&$State, &str)->Perhaps<$Type>> = const EXPRS: DslNsMap<'t, fn (&'t $State, &str)->Perhaps<$Type>> =
DslNsMap::new(&[$(dsl_ns!{@exp ($state: $State) -> $Type { $pat => $body }}),*]); DslNsMap::new(&[$(dsl_ns!{@exp ($state: $State) -> $Type { $pat => $body }}),*]);
fn from_literal <D: Dsl> (&self, dsl: &D) -> Perhaps<$Type> { fn from <D: Dsl> (&'t self, dsl: D) -> Perhaps<$Type> {
Ok(if let Some(src) = dsl.src()? { if let Ok(Some(src)) = dsl.src() {
Some(to_number(src)? as $Type) if let Ok(num) = to_number(src) {
Ok(Some(num as $Type))
} else if let Ok(Some(src)) = src.word() {
self.from_word(src)
} else { } else {
None self.from_expr(src)
}) }
} else {
Ok(None)
}
} }
} }
}; };
@ -309,9 +315,9 @@ fn ok_flat <T> (x: Option<DslPerhaps<T>>) -> DslPerhaps<T> {
// Regular form for single type // Regular form for single type
(|$state:ident : $State: ty| -> $Type:ty { $( $pat:tt => $body:expr ),* $(,)? }) => { (|$state:ident : $State: ty| -> $Type:ty { $( $pat:tt => $body:expr ),* $(,)? }) => {
impl<'t> DslNs<'t, $Type> for $State { impl<'t> DslNs<'t, $Type> for $State {
const WORDS: DslNsMap<'t, fn (&$State)->Perhaps<$Type>> = const WORDS: DslNsMap<'t, fn (&'t $State)->Perhaps<$Type>> =
DslNsMap::new(&[$(dsl_ns!{@word ($state: $State) -> $Type { $pat => $body }}),*]); DslNsMap::new(&[$(dsl_ns!{@word ($state: $State) -> $Type { $pat => $body }}),*]);
const EXPRS: DslNsMap<'t, fn (&$State, &str)->Perhaps<$Type>> = const EXPRS: DslNsMap<'t, fn (&'t $State, &str)->Perhaps<$Type>> =
DslNsMap::new(&[$(dsl_ns!{@exp ($state: $State) -> $Type { $pat => $body }}),*]); DslNsMap::new(&[$(dsl_ns!{@exp ($state: $State) -> $Type { $pat => $body }}),*]);
} }
}; };
@ -352,36 +358,34 @@ fn ok_flat <T> (x: Option<DslPerhaps<T>>) -> DslPerhaps<T> {
pub trait DslNs<'t, T: 't>: 't { pub trait DslNs<'t, T: 't>: 't {
/// Known symbols. /// Known symbols.
const WORDS: DslNsMap<'t, fn (&Self)->Perhaps<T>> = DslNsMap::new(&[]); const WORDS: DslNsMap<'t, fn (&'t Self)->Perhaps<T>> = DslNsMap::new(&[]);
/// Known expressions. /// Known expressions.
const EXPRS: DslNsMap<'t, fn (&Self, &str)->Perhaps<T>> = DslNsMap::new(&[]); const EXPRS: DslNsMap<'t, fn (&'t Self, &str)->Perhaps<T>> = DslNsMap::new(&[]);
/// Resolve an expression or symbol. /// Resolve an expression or symbol.
fn from <D: Dsl> (&self, dsl: &D) -> Perhaps<T> { fn from <D: Dsl> (&'t self, dsl: D) -> Perhaps<T> {
if let Ok(Some(literal)) = self.from_literal(dsl) { if let Ok(Some(src)) = dsl.src() {
Ok(Some(literal)) if let Ok(Some(src)) = src.word() {
} else if let Ok(Some(meaning)) = self.from_word(dsl) { self.from_word(src)
Ok(Some(meaning))
} else { } else {
self.from_expr(dsl) self.from_expr(src)
} }
} } else {
/// Resolve as literal if valid.
fn from_literal <D: Dsl> (&self, _: &D) -> Perhaps<T> {
Ok(None) Ok(None)
} }
}
/// Resolve a symbol if known. /// Resolve a symbol if known.
fn from_word <D: Dsl> (&self, dsl: &D) -> Perhaps<T> { fn from_word <D: Dsl> (&'t self, dsl: D) -> Perhaps<T> {
if let Some(dsl) = dsl.word()? { if let Some(dsl) = dsl.word()? {
for (key, get) in Self::WORDS.0 { if dsl == *key { return get(self) } } for (word, get) in Self::WORDS.0 { if dsl == *word { return get(self) } }
} }
return Ok(None) return Ok(None)
} }
/// Resolve an expression if known. /// Resolve an expression if known.
fn from_expr <D: Dsl> (&self, dsl: &D) -> Perhaps<T> { fn from_expr <D: Dsl> (&'t self, dsl: D) -> Perhaps<T> {
if let Some(expr) = dsl.expr()? { if let Some(head) = dsl.expr().head()? {
for (key, get) in Self::EXPRS.0.iter() { for (key, value) in Self::EXPRS.0.iter() {
if Some(*key) == expr.head()? { if head == *key {
return get(self, &expr.tail()?.unwrap_or("")) return value(self, dsl.expr().tail()?.unwrap_or(""))
} }
} }
} }
@ -396,35 +400,3 @@ impl<'t, T: Debug + 't> DslNsMap<'t, T> {
/// Populate a namespace with pre-existing values. /// Populate a namespace with pre-existing values.
pub const fn new (data: &'t [(&'t str, T)]) -> Self { Self(data) /* TODO a search trie */ } pub const fn new (data: &'t [(&'t str, T)]) -> Self { Self(data) /* TODO a search trie */ }
} }
#[macro_export] macro_rules!dsl_words((|$state:ident|->$Type:ty$({
$($word:literal => $body:expr),* $(,)?
})?)=>{
const WORDS: DslNsMap<'t, fn (&Self)->Perhaps<$Type>> = DslNsMap::new(&[$(
$(($word, |$state: &Self|{Ok(Some($body))})),*
)? ]);
});
#[macro_export] macro_rules!dsl_exprs((|$state:ident|->$Type:ty$({
$($name:literal ($($arg:ident:$ty:ty),* $(,)?) => $body:expr),* $(,)?
})?)=>{
const EXPRS: DslNsMap<'t, fn (&Self, &str)->Perhaps<$Type>> = DslNsMap::new(&[$(
$(($name, |$state: &Self, tail: &str|{
$(
let head = tail.head()?.unwrap_or_default();
let tail = tail.tail()?.unwrap_or_default();
let $arg: $ty = if let Some(arg) = $state.from(&head)? {
arg
} else {
return Err(format!("{}: arg \"{}\" ({}) got: {head} {tail}",
$name,
stringify!($arg),
stringify!($ty),
).into())
};
)*
Ok(Some($body))
})),*
)? ]);
});

View file

@ -4,11 +4,12 @@ description = "UI metaframework, input layer."
version = { workspace = true } version = { workspace = true }
edition = { workspace = true } edition = { workspace = true }
[lib] [features]
path = "input.rs" dsl = [ "tengri_dsl" ]
[dependencies] [dependencies]
tengri_core = { path = "../core" } tengri_core = { path = "../core" }
tengri_dsl = { optional = true, path = "../dsl" }
[dev-dependencies] [dev-dependencies]
tengri_tui = { path = "../tui" } tengri_tui = { path = "../tui" }

View file

@ -1,6 +1,16 @@
***tengri_input*** is where tengri's input handling is defined. # `tengri_engine`
the following items are provided: ## rendering
* `Input` trait, for defining for input sources
* `Handle` trait and `handle!` macro, for defining input handlers ## input handling
* `Command` trait and the `command!` macro, for defining commands that inputs may result in
the **input thread** polls for keyboard events
and passes them onto the application's `Handle::handle` method.
thus, for a type to be a valid application for engine `E`,
it must implement the trait `Handle<E>`, which allows it
to respond to user input.
this thread has write access to the application state,
and is responsible for mutating it in response to
user activity.

View file

@ -1,77 +0,0 @@
#![feature(associated_type_defaults)]
#![feature(if_let_guard)]
pub(crate) use tengri_core::*;
#[cfg(test)] mod input_test;
/// Event source
pub trait Input: Sized {
/// Type of input event
type Event;
/// Result of handling input
type Handled; // TODO: make this an Option<Box dyn Command<Self>> containing the undo
/// Currently handled event
fn event (&self) -> &Self::Event;
/// Whether component should exit
fn is_done (&self) -> bool;
/// Mark component as done
fn done (&self);
}
flex_trait_mut!(Handle <E: Input> {
fn handle (&mut self, _input: &E) -> Perhaps<E::Handled> {
Ok(None)
}
});
pub trait Command<S>: Send + Sync + Sized {
fn execute (self, state: &mut S) -> Perhaps<Self>;
fn delegate <T> (self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps<T>
where Self: Sized
{
Ok(self.execute(state)?.map(wrap))
}
}
impl<S, T: Command<S>> Command<S> for Option<T> {
fn execute (self, _: &mut S) -> Perhaps<Self> {
Ok(None)
}
fn delegate <U> (self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps<U>
where Self: Sized
{
Ok(None)
}
}
/// Implement [Command] for given `State` and `handler`
#[macro_export] macro_rules! command {
($(<$($l:lifetime),+>)?|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
impl$(<$($l),+>)? ::tengri::input::Command<$State> for $Command {
fn execute ($self, $state: &mut $State) -> Perhaps<Self> {
Ok($handler)
}
}
};
}
/// Implement [Handle] for given `State` and `handler`.
#[macro_export] macro_rules! handle {
(|$self:ident:$State:ty,$input:ident|$handler:expr) => {
impl<E: Engine> ::tengri::input::Handle<E> for $State {
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
$handler
}
}
};
($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => {
impl ::tengri::input::Handle<$E> for $State {
fn handle (&mut $self, $input: &$E) ->
Perhaps<<$E as ::tengri::input::Input>::Handled>
{
$handler
}
}
}
}

37
input/src/input.rs Normal file
View file

@ -0,0 +1,37 @@
use crate::*;
/// Event source
pub trait Input: Sized {
/// Type of input event
type Event;
/// Result of handling input
type Handled; // TODO: make this an Option<Box dyn Command<Self>> containing the undo
/// Currently handled event
fn event (&self) -> &Self::Event;
/// Whether component should exit
fn is_done (&self) -> bool;
/// Mark component as done
fn done (&self);
}
flex_trait_mut!(Handle <E: Input> {
fn handle (&mut self, _input: &E) -> Perhaps<E::Handled> {
Ok(None)
}
});
pub trait Command<S>: Send + Sync + Sized {
fn execute (self, state: &mut S) -> Perhaps<Self>;
fn delegate <T> (self, state: &mut S, wrap: impl Fn(Self)->T) -> Perhaps<T>
where Self: Sized
{
Ok(self.execute(state)?.map(wrap))
}
}
impl<S, T: Command<S>> Command<S> for Option<T> {
fn execute (self, _: &mut S) -> Perhaps<Self> {
Ok(None)
}
fn delegate <U> (self, _: &mut S, _: impl Fn(Self)->U) -> Perhaps<U>
where Self: Sized
{
Ok(None)
}
}

76
input/src/input_dsl.rs Normal file
View file

@ -0,0 +1,76 @@
use crate::*;
use std::{sync::Arc, collections::BTreeMap, path::PathBuf};
/// A collection of input bindings.
///
/// Each contained layer defines a mapping from input event to command invocation
/// over a given state. Furthermore, each layer may have an associated cond,
/// so that only certain layers are active at a given time depending on state.
///
/// When a key is pressed, the bindings for it are checked in sequence.
/// When the first non-conditional or true conditional binding is executed,
/// that .event()binding's value is returned.
#[derive(Debug)]
pub struct EventMap<E, C>(
/// Map of each event (e.g. key combination) to
/// all command expressions bound to it by
/// all loaded input layers.
pub BTreeMap<E, Vec<Binding<C>>>
);
/// An input binding.
#[derive(Debug, Clone)]
pub struct Binding<C> {
pub commands: Arc<[C]>,
pub condition: Option<Condition>,
pub description: Option<Arc<str>>,
pub source: Option<Arc<PathBuf>>,
}
impl<C> Binding<C> {
pub fn from_dsl (dsl: impl Dsl) -> Usually<Self> {
let command: Option<C> = None;
let condition: Option<Condition> = None;
let description: Option<Arc<str>> = None;
let source: Option<Arc<PathBuf>> = None;
if let Some(command) = command {
Ok(Self { commands: [command].into(), condition, description, source })
} else {
Err(format!("no command in {dsl:?}").into())
}
}
}
/// Input bindings are only returned if this evaluates to true
#[derive(Clone)]
pub struct Condition(Arc<Box<dyn Fn()->bool + Send + Sync>>);
impl_debug!(Condition |self, w| { write!(w, "*") });
/// Default is always empty map regardless if `E` and `C` implement [Default].
impl<E, C> Default for EventMap<E, C> {
fn default () -> Self { Self(Default::default()) }
}
impl<E: Clone + Ord, C> EventMap<E, C> {
/// Create a new event map
pub fn new () -> Self {
Default::default()
}
/// Add a binding to an owned event map.
pub fn def (mut self, event: E, binding: Binding<C>) -> Self {
self.add(event, binding);
self
}
/// Add a binding to an event map.
pub fn add (&mut self, event: E, binding: Binding<C>) -> &mut Self {
if !self.0.contains_key(&event) {
self.0.insert(event.clone(), Default::default());
}
self.0.get_mut(&event).unwrap().push(binding);
self
}
/// Return the binding(s) that correspond to an event.
pub fn query (&self, event: &E) -> Option<&[Binding<C>]> {
self.0.get(event).map(|x|x.as_slice())
}
/// Return the first binding that corresponds to an event, considering conditions.
pub fn dispatch (&self, event: &E) -> Option<&Binding<C>> {
self.query(event)
.map(|bb|bb.iter().filter(|b|b.condition.as_ref().map(|c|(c.0)()).unwrap_or(true)).next())
.flatten()
}
}

30
input/src/input_macros.rs Normal file
View file

@ -0,0 +1,30 @@
/// Implement [Command] for given `State` and `handler`
#[macro_export] macro_rules! command {
($(<$($l:lifetime),+>)?|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
impl$(<$($l),+>)? ::tengri::input::Command<$State> for $Command {
fn execute ($self, $state: &mut $State) -> Perhaps<Self> {
Ok($handler)
}
}
};
}
/// Implement [Handle] for given `State` and `handler`.
#[macro_export] macro_rules! handle {
(|$self:ident:$State:ty,$input:ident|$handler:expr) => {
impl<E: Engine> ::tengri::input::Handle<E> for $State {
fn handle (&mut $self, $input: &E) -> Perhaps<E::Handled> {
$handler
}
}
};
($E:ty: |$self:ident:$State:ty,$input:ident|$handler:expr) => {
impl ::tengri::input::Handle<$E> for $State {
fn handle (&mut $self, $input: &$E) ->
Perhaps<<$E as ::tengri::input::Input>::Handled>
{
$handler
}
}
}
}

9
input/src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
#![feature(associated_type_defaults)]
#![feature(if_let_guard)]
pub(crate) use tengri_core::*;
mod input_macros;
mod input; pub use self::input::*;
#[cfg(feature = "dsl")] pub(crate) use ::tengri_dsl::*;
#[cfg(feature = "dsl")] mod input_dsl;
#[cfg(feature = "dsl")] pub use self::input_dsl::*;
#[cfg(test)] mod input_test;

View file

@ -1,20 +1,75 @@
***tengri_output*** is an abstract interface layout framework. # `tengri_output`
it expresses the following notions: ## free floating layout primitives
* [**space:**](./src/space.rs) `Direction`, `Coordinate`, `Area`, `Size`, `Measure` this crate exposes several layout operators
which work entirely in unsigned coordinates
and are generic over the trait `Content`.
most importantly, they are not dependent on rendering framework.
* [**output:**](./src/output.rs) `Output`, `Render`, `Content` |operator|description|
* the layout operators are generic over `Render` and/or `Content` |-|-|
* the traits `Render` and `Content` are generic over `Output` |**`When(x, a)`**|render `a` only when `x == true`|
* implement `Output` to bring a layout to a new backend: |**`Either(x, a, b)`**|render `a` when `x == true`, otherwise render `b`|
[see `TuiOut` in `tengri_tui`](../tui/src/tui_engine/tui_output.rs) |**`Map(get_iterator, callback)`**|transform items in uniform way|
|**`Bsp`**|concatenative layout|
|...|...|
|**`Align`**|pin content along axis|
|...|...|
|**`Fill`**|**make content's dimension equal to container's:**|
|`Fill::x(a)`|use container's width for content|
|`Fill::y(a)`|use container's height for content|
|`Fill::xy(a)`|use container's width and height for content|
|**`Fixed`**|**assign fixed dimension to content:**|
|`Fixed::x(w, a)`|use width `w` for content|
|`Fixed::y(w, a)`|use height `w` for content|
|`Fixed::xy(w, h, a)`|use width `w` and height `h` for content|
|**`Expand`/`Shrink`**|**change dimension of content:**|
|`Expand::x(n, a)`/`Shrink::x(n, a)`|increment/decrement width of content area by `n`|
|`Expand::y(n, a)`/`Shrink::y(n, a)`|increment/decrement height of content area by `m`|
|`Expand::xy(n, m, a)`/`Shrink::xy(n, m, a)`|increment/decrement width of content area by `n`, height by `m`|
|**`Min`/`Max`**|**constrain dimension of content:**|
|`Min::x(w, a)`/`Max::x(w, a)`|enforce minimum/maximum width `w` for content|
|`Min::y(h, a)`/`Max::y(h, a)`|enforce minimum/maximum height `h` for content|
|`Min::xy(w, h, a)`/`Max::xy(w, h, a)`|enforce minimum/maximum width `w` and height `h` for content|
|**`Push`/`Pull`**|**move content along axis:**|
|`Push::x(n, a)`/`Pull::x(n, a)`|increment/decrement `x` of content area|
|`Push::y(n, a)`/`Pull::y(n, a)`|increment/decrement `y` of content area|
|`Push::xy(n, m, a)`/`Pull::xy(n, m, a)`|increment/decrement `x` and `y` of content area|
* [**layout:**](./src/layout.rs) **todo:**
* conditionals: `When`, `Either` * sensible `Margin`/`Padding`
* iteration: `Map` * `Reduce`
* concatenation: `Bsp`
* positioning: `Align`, `Push`, `Pull` ## example rendering loop
* sizing: `Fill`, `Fixed`, `Expand`, `Shrink`, `Min`, `Max`
* implement custom components (that may be backend-dependent): the **render thread** continually invokes the
[see `tui_content` in `tengri_tui`](../tui/src/tui_content) `Content::render` method of the application
to redraw the display. it does this efficiently
by using ratatui's double buffering.
thus, for a type to be a valid application for engine `E`,
it must implement the trait `Content<E>`, which allows
it to display content to the engine's output.
the most important thing about the `Content` trait is that
it composes:
* you can implement `Content::content` to build
`Content`s out of other `Content`s
* and/or `Content::area` for custom positioning and sizing,
* and/or `Content::render` for custom rendering
within the given `Content`'s area.
the manner of output is determined by the
`Engine::Output` type, a mutable pointer to which
is passed to the render method, e.g. in the case of
the `Tui` engine: `fn render(&self, output: &mut TuiOut)`
you can use `TuiOut::blit` and `TuiOut::place`
to draw at specified coordinates of the display, and/or
directly modify the underlying `ratatui::Buffer` at
`output.buffer`
rendering is intended to work with read-only access
to the application state. if you really need to update
values during rendering, use interior mutability.

View file

@ -10,7 +10,7 @@ pub(crate) use tengri_core::*;
#[cfg(feature = "dsl")] pub(crate) use ::tengri_dsl::*; #[cfg(feature = "dsl")] pub(crate) use ::tengri_dsl::*;
mod space; pub use self::space::*; mod space; pub use self::space::*;
mod layout; pub use self::layout::*; mod ops; pub use self::ops::*;
mod output; pub use self::output::*; mod output; pub use self::output::*;
#[cfg(test)] mod test; #[cfg(test)] mod test;

View file

@ -47,7 +47,7 @@ impl<E: Output, C: Content<E>> Render<E> for C {
/// Opaque pointer to a renderable living on the heap. /// Opaque pointer to a renderable living on the heap.
/// ///
/// Return this from [Content::content] to use dynamic dispatch. /// Return this from [Content::content] to use dynamic dispatch.
pub type RenderBox<E> = Box<dyn Render<E>>; pub type RenderBox<E> = Box<RenderDyn<E>>;
/// You can render from a box. /// You can render from a box.
impl<E: Output> Content<E> for RenderBox<E> { impl<E: Output> Content<E> for RenderBox<E> {
@ -55,8 +55,11 @@ impl<E: Output> Content<E> for RenderBox<E> {
//fn boxed <'b> (self) -> RenderBox<'b, E> where Self: Sized + 'b { self } //fn boxed <'b> (self) -> RenderBox<'b, E> where Self: Sized + 'b { self }
} }
/// Opaque pointer to a renderable.
pub type RenderDyn<E> = dyn Render<E>;
/// You can render from an opaque pointer. /// You can render from an opaque pointer.
impl<E: Output> Content<E> for &dyn Render<E> where Self: Sized { impl<E: Output> Content<E> for &RenderDyn<E> where Self: Sized {
fn content (&self) -> impl Render<E> + '_ { fn content (&self) -> impl Render<E> + '_ {
#[allow(suspicious_double_ref_op)] #[allow(suspicious_double_ref_op)]
self.deref() self.deref()

View file

@ -9,10 +9,10 @@ default = [ "input", "output", "tui" ]
input = [ "tengri_input" ] input = [ "tengri_input" ]
output = [ "tengri_output" ] output = [ "tengri_output" ]
tui = [ "tengri_tui" ] tui = [ "tengri_tui" ]
dsl = [ "tengri_dsl", "tengri_output/dsl", "tengri_tui/dsl" ] dsl = [ "tengri_dsl", "tengri_input/dsl", "tengri_output/dsl", "tengri_tui/dsl" ]
[dependencies] [dependencies]
tengri_core = { workspace = true } tengri_core = { path = "../core" }
tengri_dsl = { optional = true, path = "../dsl" } tengri_dsl = { optional = true, path = "../dsl" }
tengri_input = { optional = true, path = "../input" } tengri_input = { optional = true, path = "../input" }
tengri_output = { optional = true, path = "../output" } tengri_output = { optional = true, path = "../output" }

View file

@ -1,17 +1,3 @@
***tengri*** is a metaframework for building interactive applications with rust. (aren't we all?) # tengri
tengri is developed as part of [***tek***](https://codeberg.org/unspeaker/tek), an interface metaframework. currently supports ratatui.
a music program for terminals.
tengri contains:
* [***dizzle***](./dsl), a framework for defining domain-specific languages
* [***output***](./output), an abstract UI layout framework
* [***input***](./input), an abstract UI event framework
* [***tui***](./tui), an implementation of tengri over [***ratatui***](https://ratatui.rs/).
as well as:
* [***core***](./core), the shared definitions ("utils") module
* [***proc***](./proc), the space for procedural macros
* [***tengri***](./tengri), the top-level reexport crate
tengri is published under [**AGPL3**](./LICENSE).

View file

@ -5,13 +5,13 @@ version = { workspace = true }
edition = { workspace = true } edition = { workspace = true }
[features] [features]
dsl = [ "tengri_dsl", "tengri_output/dsl" ] dsl = [ "tengri_dsl", "tengri_input/dsl", "tengri_output/dsl" ]
[dependencies] [dependencies]
tengri_core = { workspace = true } tengri_core = { path = "../core" }
tengri_input = { workspace = true } tengri_input = { path = "../input" }
tengri_output = { workspace = true } tengri_output = { path = "../output" }
tengri_dsl = { workspace = true, optional = true } tengri_dsl = { optional = true, path = "../dsl" }
palette = { workspace = true } palette = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
@ -24,6 +24,6 @@ quanta = { workspace = true }
unicode-width = { workspace = true } unicode-width = { workspace = true }
[dev-dependencies] [dev-dependencies]
tengri = { workspace = true, features = [ "dsl" ] } tengri = { path = "../tengri", features = [ "dsl" ] }
tengri_dsl = { workspace = true } tengri_dsl = { path = "../dsl" }
tengri_proc = { workspace = true } tengri_proc = { path = "../proc" }

View file

@ -1,4 +1,15 @@
***tengri_tui*** is an implementation of [tengri_output](../output) and [tengri_input](../input) # `tengri_tui`
on top of [ratatui](https://ratatui.rs/) and [crossterm](https://github.com/crossterm-rs/crossterm).
tengri is published under [**AGPL3**](../LICENSE). the `Tui` struct (the *engine*) implements the
`tengri_input::Input` and `tengri_output::Output` traits.
at launch, the `Tui` engine spawns two threads,
a **render thread** and an **input thread**. (the
application may spawn further threads, such as a
**jack thread**.)
all threads communicate using shared ownership,
`Arc<RwLock>` and `Arc<Atomic>`. the engine and
application instances are expected to be wrapped
in `Arc<RwLock>`; internally, those synchronization
mechanisms may be used liberally.

View file

@ -7,12 +7,6 @@ mod tui_event; pub use self::tui_event::*;
mod tui_output; pub use self::tui_output::*; mod tui_output; pub use self::tui_output::*;
mod tui_perf; pub use self::tui_perf::*; mod tui_perf; pub use self::tui_perf::*;
// The `Tui` struct (the *engine*) implements the
// `tengri_input::Input` and `tengri_output::Output` traits.
// At launch, the `Tui` engine spawns two threads, the render thread and the input thread.
// the application may further spawn other threads. All threads communicate using shared ownership:
// `Arc<RwLock<T>>` and `Arc<AtomicT>`. Thus, at launch the engine and application instances are expected to be wrapped in `Arc<RwLock>`.
pub struct Tui { pub struct Tui {
pub exited: Arc<AtomicBool>, pub exited: Arc<AtomicBool>,
pub backend: CrosstermBackend<Stdout>, pub backend: CrosstermBackend<Stdout>,