use std::any::{Any, TypeId};
use std::io::{self, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
struct RawModeGuard {
bracketed_paste: bool,
keyboard_enhanced: bool,
}
impl Drop for RawModeGuard {
fn drop(&mut self) {
use crossterm::execute;
let mut stdout = io::stdout();
if self.keyboard_enhanced {
let _ = execute!(stdout, crossterm::event::PopKeyboardEnhancementFlags);
}
if self.bracketed_paste {
let _ = execute!(stdout, crossterm::event::DisableBracketedPaste);
}
let _ = crossterm::terminal::disable_raw_mode();
let _ = stdout.write_all(b"\x1b[?25h");
let _ = stdout.flush();
}
}
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use futures::StreamExt;
use tokio::sync::{mpsc, oneshot};
use crate::component::VStack;
use crate::element::Elements;
use crate::inline::InlineRenderer;
use crate::node::NodeId;
type StateUpdateFn<S> = Box<dyn FnOnce(&mut S) + Send>;
type StateGetFn<S> = Box<dyn FnOnce(&S) + Send>;
type ViewFn<S> = Box<dyn Fn(&S) -> Elements>;
type CommitCallbackFn<S> = Box<dyn FnMut(&CommittedElement, &mut S)>;
type EventHandlerFn<'a, S> = Option<&'a mut dyn FnMut(&Event, &mut S) -> ControlFlow>;
enum AppMessage<S> {
UpdateState(StateUpdateFn<S>),
GetState(StateGetFn<S>),
}
#[derive(Debug, Clone)]
pub struct CommittedElement {
pub key: Option<String>,
pub index: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlFlow {
Continue,
Exit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CtrlCBehavior {
#[default]
Exit,
Deliver,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KeyboardProtocol {
#[default]
Legacy,
Enhanced,
}
pub struct Handle<S: Send + 'static> {
tx: mpsc::UnboundedSender<AppMessage<S>>,
exit: Arc<AtomicBool>,
}
impl<S: Send + 'static> Handle<S> {
pub fn update(&self, f: impl FnOnce(&mut S) + Send + 'static) {
let _ = self.tx.send(AppMessage::UpdateState(Box::new(f)));
}
pub fn fetch<T: Send + 'static>(
&self,
f: impl FnOnce(&S) -> T + Send + 'static,
) -> oneshot::Receiver<T> {
let (tx, rx) = oneshot::channel();
let _ = self.tx.send(AppMessage::GetState(Box::new(move |s| {
let _ = tx.send(f(s));
})));
rx
}
pub fn exit(&self) {
self.exit.store(true, Ordering::Release);
}
}
impl<S: Send + 'static> Clone for Handle<S> {
fn clone(&self) -> Self {
Handle {
tx: self.tx.clone(),
exit: self.exit.clone(),
}
}
}
pub struct ApplicationBuilder<S: Send + 'static> {
state: Option<S>,
view_fn: Option<ViewFn<S>>,
width: Option<u16>,
on_commit: Option<CommitCallbackFn<S>>,
root_contexts: Vec<(TypeId, Box<dyn Any + Send + Sync>)>,
ctrl_c: CtrlCBehavior,
keyboard_protocol: KeyboardProtocol,
extra_newlines_at_exit: usize,
bracketed_paste: bool,
}
impl<S: Send + 'static> ApplicationBuilder<S> {
pub fn state(mut self, state: S) -> Self {
self.state = Some(state);
self
}
pub fn view(mut self, f: impl Fn(&S) -> Elements + 'static) -> Self {
self.view_fn = Some(Box::new(f));
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = Some(width);
self
}
pub fn on_commit(mut self, f: impl FnMut(&CommittedElement, &mut S) + 'static) -> Self {
self.on_commit = Some(Box::new(f));
self
}
pub fn ctrl_c(mut self, behavior: CtrlCBehavior) -> Self {
self.ctrl_c = behavior;
self
}
pub fn keyboard_protocol(mut self, protocol: KeyboardProtocol) -> Self {
self.keyboard_protocol = protocol;
self
}
pub fn bracketed_paste(mut self, enabled: bool) -> Self {
self.bracketed_paste = enabled;
self
}
pub fn with_context<T: Any + Send + Sync>(mut self, value: T) -> Self {
self.root_contexts
.push((TypeId::of::<T>(), Box::new(value)));
self
}
pub fn extra_newlines_at_exit(mut self, extra_newlines: usize) -> Self {
self.extra_newlines_at_exit = extra_newlines;
self
}
pub fn build(self) -> io::Result<(Application<S>, Handle<S>)> {
let state = self.state.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Application requires .state()")
})?;
let view_fn = self.view_fn.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "Application requires .view()")
})?;
let width = match self.width {
Some(w) => w,
None => crossterm::terminal::size()?.0,
};
let (tx, rx) = mpsc::unbounded_channel();
let exit = Arc::new(AtomicBool::new(false));
let handle = Handle {
tx,
exit: exit.clone(),
};
let mut inline = InlineRenderer::new(width);
for (type_id, value) in self.root_contexts {
inline.set_root_context_raw(type_id, value);
}
let container = inline.push(VStack);
let app = Application {
state,
view_fn,
inline,
container,
dirty: true,
on_commit: self.on_commit,
rx,
exit,
ctrl_c: self.ctrl_c,
keyboard_protocol: self.keyboard_protocol,
extra_newlines_at_exit: self.extra_newlines_at_exit,
bracketed_paste: self.bracketed_paste,
};
Ok((app, handle))
}
}
pub struct Application<S: Send + 'static> {
state: S,
view_fn: ViewFn<S>,
inline: InlineRenderer,
container: NodeId,
dirty: bool,
on_commit: Option<CommitCallbackFn<S>>,
rx: mpsc::UnboundedReceiver<AppMessage<S>>,
exit: Arc<AtomicBool>,
ctrl_c: CtrlCBehavior,
keyboard_protocol: KeyboardProtocol,
extra_newlines_at_exit: usize,
bracketed_paste: bool,
}
impl<S: Send + 'static> Application<S> {
pub fn builder() -> ApplicationBuilder<S> {
ApplicationBuilder {
state: None,
view_fn: None,
width: None,
on_commit: None,
root_contexts: Vec::new(),
ctrl_c: CtrlCBehavior::default(),
keyboard_protocol: KeyboardProtocol::default(),
extra_newlines_at_exit: 0,
bracketed_paste: false,
}
}
pub async fn run(&mut self) -> io::Result<()> {
let mut stdout = io::stdout();
self.render_loop(&mut stdout).await
}
pub async fn run_loop(&mut self) -> io::Result<()> {
let mut stdout = io::stdout();
self.rebuild();
self.flush_to(&mut stdout)?;
let guard = self.enter_raw_mode()?;
let result = self.interactive_loop(None, &mut stdout).await;
self.leave_raw_mode(guard, &mut stdout);
result
}
pub async fn run_interactive(
&mut self,
mut handler: impl FnMut(&Event, &mut S) -> ControlFlow,
) -> io::Result<()> {
let mut stdout = io::stdout();
self.rebuild();
self.flush_to(&mut stdout)?;
let guard = self.enter_raw_mode()?;
let result = self.interactive_loop(Some(&mut handler), &mut stdout).await;
self.leave_raw_mode(guard, &mut stdout);
result
}
pub fn update(&mut self, f: impl FnOnce(&mut S)) {
f(&mut self.state);
self.dirty = true;
}
pub fn handle_event(&mut self, event: &Event) {
self.inline.handle_event(event);
}
pub fn tick(&mut self) {
if self.inline.tick() {
self.dirty = true;
}
}
pub fn has_active(&self) -> bool {
self.inline.has_active()
}
pub fn state(&self) -> &S {
&self.state
}
pub fn is_exit_requested(&self) -> bool {
self.exit.load(Ordering::Acquire)
}
pub fn flush(&mut self, writer: &mut impl Write) -> io::Result<()> {
self.drain_updates();
if self.dirty {
self.rebuild();
}
self.flush_to(writer)?;
self.check_commits();
Ok(())
}
pub fn renderer(&mut self) -> &mut InlineRenderer {
&mut self.inline
}
fn enter_raw_mode(&self) -> io::Result<RawModeGuard> {
use crossterm::execute;
let mut stdout = io::stdout();
crossterm::terminal::enable_raw_mode()?;
let bracketed_paste = self.bracketed_paste;
if bracketed_paste {
let _ = execute!(stdout, crossterm::event::EnableBracketedPaste);
}
let keyboard_enhanced = self.keyboard_protocol == KeyboardProtocol::Enhanced && {
crossterm::terminal::supports_keyboard_enhancement().unwrap_or(false)
};
if keyboard_enhanced {
let _ = execute!(
stdout,
crossterm::event::PushKeyboardEnhancementFlags(
crossterm::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| crossterm::event::KeyboardEnhancementFlags::REPORT_EVENT_TYPES
)
);
}
Ok(RawModeGuard {
bracketed_paste,
keyboard_enhanced,
})
}
fn leave_raw_mode(&mut self, guard: RawModeGuard, stdout: &mut impl Write) {
let finalize_bytes = self.inline.finalize();
if !finalize_bytes.is_empty() {
let _ = stdout.write_all(&finalize_bytes);
let _ = stdout.flush();
}
drop(guard);
let _ = writeln!(stdout);
for _ in 0..self.extra_newlines_at_exit {
let _ = writeln!(stdout);
}
let _ = stdout.flush();
}
async fn render_loop(&mut self, writer: &mut impl Write) -> io::Result<()> {
self.rebuild();
self.flush_to(writer)?;
let mut tick_interval = tokio::time::interval(Duration::from_millis(16));
let mut channel_open = true;
loop {
if self.exit.load(Ordering::Acquire) {
break;
}
let has_active = self.inline.has_active();
if !channel_open && !has_active {
break;
}
tokio::select! {
result = self.rx.recv(), if channel_open => {
match result {
Some(AppMessage::GetState(get)) => {
get(&self.state);
}
Some(AppMessage::UpdateState(update)) => {
update(&mut self.state);
self.dirty = true;
}
None => {
channel_open = false;
}
}
}
_ = tick_interval.tick(), if has_active => {
if self.inline.tick() {
self.dirty = true;
}
}
}
if self.dirty {
self.rebuild();
}
self.flush_to(writer)?;
self.check_commits();
}
self.flush_to(writer)?;
self.check_commits();
let finalize_bytes = self.inline.finalize();
if !finalize_bytes.is_empty() {
writer.write_all(&finalize_bytes)?;
writer.flush()?;
}
Ok(())
}
async fn interactive_loop(
&mut self,
mut handler: EventHandlerFn<'_, S>,
stdout: &mut impl Write,
) -> io::Result<()> {
let mut event_stream = crossterm::event::EventStream::new();
let mut tick_interval = tokio::time::interval(Duration::from_millis(16));
let mut channel_open = true;
loop {
if self.exit.load(Ordering::Acquire) {
break;
}
let has_active = self.inline.has_active();
if !channel_open && !has_active {
break;
}
tokio::select! {
maybe_event = event_stream.next() => {
let evt = match maybe_event {
Some(Ok(evt)) => evt,
Some(Err(e)) => return Err(e),
None => break, };
if self.ctrl_c == CtrlCBehavior::Exit
&& let Event::Key(KeyEvent {
code: KeyCode::Char('c'),
modifiers,
kind: KeyEventKind::Press,
..
}) = &evt
&& modifiers.contains(KeyModifiers::CONTROL)
{
break;
}
if let Event::Resize(new_width, _) = &evt {
let output = self.inline.resize(*new_width);
stdout.write_all(&output)?;
stdout.flush()?;
self.dirty = true;
} else {
self.inline.handle_event(&evt);
self.dirty = true;
if let Some(ref mut h) = handler {
let flow = h(&evt, &mut self.state);
if matches!(flow, ControlFlow::Exit) {
break;
}
}
}
}
result = self.rx.recv(), if channel_open => {
match result {
Some(AppMessage::UpdateState(update)) => {
update(&mut self.state);
self.dirty = true;
}
Some(AppMessage::GetState(get)) => {
get(&self.state);
}
None => {
channel_open = false;
}
}
}
_ = tick_interval.tick(), if has_active => {
if self.inline.tick() {
self.dirty = true;
}
}
}
if self.dirty {
self.rebuild();
}
self.flush_to(stdout)?;
self.check_commits();
}
if self.dirty {
self.rebuild();
}
self.flush_to(stdout)?;
self.check_commits();
Ok(())
}
fn rebuild(&mut self) {
let elements = (self.view_fn)(&self.state);
self.inline.rebuild(self.container, elements);
self.dirty = false;
}
fn drain_updates(&mut self) {
while let Ok(update) = self.rx.try_recv() {
match update {
AppMessage::UpdateState(update) => {
update(&mut self.state);
self.dirty = true;
}
AppMessage::GetState(get) => {
get(&self.state);
}
}
}
}
fn flush_to(&mut self, writer: &mut impl Write) -> io::Result<()> {
let output = self.inline.render();
if !output.is_empty() {
writer.write_all(&output)?;
writer.flush()?;
}
Ok(())
}
fn check_commits(&mut self) {
let terminal_height = crossterm::terminal::size()
.map(|(_, h)| h)
.unwrap_or(u16::MAX);
self.check_commits_with_height(terminal_height);
}
fn check_commits_with_height(&mut self, terminal_height: u16) {
if self.on_commit.is_none() {
return;
}
let committed = self
.inline
.detect_committed(self.container, terminal_height);
if committed.is_empty() {
return;
}
let children = self.inline.children(self.container);
let mut committed_height: u16 = 0;
for &(i, _) in &committed {
committed_height += self.inline.node_last_height(children[i]);
}
let on_commit = self.on_commit.as_mut().unwrap();
for (index, key) in &committed {
let elem = CommittedElement {
key: key.clone(),
index: *index,
};
on_commit(&elem, &mut self.state);
}
self.dirty = true;
self.inline
.commit(self.container, committed.len(), committed_height);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::spinner::Spinner;
use crate::components::text::Text;
fn text_view(state: &Vec<String>) -> Elements {
let mut els = Elements::new();
for line in state {
els.add(Text::unstyled(line.as_str()));
}
els
}
#[test]
fn initial_flush_renders_content() {
let (mut app, _handle) = Application::builder()
.state(vec!["hello".to_string()])
.view(text_view)
.width(20)
.build()
.unwrap();
let mut output = Vec::new();
app.flush(&mut output).unwrap();
let s = String::from_utf8_lossy(&output);
assert!(s.contains("hello"));
}
#[test]
fn update_triggers_rebuild_on_flush() {
let (mut app, _handle) = Application::builder()
.state(vec!["before".to_string()])
.view(text_view)
.width(20)
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
assert_eq!(app.state(), &vec!["before".to_string()]);
app.update(|s| {
s.clear();
s.push("after".to_string());
});
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
assert_eq!(app.state(), &vec!["after".to_string()]);
assert!(!buf.is_empty());
}
#[test]
fn handle_update_applied_on_flush() {
let (mut app, handle) = Application::builder()
.state(vec!["initial".to_string()])
.view(text_view)
.width(20)
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
handle.update(|s| s.push("from_handle".to_string()));
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
let s = String::from_utf8_lossy(&buf);
assert!(s.contains("from_handle"));
}
#[test]
fn handle_update_from_another_thread() {
let (mut app, handle) = Application::builder()
.state(0u32)
.view(|n: &u32| {
let mut els = Elements::new();
els.add(Text::unstyled(format!("count: {}", n)));
els
})
.width(20)
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
let t = std::thread::spawn(move || {
handle.update(|s| *s = 42);
});
t.join().unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
assert_eq!(*app.state(), 42);
}
#[test]
fn state_readable() {
let (app, _handle) = Application::builder()
.state(42u32)
.view(|_: &u32| Elements::new())
.width(10)
.build()
.unwrap();
assert_eq!(*app.state(), 42);
}
#[test]
fn handle_exit_sets_flag() {
let (app, handle) = Application::builder()
.state(0u32)
.view(|_: &u32| Elements::new())
.width(10)
.build()
.unwrap();
assert!(!app.is_exit_requested());
handle.exit();
assert!(app.is_exit_requested());
}
#[test]
fn has_active_reflects_effects() {
let (mut app, _handle) = Application::builder()
.state(true) .view(|show: &bool| {
let mut els = Elements::new();
if *show {
els.add(Spinner::new("loading")).key("s");
}
els
})
.width(20)
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
assert!(app.has_active());
app.update(|s| *s = false);
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
assert!(!app.has_active());
}
#[test]
fn tick_advances_effects() {
let (mut app, _handle) = Application::builder()
.state(true)
.view(|show: &bool| {
let mut els = Elements::new();
if *show {
els.add(Spinner::new("loading"));
}
els
})
.width(20)
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
std::thread::sleep(Duration::from_millis(100));
app.tick();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
assert!(!buf.is_empty());
}
#[test]
fn multiple_handle_updates_batched() {
let (mut app, handle) = Application::builder()
.state(0u32)
.view(|n: &u32| {
let mut els = Elements::new();
els.add(Text::unstyled(format!("count: {}", n)));
els
})
.width(20)
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
handle.update(|s| *s += 1);
handle.update(|s| *s += 1);
handle.update(|s| *s += 1);
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
assert_eq!(*app.state(), 3);
assert!(!buf.is_empty());
}
#[test]
fn empty_state_produces_no_content() {
let (mut app, _handle) = Application::builder()
.state(())
.view(|_: &()| Elements::new())
.width(10)
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
assert!(buf.is_empty());
}
#[test]
fn renderer_accessible() {
let (mut app, _handle) = Application::builder()
.state(vec!["test".to_string()])
.view(text_view)
.width(20)
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
let renderer = app.renderer();
assert!(!renderer.has_active());
}
#[tokio::test]
async fn run_exits_when_handle_dropped_and_idle() {
let (mut app, handle) = Application::builder()
.state(true) .view(|show: &bool| {
let mut els = Elements::new();
if *show {
els.add(Spinner::new("loading")).key("s");
}
els
})
.width(20)
.build()
.unwrap();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
handle.update(|s| *s = false);
});
let mut buf = Vec::new();
app.render_loop(&mut buf).await.unwrap();
assert!(!app.has_active());
}
#[tokio::test]
async fn handle_update_from_async_task() {
let (mut app, handle) = Application::builder()
.state(0u32)
.view(|n: &u32| {
let mut els = Elements::new();
els.add(Text::unstyled(format!("count: {}", n)));
els
})
.width(20)
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
let task = tokio::spawn(async move {
handle.update(|s| *s = 99);
});
task.await.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
assert_eq!(*app.state(), 99);
}
#[test]
fn commit_fires_when_content_exceeds_terminal() {
use std::sync::{Arc, Mutex};
let committed_keys: Arc<Mutex<Vec<Option<String>>>> = Arc::new(Mutex::new(Vec::new()));
let keys_clone = committed_keys.clone();
let (mut app, _handle) = Application::builder()
.state(vec![
"line1".to_string(),
"line2".to_string(),
"line3".to_string(),
])
.view(|lines: &Vec<String>| {
let mut els = Elements::new();
for (i, line) in lines.iter().enumerate() {
els.add(Text::unstyled(line.as_str()))
.key(format!("line-{}", i));
}
els
})
.width(20)
.on_commit(move |elem, state| {
keys_clone.lock().unwrap().push(elem.key.clone());
state.remove(0);
})
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
app.check_commits_with_height(2);
let keys = committed_keys.lock().unwrap();
assert_eq!(keys.len(), 1);
assert_eq!(keys[0], Some("line-0".to_string()));
drop(keys);
assert_eq!(app.state().len(), 2);
assert_eq!(app.state()[0], "line2");
}
#[test]
fn no_commit_when_all_content_visible() {
let committed_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let count_clone = committed_count.clone();
let (mut app, _handle) = Application::builder()
.state(vec!["line1".to_string()])
.view(|lines: &Vec<String>| {
let mut els = Elements::new();
for line in lines {
els.add(Text::unstyled(line.as_str()));
}
els
})
.width(20)
.on_commit(move |_, _| {
count_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
})
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
app.check_commits_with_height(10);
assert_eq!(
committed_count.load(std::sync::atomic::Ordering::Relaxed),
0
);
}
#[test]
fn no_commit_without_callback() {
let (mut app, _handle) = Application::builder()
.state(vec!["a".to_string(), "b".to_string(), "c".to_string()])
.view(|lines: &Vec<String>| {
let mut els = Elements::new();
for line in lines {
els.add(Text::unstyled(line.as_str()));
}
els
})
.width(20)
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
app.check_commits_with_height(1);
assert_eq!(app.state().len(), 3); }
#[test]
fn multiple_commits_at_once() {
let committed_count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let count_clone = committed_count.clone();
let (mut app, _handle) = Application::builder()
.state(vec![
"a".to_string(),
"b".to_string(),
"c".to_string(),
"d".to_string(),
"e".to_string(),
])
.view(|lines: &Vec<String>| {
let mut els = Elements::new();
for line in lines {
els.add(Text::unstyled(line.as_str()));
}
els
})
.width(20)
.on_commit(move |_, state: &mut Vec<String>| {
count_clone.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
state.remove(0);
})
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
app.check_commits_with_height(2);
assert_eq!(
committed_count.load(std::sync::atomic::Ordering::Relaxed),
3
);
assert_eq!(app.state().len(), 2);
assert_eq!(app.state()[0], "d");
}
struct CtxReader;
#[derive(Default)]
struct CtxReaderState {
value: Option<String>,
}
impl crate::component::Component for CtxReader {
type State = CtxReaderState;
fn render(
&self,
area: ratatui_core::layout::Rect,
buf: &mut ratatui_core::buffer::Buffer,
state: &Self::State,
) {
use ratatui_core::widgets::Widget;
if let Some(ref v) = state.value {
ratatui_widgets::paragraph::Paragraph::new(v.as_str()).render(area, buf);
}
}
fn lifecycle(
&self,
hooks: &mut crate::hooks::Hooks<Self, Self::State>,
_state: &Self::State,
) {
hooks.use_context::<String>(|value, _props, state| {
state.value = value.cloned();
});
}
}
#[test]
fn with_context_available_to_components() {
let (mut app, _handle) = Application::builder()
.state(())
.view(|_: &()| {
let mut els = Elements::new();
els.add(CtxReader);
els
})
.width(30)
.with_context("app-context".to_string())
.build()
.unwrap();
let mut buf = Vec::new();
app.flush(&mut buf).unwrap();
let s = String::from_utf8_lossy(&buf);
assert!(s.contains("app-context"));
}
}