use std::{fmt::{Debug, Display},
marker::PhantomData};
use smallvec::smallvec;
use tokio::sync::mpsc;
use super::{BoxedSafeApp, Continuation, DefaultInputEventHandler, EventPropagation,
MainEventLoopFuture};
use crate::{ch, col, glyphs, height, inline_string, lock_output_device_as_mut,
new_style, ok, render_pipeline, row,
telemetry::{telemetry_default_constants, Telemetry},
telemetry_record, width, Ansi256GradientIndex, ColorWheel, ColorWheelConfig,
ColorWheelSpeed, CommonResult, ComponentRegistryMap, DefaultSize,
DefaultTiming, Flush, FlushKind, GCStringExt, GlobalData,
GradientGenerationPolicy, HasFocus, InputDevice, InputDeviceExt, InputEvent,
LockedOutputDevice, MinSize, OffscreenBufferPool, OutputDevice, RawMode,
RenderOp, RenderPipeline, Size, SufficientSize, TelemetryAtomHint,
TerminalWindowMainThreadSignal, TextColorizationPolicy, ZOrder,
DEBUG_TUI_MOD, DISPLAY_LOG_TELEMETRY};
pub fn main_event_loop_impl<'a, S, AS>(
app: BoxedSafeApp<S, AS>,
exit_keys: &'a [InputEvent],
state: S,
initial_size: Size,
input_device: InputDevice,
output_device: OutputDevice,
) -> MainEventLoopFuture<'a, S, AS>
where
S: Display + Debug + Default + Clone + Sync + Send + 'a,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
Box::pin(async move {
let mut app = app;
let event_loop_state = EventLoopState::initialize(
state,
initial_size,
output_device.clone(),
&mut app,
)?;
run_main_event_loop(
event_loop_state,
app,
exit_keys,
input_device,
output_device,
)
.await
})
}
#[allow(missing_debug_implementations)]
pub struct EventLoopState<S, AS>
where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
global_data: GlobalData<S, AS>,
main_thread_channel_receiver: mpsc::Receiver<TerminalWindowMainThreadSignal<AS>>,
component_registry_map: ComponentRegistryMap<S, AS>,
has_focus: HasFocus,
telemetry: Telemetry<{ telemetry_default_constants::RING_BUFFER_SIZE }>,
}
impl<S, AS> EventLoopState<S, AS>
where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
#[allow(clippy::needless_pass_by_value)]
fn initialize(
state: S,
initial_size: Size,
output_device: OutputDevice,
app: &mut BoxedSafeApp<S, AS>,
) -> CommonResult<Self> {
let (main_thread_channel_sender, main_thread_channel_receiver) =
mpsc::channel::<TerminalWindowMainThreadSignal<AS>>(
DefaultSize::MainThreadSignalChannelBufferSize.into(),
);
let global_data = GlobalData::try_to_create_instance(
main_thread_channel_sender,
state,
initial_size,
output_device.clone(),
OffscreenBufferPool::new(initial_size),
)?;
let component_registry_map = ComponentRegistryMap::default();
let has_focus = HasFocus::default();
let telemetry = Telemetry::new((
DefaultTiming::TelemetryRateLimitTimeThresholdMicros.into(),
DefaultTiming::TelemetryFilterLowestResponseTimeMinMicros.into(),
));
let mut event_loop_state = Self {
global_data,
main_thread_channel_receiver,
component_registry_map,
has_focus,
telemetry,
};
event_loop_state.initialize_app_and_render(app, &output_device)?;
Ok(event_loop_state)
}
fn initialize_app_and_render(
&mut self,
app: &mut BoxedSafeApp<S, AS>,
output_device: &OutputDevice,
) -> CommonResult<()> {
RawMode::start(
self.global_data.window_size,
lock_output_device_as_mut!(output_device),
output_device.is_mock,
);
let telemetry = &mut self.telemetry;
telemetry_record!(
@telemetry: telemetry,
@hint: TelemetryAtomHint::Render,
@block: {
app.app_init(&mut self.component_registry_map, &mut self.has_focus);
AppManager::render_app(
app,
&mut self.global_data,
&mut self.component_registry_map,
&mut self.has_focus,
lock_output_device_as_mut!(output_device),
output_device.is_mock,
)?;
},
@after_block: {
self.global_data.set_hud_report(telemetry.report());
}
);
self.log_startup_info();
Ok(())
}
fn log_startup_info(&self) {
(DISPLAY_LOG_TELEMETRY || DEBUG_TUI_MOD).then(|| {
tracing::info!(
message = %inline_string!(
"main_event_loop {sp} Startup {ch}",
sp = glyphs::RIGHT_ARROW_GLYPH,
ch = glyphs::CELEBRATE_GLYPH
),
global_data_mut_ref = ?self.global_data,
);
});
}
fn log_shutdown_info(&self) {
(DISPLAY_LOG_TELEMETRY || DEBUG_TUI_MOD).then(|| {
tracing::info!(
message = %inline_string!(
"main_event_loop {sp} Shutdown {ch}",
ch = glyphs::BYE_GLYPH,
sp = glyphs::RIGHT_ARROW_GLYPH,
),
session_duration = %self.telemetry.session_duration()
);
});
}
pub fn log_telemetry_info(&mut self) {
(DISPLAY_LOG_TELEMETRY || DEBUG_TUI_MOD).then(|| {
tracing::info!(
message = %inline_string!(
"AppManager::render_app() ok {ch}",
ch = glyphs::PAINT_GLYPH
),
window_size = ?self.global_data.window_size,
state = %self.global_data.state,
report = %self.global_data.get_hud_report_no_spinner(),
);
if let Some(ref mut offscreen_buffer) =
self.global_data.maybe_saved_offscreen_buffer
{
let mem_used = inline_string!(
"mem used: {size}",
size = offscreen_buffer.get_mem_size_cached()
);
tracing::info!(
message = %inline_string!(
"AppManager::render_app() offscreen_buffer {mem_used} {ch}",
mem_used = mem_used,
ch = glyphs::SCREEN_BUFFER_GLYPH
),
);
}
});
}
}
async fn run_main_event_loop<S, AS>(
mut event_loop_state: EventLoopState<S, AS>,
mut app: BoxedSafeApp<S, AS>,
exit_keys: &[InputEvent],
mut input_device: InputDevice,
output_device: OutputDevice,
) -> CommonResult<(GlobalData<S, AS>, InputDevice, OutputDevice)>
where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
loop {
tokio::select! {
maybe_signal = event_loop_state.main_thread_channel_receiver.recv() => {
if let Some(signal) = maybe_signal
&& handle_main_thread_signal(
signal,
&mut event_loop_state,
&mut app,
exit_keys,
&output_device,
)? {
break; }
}
maybe_input_event = input_device.next_input_event() => {
if let Some(input_event) = maybe_input_event {
handle_input_event(
input_event,
&mut event_loop_state,
&mut app,
exit_keys,
&output_device,
);
} else {
break;
}
}
}
event_loop_state.log_telemetry_info();
}
event_loop_state.log_shutdown_info();
Ok((event_loop_state.global_data, input_device, output_device))
}
fn handle_main_thread_signal<S, AS>(
signal: TerminalWindowMainThreadSignal<AS>,
event_loop_state: &mut EventLoopState<S, AS>,
app: &mut BoxedSafeApp<S, AS>,
exit_keys: &[InputEvent],
output_device: &OutputDevice,
) -> CommonResult<bool>
where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
match signal {
TerminalWindowMainThreadSignal::Exit => {
RawMode::end(
event_loop_state.global_data.window_size,
lock_output_device_as_mut!(output_device),
output_device.is_mock,
);
Ok(true) }
TerminalWindowMainThreadSignal::Render(_) => {
handle_render_signal(event_loop_state, app, output_device)?;
Ok(false)
}
TerminalWindowMainThreadSignal::ApplyAppSignal(action) => {
handle_app_signal(&action, event_loop_state, app, exit_keys, output_device);
Ok(false)
}
}
}
fn handle_render_signal<S, AS>(
event_loop_state: &mut EventLoopState<S, AS>,
app: &mut BoxedSafeApp<S, AS>,
output_device: &OutputDevice,
) -> CommonResult<()>
where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
let telemetry = &mut event_loop_state.telemetry;
telemetry_record!(
@telemetry: telemetry,
@hint: TelemetryAtomHint::Render,
@block: {
AppManager::render_app(
app,
&mut event_loop_state.global_data,
&mut event_loop_state.component_registry_map,
&mut event_loop_state.has_focus,
lock_output_device_as_mut!(output_device),
output_device.is_mock,
)?;
},
@after_block: {
event_loop_state.global_data.set_hud_report(telemetry.report());
}
);
Ok(())
}
fn handle_app_signal<S, AS>(
action: &AS,
event_loop_state: &mut EventLoopState<S, AS>,
app: &mut BoxedSafeApp<S, AS>,
exit_keys: &[InputEvent],
output_device: &OutputDevice,
) where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
let telemetry = &mut event_loop_state.telemetry;
telemetry_record!(
@telemetry: telemetry,
@hint: TelemetryAtomHint::Signal,
@block: {
let result = app.app_handle_signal(
action,
&mut event_loop_state.global_data,
&mut event_loop_state.component_registry_map,
&mut event_loop_state.has_focus,
);
handle_result_generated_by_app_after_handling_action_or_input_event(
result,
None,
exit_keys,
app,
&mut event_loop_state.global_data,
&mut event_loop_state.component_registry_map,
&mut event_loop_state.has_focus,
lock_output_device_as_mut!(output_device),
output_device.is_mock,
);
},
@after_block: {
event_loop_state.global_data.set_hud_report(telemetry.report());
}
);
}
fn handle_input_event<S, AS>(
input_event: InputEvent,
event_loop_state: &mut EventLoopState<S, AS>,
app: &mut BoxedSafeApp<S, AS>,
exit_keys: &[InputEvent],
output_device: &OutputDevice,
) where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
log_input_event_if_enabled(&input_event);
if let InputEvent::Resize(new_size) = input_event {
handle_resize_event(new_size, event_loop_state, app, output_device);
}
process_input_event(input_event, event_loop_state, app, exit_keys, output_device);
}
fn log_input_event_if_enabled(input_event: &InputEvent) {
(DISPLAY_LOG_TELEMETRY || DEBUG_TUI_MOD).then(|| {
if let InputEvent::Keyboard(_) = input_event {
tracing::info!(
message = %inline_string!(
"main_event_loop {sp} Tick {ch}",
sp = glyphs::RIGHT_ARROW_GLYPH,
ch = glyphs::CLOCK_TICK_GLYPH
),
input_event = ?input_event
);
}
});
}
fn handle_resize_event<S, AS>(
new_size: Size,
event_loop_state: &mut EventLoopState<S, AS>,
app: &mut BoxedSafeApp<S, AS>,
output_device: &OutputDevice,
) where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
let telemetry = &mut event_loop_state.telemetry;
telemetry_record!(
@telemetry: telemetry,
@hint: TelemetryAtomHint::Resize,
@block: {
handle_resize(
new_size,
&mut event_loop_state.global_data,
app,
&mut event_loop_state.component_registry_map,
&mut event_loop_state.has_focus,
lock_output_device_as_mut!(output_device),
output_device.is_mock,
);
},
@after_block: {
event_loop_state.global_data.set_hud_report(telemetry.report());
}
);
}
fn process_input_event<S, AS>(
input_event: InputEvent,
event_loop_state: &mut EventLoopState<S, AS>,
app: &mut BoxedSafeApp<S, AS>,
exit_keys: &[InputEvent],
output_device: &OutputDevice,
) where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
let telemetry = &mut event_loop_state.telemetry;
telemetry_record!(
@telemetry: telemetry,
@hint: TelemetryAtomHint::Input,
@block: {
actually_process_input_event(
&mut event_loop_state.global_data,
app,
input_event,
exit_keys,
&mut event_loop_state.component_registry_map,
&mut event_loop_state.has_focus,
lock_output_device_as_mut!(output_device),
output_device.is_mock,
);
},
@after_block: {
event_loop_state.global_data.set_hud_report(telemetry.report());
}
);
}
#[allow(clippy::too_many_arguments)]
fn actually_process_input_event<S, AS>(
global_data_mut_ref: &mut GlobalData<S, AS>,
app: &mut BoxedSafeApp<S, AS>,
input_event: InputEvent,
exit_keys: &[InputEvent],
component_registry_map: &mut ComponentRegistryMap<S, AS>,
has_focus: &mut HasFocus,
locked_output_device: LockedOutputDevice<'_>,
is_mock: bool,
) where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
let result = app.app_handle_input_event(
input_event.clone(),
global_data_mut_ref,
component_registry_map,
has_focus,
);
handle_result_generated_by_app_after_handling_action_or_input_event(
result,
Some(input_event),
exit_keys,
app,
global_data_mut_ref,
component_registry_map,
has_focus,
locked_output_device,
is_mock,
);
}
#[allow(clippy::implicit_hasher)]
pub fn handle_resize<S, AS>(
new_size: Size,
global_data_mut_ref: &mut GlobalData<S, AS>,
app: &mut BoxedSafeApp<S, AS>,
component_registry_map: &mut ComponentRegistryMap<S, AS>,
has_focus: &mut HasFocus,
locked_output_device: LockedOutputDevice<'_>,
is_mock: bool,
) where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send,
{
global_data_mut_ref.set_size(new_size);
global_data_mut_ref.maybe_saved_offscreen_buffer = None;
global_data_mut_ref.offscreen_buffer_pool.resize(new_size);
AppManager::render_app(
app,
global_data_mut_ref,
component_registry_map,
has_focus,
locked_output_device,
is_mock,
)
.ok();
}
#[allow(clippy::too_many_arguments)]
fn handle_result_generated_by_app_after_handling_action_or_input_event<S, AS>(
result: CommonResult<EventPropagation>,
maybe_input_event: Option<InputEvent>,
exit_keys: &[InputEvent],
app: &mut BoxedSafeApp<S, AS>,
global_data_mut_ref: &mut GlobalData<S, AS>,
component_registry_map: &mut ComponentRegistryMap<S, AS>,
has_focus: &mut HasFocus,
locked_output_device: LockedOutputDevice<'_>,
is_mock: bool,
) where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send + 'static,
{
let main_thread_channel_sender =
global_data_mut_ref.main_thread_channel_sender.clone();
match result {
Ok(event_propagation) => match event_propagation {
EventPropagation::Propagate => {
if let Some(input_event) = maybe_input_event {
let check_if_exit_keys_pressed =
DefaultInputEventHandler::no_consume(input_event, exit_keys);
if let Continuation::Exit = check_if_exit_keys_pressed {
request_exit_by_sending_signal(main_thread_channel_sender);
}
}
}
EventPropagation::ConsumedRender => {
AppManager::render_app(
app,
global_data_mut_ref,
component_registry_map,
has_focus,
locked_output_device,
is_mock,
)
.ok();
}
EventPropagation::Consumed => {}
EventPropagation::ExitMainEventLoop => {
request_exit_by_sending_signal(main_thread_channel_sender);
}
},
Err(error) => {
tracing::error!(
message = %inline_string!(
"main_event_loop {sp} handle_result_generated_by_app_after_handling_action_or_input_event {ch}",
ch = glyphs::SUSPICIOUS_GLYPH,
sp = glyphs::RIGHT_ARROW_GLYPH,
),
error = ?error
);
}
}
}
fn request_exit_by_sending_signal<AS>(
channel_sender: mpsc::Sender<TerminalWindowMainThreadSignal<AS>>,
) where
AS: Debug + Default + Clone + Sync + Send + 'static,
{
tokio::spawn(async move {
channel_sender
.send(TerminalWindowMainThreadSignal::Exit)
.await
.ok();
});
}
struct AppManager<S, AS>
where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send,
{
_phantom: PhantomData<(S, AS)>,
}
impl<S, AS> AppManager<S, AS>
where
S: Display + Debug + Default + Clone + Sync + Send,
AS: Debug + Default + Clone + Sync + Send,
{
pub fn render_app(
app: &mut BoxedSafeApp<S, AS>,
global_data_mut_ref: &mut GlobalData<S, AS>,
component_registry_map: &mut ComponentRegistryMap<S, AS>,
has_focus: &mut HasFocus,
locked_output_device: LockedOutputDevice<'_>,
is_mock: bool,
) -> CommonResult<()> {
let window_size = global_data_mut_ref.window_size;
let render_result = match window_size
.fits_min_size(width(MinSize::Col as u8) + height(MinSize::Row as u8))
{
SufficientSize::IsLargeEnough => {
app.app_render(global_data_mut_ref, component_registry_map, has_focus)
}
SufficientSize::IsTooSmall => {
global_data_mut_ref.maybe_saved_offscreen_buffer = None;
Ok(render_window_too_small_error(window_size))
}
};
match render_result {
Err(error) => {
RenderOp::default().flush(locked_output_device);
DEBUG_TUI_MOD.then(|| {
tracing::error!(
message = %inline_string!(
"AppManager::render_app() error {ch}",
ch = glyphs::SUSPICIOUS_GLYPH
),
details = ?error
);
});
}
Ok(render_pipeline) => {
render_pipeline.paint(
FlushKind::ClearBeforeFlush,
global_data_mut_ref,
locked_output_device,
is_mock,
);
}
}
ok!()
}
}
fn render_window_too_small_error(window_size: Size) -> RenderPipeline {
let msg = inline_string!(
"Window size is too small. Minimum size is {} cols x {} rows",
MinSize::Col as u8,
MinSize::Row as u8
);
let msg_gcs = msg.grapheme_string();
let trunc_msg = msg_gcs.trunc_end_to_fit(window_size);
let trunc_msg_gcs = trunc_msg.grapheme_string();
let trunc_msg_width = trunc_msg_gcs.display_width;
let row_pos = row({
let it = window_size.row_height / ch(2);
*it
});
let col_pos = col({
let it = (window_size.col_width - trunc_msg_width) / ch(2);
*it
});
let mut pipeline = render_pipeline!();
let style_bold = new_style!(bold);
render_pipeline! {
@push_into pipeline
at ZOrder::Normal
=>
RenderOp::ResetColor,
RenderOp::MoveCursorPositionAbs(col_pos + row_pos)
}
render_pipeline! {
@push_styled_texts_into pipeline
at ZOrder::Normal
=>
ColorWheel::new(smallvec![
ColorWheelConfig::RgbRandom(ColorWheelSpeed::Fast),
ColorWheelConfig::Ansi256(Ansi256GradientIndex::DarkRedToDarkMagenta, ColorWheelSpeed::Medium),
])
.colorize_into_styled_texts(
&trunc_msg_gcs,
GradientGenerationPolicy::RegenerateGradientAndIndexBasedOnTextLength,
TextColorizationPolicy::ColorEachCharacter(Some(style_bold)),
)
}
pipeline
}
#[cfg(test)]
mod tests {
use std::{fmt::{Debug, Display, Formatter},
time::Duration};
use smallvec::smallvec;
use test_fixture_app::AppMainTest;
use test_fixture_state::{AppSignal, State};
use crate::{assert_eq2, ch, col, defaults::get_default_gradient_stops, height,
inline_string, is_fully_uninteractive_terminal, key_press,
main_event_loop_impl, new_style, ok, render_ops, render_pipeline,
render_tui_styled_texts_into, send_signal, tui_color, tui_style_attrib,
tui_styled_text, width, App, ColorWheel, ColorWheelConfig,
ColorWheelSpeed, CommonResult, ComponentRegistryMap,
CrosstermEventResult, EventPropagation, GlobalData,
GradientGenerationPolicy, GradientLengthKind, HasFocus, InlineVec,
InputDevice, InputDeviceExtMock, InputEvent, Key, KeyPress,
OutputDevice, OutputDeviceExt, PixelChar, RenderOp, RenderPipeline,
SpecialKey, TTYResult, TerminalWindowMainThreadSignal,
TextColorizationPolicy, TuiStyle, ZOrder};
#[tokio::test]
#[allow(clippy::needless_return)]
async fn test_main_event_loop_impl() -> CommonResult<()> {
let app = Box::<AppMainTest>::default();
let exit_keys: InlineVec<InputEvent> =
smallvec![InputEvent::Keyboard(key_press! { @char 'x' })];
let generator_vec: InlineVec<CrosstermEventResult> = smallvec![
Ok(crossterm::event::Event::Key(
crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Up,
crossterm::event::KeyModifiers::empty(),
),
)),
Ok(crossterm::event::Event::Key(
crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Up,
crossterm::event::KeyModifiers::empty(),
),
)),
Ok(crossterm::event::Event::Key(
crossterm::event::KeyEvent::new(
crossterm::event::KeyCode::Char('x'),
crossterm::event::KeyModifiers::empty(),
),
)),
];
let initial_size = width(65) + height(11);
let input_device =
InputDevice::new_mock_with_delay(generator_vec, Duration::from_millis(10));
let (output_device, stdout_mock) = OutputDevice::new_mock();
let state = State::default();
let (global_data, _, _) = main_event_loop_impl(
app,
&exit_keys,
state,
initial_size,
input_device,
output_device,
)
.await?;
assert_eq!(global_data.state.counter, 2);
assert!(stdout_mock
.get_copy_of_buffer_as_string_strip_ansi()
.contains("State{counter:2}"));
let my_offscreen_buffer = global_data.maybe_saved_offscreen_buffer.unwrap();
if let TTYResult::IsNotInteractive = is_fully_uninteractive_terminal() {
{
let PixelChar::PlainText {
display_char,
maybe_style: _,
} = my_offscreen_buffer.buffer[4][7]
else {
panic!(
"Expected PixelChar::PlainText, got: {:?}",
my_offscreen_buffer.buffer[4][7]
);
};
assert_eq2!(display_char.to_string(), "S");
}
{
let PixelChar::PlainText {
display_char,
maybe_style: _,
} = my_offscreen_buffer.buffer[10][7]
else {
panic!(
"Expected PixelChar::PlainText, got: {:?}",
my_offscreen_buffer.buffer[10][7]
);
};
assert_eq2!(display_char.to_string(), "H");
}
}
else {
{
assert_eq2!(
PixelChar::PlainText {
display_char: 'S',
maybe_style: Some(TuiStyle {
color_fg: Some(tui_color!(102, 0, 255)),
..Default::default()
}),
},
my_offscreen_buffer.buffer[4][7].clone()
);
}
{
assert_eq2!(
PixelChar::PlainText {
display_char: 'H',
maybe_style: Some(TuiStyle {
id: None,
dim: Some(tui_style_attrib::Dim),
..Default::default()
}),
},
my_offscreen_buffer.buffer[10][7].clone()
);
}
}
ok!()
}
mod test_fixture_state {
use super::*;
#[derive(Default, Clone, Debug)]
#[non_exhaustive]
#[allow(dead_code)]
pub enum AppSignal {
Add,
Sub,
Clear,
#[default]
Noop,
}
#[derive(Clone, PartialEq, Eq, Default)]
pub struct State {
pub counter: isize,
}
impl Display for State {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "State{{counter:{}}}", self.counter)
}
}
impl Debug for State {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "State {{ counter: {:?} }}", self.counter)
}
}
}
mod test_fixture_app {
use crate::ColorWheel;
#[derive(Default)]
pub struct AppMainTest {
pub data: AppDataTest,
}
#[derive(Default)]
pub struct AppDataTest {
pub color_wheel_rgb: ColorWheel,
}
}
mod test_fixture_app_main_impl_trait_app {
use super::*;
use crate::{row, throws_with_return, GCStringExt, Pos};
impl App for AppMainTest {
type S = State;
type AS = AppSignal;
fn app_render(
&mut self,
global_data_mut_ref: &mut GlobalData<State, AppSignal>,
_component_registry_map: &mut ComponentRegistryMap<State, AppSignal>,
_has_focus: &mut HasFocus,
) -> CommonResult<RenderPipeline> {
throws_with_return!({
let state_string =
inline_string!("{a:?}", a = global_data_mut_ref.state);
let data = &mut self.data;
let sample_line_of_text =
format!("{state_string}, gradient: [index: X, len: Y]");
let content_size_col = width(sample_line_of_text.len());
let window_size = global_data_mut_ref.window_size;
let col_idx = col({
let it = window_size.col_width - content_size_col;
*it / ch(2)
});
let mut row_idx = row({
let it = window_size.row_height - height(2);
*it / ch(2)
});
let mut pipeline = render_pipeline!();
pipeline.push(ZOrder::Normal, {
let mut acc_render_op = render_ops! {
@new
RenderOp::ResetColor,
};
acc_render_op += RenderOp::MoveCursorPositionAbs(Pos {
col_index: col_idx,
row_index: row_idx,
});
let index = data.color_wheel_rgb.get_index();
let len = match data.color_wheel_rgb.get_gradient_len() {
GradientLengthKind::ColorWheel(len) => len,
_ => 0,
};
let string = inline_string!(
"{state_string}, gradient: [index: {a:?}, len: {b}]",
a = index,
b = len
);
let string_gcs = string.grapheme_string();
render_ops!(
@render_styled_texts_into acc_render_op
=>
data.color_wheel_rgb.colorize_into_styled_texts(
&string_gcs,
GradientGenerationPolicy::ReuseExistingGradientAndIndex,
TextColorizationPolicy::ColorEachWord(None),
)
);
*row_idx += 1;
acc_render_op
});
text_fixture_status_bar::create_status_bar_message(
&mut pipeline,
window_size,
);
pipeline
});
}
fn app_handle_input_event(
&mut self,
input_event: InputEvent,
global_data_mut_ref: &mut GlobalData<State, AppSignal>,
_component_registry_map: &mut ComponentRegistryMap<State, AppSignal>,
_has_focus: &mut HasFocus,
) -> CommonResult<EventPropagation> {
throws_with_return!({
let mut event_consumed = false;
if let InputEvent::Keyboard(KeyPress::Plain { key }) = input_event {
if let Key::Character(typed_char) = key {
match typed_char {
'+' => {
event_consumed = true;
send_signal!(
global_data_mut_ref.main_thread_channel_sender,
TerminalWindowMainThreadSignal::ApplyAppSignal(
AppSignal::Add,
)
);
}
'-' => {
event_consumed = true;
send_signal!(
global_data_mut_ref.main_thread_channel_sender,
TerminalWindowMainThreadSignal::ApplyAppSignal(
AppSignal::Sub,
)
);
}
_ => {}
}
}
if let Key::SpecialKey(special_key) = key {
match special_key {
SpecialKey::Up => {
event_consumed = true;
send_signal!(
global_data_mut_ref.main_thread_channel_sender,
TerminalWindowMainThreadSignal::ApplyAppSignal(
AppSignal::Add,
)
);
}
SpecialKey::Down => {
event_consumed = true;
send_signal!(
global_data_mut_ref.main_thread_channel_sender,
TerminalWindowMainThreadSignal::ApplyAppSignal(
AppSignal::Sub,
)
);
}
_ => {}
}
}
}
if event_consumed {
EventPropagation::ConsumedRender
} else {
EventPropagation::Propagate
}
});
}
fn app_handle_signal(
&mut self,
action: &AppSignal,
global_data_mut_ref: &mut GlobalData<State, AppSignal>,
_component_registry_map: &mut ComponentRegistryMap<State, AppSignal>,
_has_focus: &mut HasFocus,
) -> CommonResult<EventPropagation> {
throws_with_return!({
let GlobalData { state, .. } = global_data_mut_ref;
match action {
AppSignal::Add => {
state.counter += 1;
}
AppSignal::Sub => {
state.counter -= 1;
}
AppSignal::Clear => {
state.counter = 0;
}
_ => {}
}
EventPropagation::ConsumedRender
});
}
fn app_init(
&mut self,
_component_registry_map: &mut ComponentRegistryMap<State, AppSignal>,
_has_focus: &mut HasFocus,
) {
let data = &mut self.data;
data.color_wheel_rgb = ColorWheel::new(smallvec![ColorWheelConfig::Rgb(
get_default_gradient_stops(),
ColorWheelSpeed::Fast,
25,
)]);
}
}
}
mod text_fixture_status_bar {
use super::*;
use crate::{tui_styled_texts, Size};
pub fn create_status_bar_message(pipeline: &mut RenderPipeline, size: Size) {
let styled_texts = tui_styled_texts! {
tui_styled_text!{ @style: new_style!(dim) , @text: "Hints:"},
tui_styled_text!{ @style: new_style!(bold) , @text: " x : Exit 🖖 "},
tui_styled_text!{ @style: new_style!(dim) , @text: " … "},
tui_styled_text!{ @style: new_style!(underline) , @text: " ↑ / + : inc "},
tui_styled_text!{ @style: new_style!(dim) , @text: " … "},
tui_styled_text!{ @style: new_style!(underline) , @text: " ↓ / - : dec "},
};
let display_width = styled_texts.display_width();
let col_center = *(size.col_width - display_width) / ch(2);
let row_bottom = size.row_height.convert_to_row_index();
let center = col(col_center) + row_bottom;
let mut render_ops = render_ops!();
render_ops.push(RenderOp::MoveCursorPositionAbs(center));
render_tui_styled_texts_into(&styled_texts, &mut render_ops);
pipeline.push(ZOrder::Normal, render_ops);
}
}
}