use std::marker::PhantomData;
use ratatui::widgets::{Block, Borders};
use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
pub(crate) mod render;
mod types;
pub use types::{SelectedType, TimelineEvent, TimelineMessage, TimelineOutput, TimelineSpan};
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct TimelineState {
pub(crate) events: Vec<TimelineEvent>,
pub(crate) spans: Vec<TimelineSpan>,
pub(crate) view_start: f64,
pub(crate) view_end: f64,
pub(crate) selected_index: Option<usize>,
pub(crate) selected_type: SelectedType,
pub(crate) title: Option<String>,
pub(crate) show_labels: bool,
pub(crate) lane_count: usize,
}
impl Default for TimelineState {
fn default() -> Self {
Self {
events: Vec::new(),
spans: Vec::new(),
view_start: 0.0,
view_end: 1000.0,
selected_index: None,
selected_type: SelectedType::default(),
title: None,
show_labels: true,
lane_count: 0,
}
}
}
impl TimelineState {
pub fn new() -> Self {
Self::default()
}
pub fn with_events(mut self, events: Vec<TimelineEvent>) -> Self {
self.events = events;
self
}
pub fn with_spans(mut self, spans: Vec<TimelineSpan>) -> Self {
self.spans = spans;
self
}
pub fn with_view_range(mut self, start: f64, end: f64) -> Self {
self.view_start = start;
self.view_end = end;
self
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_show_labels(mut self, show: bool) -> Self {
self.show_labels = show;
self
}
pub fn events(&self) -> &[TimelineEvent] {
&self.events
}
pub fn events_mut(&mut self) -> &mut Vec<TimelineEvent> {
&mut self.events
}
pub fn spans(&self) -> &[TimelineSpan] {
&self.spans
}
pub fn spans_mut(&mut self) -> &mut Vec<TimelineSpan> {
&mut self.spans
}
pub fn add_event(&mut self, event: TimelineEvent) {
self.events.push(event);
}
pub fn add_span(&mut self, span: TimelineSpan) {
self.spans.push(span);
}
pub fn clear(&mut self) {
self.events.clear();
self.spans.clear();
self.selected_index = None;
}
pub fn view_range(&self) -> (f64, f64) {
(self.view_start, self.view_end)
}
pub fn set_view_range(&mut self, start: f64, end: f64) {
self.view_start = start;
self.view_end = end;
}
pub fn fit_all(&mut self) {
let (data_min, data_max) = self.data_bounds();
if data_min >= data_max {
self.view_start = 0.0;
self.view_end = 1000.0;
return;
}
let range = data_max - data_min;
let padding = range * 0.05;
self.view_start = data_min - padding;
self.view_end = data_max + padding;
}
pub fn zoom_in(&mut self) {
let range = self.view_end - self.view_start;
let center = self.view_start + range / 2.0;
let new_range = range * 0.75;
if new_range < 1.0 {
return;
}
self.view_start = center - new_range / 2.0;
self.view_end = center + new_range / 2.0;
}
pub fn zoom_out(&mut self) {
let range = self.view_end - self.view_start;
let center = self.view_start + range / 2.0;
let new_range = range / 0.75;
self.view_start = center - new_range / 2.0;
self.view_end = center + new_range / 2.0;
}
pub fn selected_event(&self) -> Option<&TimelineEvent> {
if self.selected_type == SelectedType::Event {
self.selected_index.and_then(|idx| self.events.get(idx))
} else {
None
}
}
pub fn selected_span(&self) -> Option<&TimelineSpan> {
if self.selected_type == SelectedType::Span {
self.selected_index.and_then(|idx| self.spans.get(idx))
} else {
None
}
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.title = Some(title.into());
}
pub fn show_labels(&self) -> bool {
self.show_labels
}
pub fn set_show_labels(&mut self, show: bool) {
self.show_labels = show;
}
pub fn effective_lane_count(&self) -> usize {
if self.lane_count > 0 {
self.lane_count
} else {
self.spans
.iter()
.map(|s| s.lane)
.max()
.map(|m| m + 1)
.unwrap_or(0)
}
}
fn data_bounds(&self) -> (f64, f64) {
let event_min = self.events.iter().map(|e| e.timestamp).reduce(f64::min);
let event_max = self.events.iter().map(|e| e.timestamp).reduce(f64::max);
let span_min = self.spans.iter().map(|s| s.start).reduce(f64::min);
let span_max = self.spans.iter().map(|s| s.end).reduce(f64::max);
let min = [event_min, span_min]
.into_iter()
.flatten()
.reduce(f64::min)
.unwrap_or(0.0);
let max = [event_max, span_max]
.into_iter()
.flatten()
.reduce(f64::max)
.unwrap_or(0.0);
(min, max)
}
fn select_next(&mut self) -> Option<TimelineOutput> {
let total = self.events.len() + self.spans.len();
if total == 0 {
return None;
}
let current_flat = self.flat_index();
let next_flat = match current_flat {
Some(idx) => (idx + 1) % total,
None => 0,
};
self.set_flat_index(next_flat);
self.selection_output()
}
fn select_prev(&mut self) -> Option<TimelineOutput> {
let total = self.events.len() + self.spans.len();
if total == 0 {
return None;
}
let current_flat = self.flat_index();
let prev_flat = match current_flat {
Some(idx) => {
if idx == 0 {
total - 1
} else {
idx - 1
}
}
None => total - 1,
};
self.set_flat_index(prev_flat);
self.selection_output()
}
fn flat_index(&self) -> Option<usize> {
self.selected_index.map(|idx| match self.selected_type {
SelectedType::Event => idx,
SelectedType::Span => self.events.len() + idx,
})
}
fn set_flat_index(&mut self, flat: usize) {
if flat < self.events.len() {
self.selected_type = SelectedType::Event;
self.selected_index = Some(flat);
} else {
self.selected_type = SelectedType::Span;
self.selected_index = Some(flat - self.events.len());
}
}
fn selection_output(&self) -> Option<TimelineOutput> {
match self.selected_type {
SelectedType::Event => self
.selected_event()
.map(|e| TimelineOutput::EventSelected(e.id.clone())),
SelectedType::Span => self
.selected_span()
.map(|s| TimelineOutput::SpanSelected(s.id.clone())),
}
}
pub fn update(&mut self, msg: TimelineMessage) -> Option<TimelineOutput> {
Timeline::update(self, msg)
}
}
pub struct Timeline(PhantomData<()>);
impl Component for Timeline {
type State = TimelineState;
type Message = TimelineMessage;
type Output = TimelineOutput;
fn init() -> Self::State {
TimelineState::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 key.code {
Key::Left | Key::Char('h') => Some(TimelineMessage::PanLeft),
Key::Right | Key::Char('l') => Some(TimelineMessage::PanRight),
Key::Char('+') | Key::Char('=') => Some(TimelineMessage::ZoomIn),
Key::Char('-') => Some(TimelineMessage::ZoomOut),
Key::Up | Key::Char('k') => Some(TimelineMessage::SelectPrev),
Key::Down | Key::Char('j') => Some(TimelineMessage::SelectNext),
Key::Home => Some(TimelineMessage::FitAll),
Key::Enter => {
if state.selected_index.is_some() {
match state.selected_type {
SelectedType::Event => Some(TimelineMessage::SelectNext),
SelectedType::Span => Some(TimelineMessage::SelectNext),
}
} else {
None
}
}
_ => None,
}
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
TimelineMessage::AddEvent(event) => {
state.events.push(event);
None
}
TimelineMessage::AddSpan(span) => {
state.spans.push(span);
None
}
TimelineMessage::SetEvents(events) => {
state.events = events;
state.selected_index = None;
None
}
TimelineMessage::SetSpans(spans) => {
state.spans = spans;
state.selected_index = None;
None
}
TimelineMessage::Clear => {
state.clear();
None
}
TimelineMessage::ZoomIn => {
state.zoom_in();
Some(TimelineOutput::ViewChanged {
start: state.view_start,
end: state.view_end,
})
}
TimelineMessage::ZoomOut => {
state.zoom_out();
Some(TimelineOutput::ViewChanged {
start: state.view_start,
end: state.view_end,
})
}
TimelineMessage::PanLeft => {
let range = state.view_end - state.view_start;
let shift = range * 0.1;
state.view_start -= shift;
state.view_end -= shift;
Some(TimelineOutput::ViewChanged {
start: state.view_start,
end: state.view_end,
})
}
TimelineMessage::PanRight => {
let range = state.view_end - state.view_start;
let shift = range * 0.1;
state.view_start += shift;
state.view_end += shift;
Some(TimelineOutput::ViewChanged {
start: state.view_start,
end: state.view_end,
})
}
TimelineMessage::FitAll => {
state.fit_all();
Some(TimelineOutput::ViewChanged {
start: state.view_start,
end: state.view_end,
})
}
TimelineMessage::SelectNext => state.select_next(),
TimelineMessage::SelectPrev => state.select_prev(),
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
if ctx.area.height < 3 || ctx.area.width < 3 {
return;
}
crate::annotation::with_registry(|reg| {
reg.register(
ctx.area,
crate::annotation::Annotation::container("timeline")
.with_focus(ctx.focused)
.with_disabled(ctx.disabled),
);
});
let border_style = if ctx.disabled {
ctx.theme.disabled_style()
} else if ctx.focused {
ctx.theme.focused_border_style()
} else {
ctx.theme.border_style()
};
let mut block = Block::default()
.borders(Borders::ALL)
.border_style(border_style);
if let Some(ref title) = state.title {
block = block.title(title.as_str());
}
let inner = block.inner(ctx.area);
ctx.frame.render_widget(block, ctx.area);
if inner.height == 0 || inner.width == 0 {
return;
}
render::render_timeline(
state,
ctx.frame,
inner,
ctx.theme,
ctx.focused,
ctx.disabled,
);
}
}
#[cfg(test)]
mod snapshot_tests;
#[cfg(test)]
mod tests;