mirror of
https://codeberg.org/unspeaker/tengri.git
synced 2025-12-06 19:56:44 +01:00
Compare commits
No commits in common. "e3e3c163da02165e77a259eb715749b7f0097498" and "2557a0d253dfe45eab001dcc08ebc66c2c6715d3" have entirely different histories.
e3e3c163da
...
2557a0d253
20 changed files with 383 additions and 227 deletions
10
Cargo.toml
10
Cargo.toml
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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)`
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
self.from_expr(src)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
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 {
|
||||||
|
self.from_expr(src)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
self.from_expr(dsl)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Resolve as literal if valid.
|
|
||||||
fn from_literal <D: Dsl> (&self, _: &D) -> Perhaps<T> {
|
|
||||||
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))
|
|
||||||
})),*
|
|
||||||
)? ]);
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
37
input/src/input.rs
Normal 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
76
input/src/input_dsl.rs
Normal 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
30
input/src/input_macros.rs
Normal 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
9
input/src/lib.rs
Normal 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;
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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).
|
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue