mod render;
mod state;
mod types;
use std::marker::PhantomData;
use super::{Component, EventContext, InputFieldMessage, RenderContext};
use crate::input::{Event, Key};
pub use state::EventStreamState;
pub use types::{EventLevel, StreamEvent};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
enum Focus {
#[default]
List,
Search,
}
#[derive(Clone, Debug, PartialEq)]
pub enum EventStreamMessage {
PushEvent(StreamEvent),
SetEvents(Vec<StreamEvent>),
Clear,
SetFilter(String),
SetLevelFilter(Option<EventLevel>),
SetSourceFilter(Option<String>),
SetVisibleColumns(Vec<String>),
ScrollUp,
ScrollDown,
ScrollToTop,
ScrollToBottom,
ToggleAutoScroll,
FocusSearch,
FocusList,
SearchInput(char),
SearchBackspace,
SearchDelete,
SearchLeft,
SearchRight,
SearchHome,
SearchEnd,
ClearSearch,
QuickLevelFilter(u8),
}
#[derive(Clone, Debug, PartialEq)]
pub enum EventStreamOutput {
EventAdded(u64),
FilterChanged,
EventsCleared,
}
pub struct EventStream(PhantomData<()>);
impl Component for EventStream {
type State = EventStreamState;
type Message = EventStreamMessage;
type Output = EventStreamOutput;
fn init() -> Self::State {
EventStreamState::default()
}
fn handle_event(
state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
let key = event.as_key()?;
match state.focus {
Focus::List => match key.code {
Key::Up | Key::Char('k') => Some(EventStreamMessage::ScrollUp),
Key::Down | Key::Char('j') => Some(EventStreamMessage::ScrollDown),
Key::Char('g') if key.modifiers.shift() => Some(EventStreamMessage::ScrollToBottom),
Key::Home | Key::Char('g') => Some(EventStreamMessage::ScrollToTop),
Key::End => Some(EventStreamMessage::ScrollToBottom),
Key::Char('/') => Some(EventStreamMessage::FocusSearch),
Key::Char('1') => Some(EventStreamMessage::QuickLevelFilter(1)),
Key::Char('2') => Some(EventStreamMessage::QuickLevelFilter(2)),
Key::Char('3') => Some(EventStreamMessage::QuickLevelFilter(3)),
Key::Char('4') => Some(EventStreamMessage::QuickLevelFilter(4)),
Key::Char('5') => Some(EventStreamMessage::QuickLevelFilter(5)),
Key::Char('f') => Some(EventStreamMessage::ToggleAutoScroll),
_ => None,
},
Focus::Search => match key.code {
Key::Esc => Some(EventStreamMessage::ClearSearch),
Key::Enter => Some(EventStreamMessage::FocusList),
Key::Char(c) => {
if key.modifiers.ctrl() {
None
} else {
Some(EventStreamMessage::SearchInput(c))
}
}
Key::Backspace => Some(EventStreamMessage::SearchBackspace),
Key::Delete => Some(EventStreamMessage::SearchDelete),
Key::Left => Some(EventStreamMessage::SearchLeft),
Key::Right => Some(EventStreamMessage::SearchRight),
Key::Home => Some(EventStreamMessage::SearchHome),
Key::End => Some(EventStreamMessage::SearchEnd),
_ => None,
},
}
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
EventStreamMessage::PushEvent(event) => {
let id = if event.id == 0 {
let new_id = state.next_id;
state.next_id += 1;
let mut ev = event;
ev.id = new_id;
state.events.push(ev);
new_id
} else {
let id = event.id;
if event.id >= state.next_id {
state.next_id = event.id + 1;
}
state.events.push(event);
id
};
state.evict_oldest();
if state.auto_scroll {
let len = state.visible_events().len();
state.scroll.set_content_length(len);
state.scroll.scroll_to_end();
}
Some(EventStreamOutput::EventAdded(id))
}
EventStreamMessage::SetEvents(events) => {
state.events = events;
state.next_id = state.events.iter().map(|e| e.id + 1).max().unwrap_or(0);
state.scroll.set_offset(0);
None
}
EventStreamMessage::Clear => {
state.clear();
Some(EventStreamOutput::EventsCleared)
}
EventStreamMessage::SetFilter(filter) => {
state.filter_text = filter;
state.scroll.set_offset(0);
Some(EventStreamOutput::FilterChanged)
}
EventStreamMessage::SetLevelFilter(level) => {
state.level_filter = level;
state.scroll.set_offset(0);
Some(EventStreamOutput::FilterChanged)
}
EventStreamMessage::SetSourceFilter(source) => {
state.source_filter = source;
state.scroll.set_offset(0);
Some(EventStreamOutput::FilterChanged)
}
EventStreamMessage::SetVisibleColumns(columns) => {
state.visible_columns = columns;
None
}
EventStreamMessage::ScrollUp => {
let len = state.visible_events().len();
state.scroll.set_content_length(len);
state.scroll.set_viewport_height(1.min(len));
state.scroll.scroll_up();
state.auto_scroll = false;
None
}
EventStreamMessage::ScrollDown => {
let len = state.visible_events().len();
state.scroll.set_content_length(len);
state.scroll.set_viewport_height(1.min(len));
state.scroll.scroll_down();
None
}
EventStreamMessage::ScrollToTop => {
let len = state.visible_events().len();
state.scroll.set_content_length(len);
state.scroll.set_viewport_height(1.min(len));
state.scroll.scroll_to_start();
state.auto_scroll = false;
None
}
EventStreamMessage::ScrollToBottom => {
let len = state.visible_events().len();
state.scroll.set_content_length(len);
state.scroll.set_viewport_height(1.min(len));
state.scroll.scroll_to_end();
None
}
EventStreamMessage::ToggleAutoScroll => {
state.auto_scroll = !state.auto_scroll;
if state.auto_scroll {
let len = state.visible_events().len();
state.scroll.set_content_length(len);
state.scroll.scroll_to_end();
}
None
}
EventStreamMessage::FocusSearch => {
state.focus = Focus::Search;
None
}
EventStreamMessage::FocusList => {
state.focus = Focus::List;
None
}
EventStreamMessage::SearchInput(c) => {
state.search.update(InputFieldMessage::Insert(c));
state.filter_text = state.search.value().to_string();
state.scroll.set_offset(0);
Some(EventStreamOutput::FilterChanged)
}
EventStreamMessage::SearchBackspace => {
state.search.update(InputFieldMessage::Backspace);
state.filter_text = state.search.value().to_string();
state.scroll.set_offset(0);
Some(EventStreamOutput::FilterChanged)
}
EventStreamMessage::SearchDelete => {
state.search.update(InputFieldMessage::Delete);
state.filter_text = state.search.value().to_string();
state.scroll.set_offset(0);
Some(EventStreamOutput::FilterChanged)
}
EventStreamMessage::SearchLeft => {
state.search.update(InputFieldMessage::Left);
None
}
EventStreamMessage::SearchRight => {
state.search.update(InputFieldMessage::Right);
None
}
EventStreamMessage::SearchHome => {
state.search.update(InputFieldMessage::Home);
None
}
EventStreamMessage::SearchEnd => {
state.search.update(InputFieldMessage::End);
None
}
EventStreamMessage::ClearSearch => {
state.search.update(InputFieldMessage::Clear);
state.filter_text.clear();
state.scroll.set_offset(0);
state.focus = Focus::List;
Some(EventStreamOutput::FilterChanged)
}
EventStreamMessage::QuickLevelFilter(n) => {
let level = match n {
1 => Some(EventLevel::Trace),
2 => Some(EventLevel::Debug),
3 => Some(EventLevel::Info),
4 => Some(EventLevel::Warning),
5 => Some(EventLevel::Error),
_ => None,
};
if state.level_filter == level {
state.level_filter = None;
} else {
state.level_filter = level;
}
state.scroll.set_offset(0);
Some(EventStreamOutput::FilterChanged)
}
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
render::render_event_stream(
state,
ctx.frame,
ctx.area,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
#[cfg(test)]
mod event_tests;
#[cfg(test)]
mod snapshot_tests;
#[cfg(test)]
mod tests;