use crate::{
settings::{Color, Field, Gradient, ListGradient, SettingsDescription, Value},
CachedImageId, GeneralLayoutSettings, Timer,
};
use serde_json::{to_writer, Result};
use std::borrow::Cow;
use std::cmp::{max, min};
use std::io::Write;
#[cfg(test)]
mod tests;
mod column;
pub use column::{
ColumnSettings, ColumnStartWith, ColumnState, ColumnUpdateTrigger, ColumnUpdateWith,
};
const SETTINGS_BEFORE_COLUMNS: usize = 11;
const SETTINGS_PER_COLUMN: usize = 6;
#[derive(Default, Clone)]
pub struct Component {
icon_ids: Vec<CachedImageId>,
settings: Settings,
current_split_index: Option<usize>,
scroll_offset: isize,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Settings {
pub background: ListGradient,
pub visual_split_count: usize,
pub split_preview_count: usize,
pub show_thin_separators: bool,
pub separator_last_split: bool,
pub always_show_last_split: bool,
pub fill_with_blank_space: bool,
pub display_two_rows: bool,
pub current_split_gradient: Gradient,
pub show_column_labels: bool,
pub columns: Vec<ColumnSettings>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SplitState {
pub name: String,
pub columns: Vec<ColumnState>,
pub is_current_split: bool,
pub index: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IconChange {
pub segment_index: usize,
pub icon: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct State {
pub background: ListGradient,
pub column_labels: Option<Vec<String>>,
pub splits: Vec<SplitState>,
pub icon_changes: Vec<IconChange>,
pub has_icons: bool,
pub show_thin_separators: bool,
pub show_final_separator: bool,
pub display_two_rows: bool,
pub current_split_gradient: Gradient,
}
impl Default for Settings {
fn default() -> Self {
Settings {
background: ListGradient::Alternating(
Color::transparent(),
Color::from((1.0, 1.0, 1.0, 0.04)),
),
visual_split_count: 16,
split_preview_count: 1,
show_thin_separators: true,
separator_last_split: true,
always_show_last_split: true,
fill_with_blank_space: true,
display_two_rows: false,
current_split_gradient: Gradient::Vertical(
Color::from((51.0 / 255.0, 115.0 / 255.0, 244.0 / 255.0, 1.0)),
Color::from((21.0 / 255.0, 53.0 / 255.0, 116.0 / 255.0, 1.0)),
),
show_column_labels: false,
columns: vec![
ColumnSettings {
name: String::from("Time"),
start_with: ColumnStartWith::ComparisonTime,
update_with: ColumnUpdateWith::SplitTime,
update_trigger: ColumnUpdateTrigger::OnEndingSegment,
comparison_override: None,
timing_method: None,
},
ColumnSettings {
name: String::from("+/−"),
start_with: ColumnStartWith::Empty,
update_with: ColumnUpdateWith::Delta,
update_trigger: ColumnUpdateTrigger::Contextual,
comparison_override: None,
timing_method: None,
},
],
}
}
}
impl State {
pub fn write_json<W>(&self, writer: W) -> Result<()>
where
W: Write,
{
to_writer(writer, self)
}
}
impl Component {
pub fn new() -> Self {
Default::default()
}
pub fn with_settings(settings: Settings) -> Self {
Self {
settings,
..Default::default()
}
}
pub fn settings(&self) -> &Settings {
&self.settings
}
pub fn settings_mut(&mut self) -> &mut Settings {
&mut self.settings
}
pub fn scroll_up(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_sub(1);
}
pub fn scroll_down(&mut self) {
self.scroll_offset = self.scroll_offset.saturating_add(1);
}
pub fn remount(&mut self) {
self.icon_ids.clear();
}
pub fn name(&self) -> Cow<'_, str> {
"Splits".into()
}
pub fn state(&mut self, timer: &Timer, layout_settings: &GeneralLayoutSettings) -> State {
if self.current_split_index != timer.current_split_index() {
self.current_split_index = timer.current_split_index();
self.scroll_offset = 0;
}
let run = timer.run();
self.icon_ids.resize(run.len(), CachedImageId::default());
let mut visual_split_count = self.settings.visual_split_count;
if visual_split_count == 0 {
visual_split_count = run.len();
}
let current_split = timer.current_split_index();
let method = timer.current_timing_method();
let always_show_last_split = if self.settings.always_show_last_split {
0
} else {
1
};
let skip_count = min(
current_split.map_or(0, |c_s| {
c_s.saturating_sub(
visual_split_count
.saturating_sub(2)
.saturating_sub(self.settings.split_preview_count)
.saturating_add(always_show_last_split),
) as isize
}),
run.len() as isize - visual_split_count as isize,
);
self.scroll_offset = min(
max(self.scroll_offset, -skip_count),
run.len() as isize - skip_count - visual_split_count as isize,
);
let skip_count = max(0, skip_count + self.scroll_offset) as usize;
let take_count = visual_split_count + always_show_last_split as usize - 1;
let always_show_last_split = self.settings.always_show_last_split;
let show_final_separator = self.settings.separator_last_split
&& always_show_last_split
&& skip_count + take_count + 1 < run.len();
let Settings {
show_thin_separators,
fill_with_blank_space,
display_two_rows,
ref columns,
..
} = self.settings;
let mut icon_changes = Vec::new();
let mut splits: Vec<_> = run
.segments()
.iter()
.enumerate()
.zip(self.icon_ids.iter_mut())
.skip(skip_count)
.filter(|&((i, _), _)| {
i - skip_count < take_count || (always_show_last_split && i + 1 == run.len())
})
.map(|((i, segment), icon_id)| {
let columns = columns
.iter()
.map(|column| {
column::state(
column,
timer,
layout_settings,
segment,
i,
current_split,
method,
)
})
.collect();
if let Some(icon_change) = icon_id.update_with(Some(segment.icon())) {
icon_changes.push(IconChange {
segment_index: i,
icon: icon_change.to_owned(),
});
}
SplitState {
name: segment.name().to_string(),
columns,
is_current_split: Some(i) == current_split,
index: i,
}
})
.collect();
if fill_with_blank_space && splits.len() < visual_split_count {
let blank_split_count = visual_split_count - splits.len();
for i in 0..blank_split_count {
splits.push(SplitState {
name: String::new(),
columns: Vec::new(),
is_current_split: false,
index: (usize::max_value() ^ 1) - 2 * i,
});
}
}
let column_labels = if self.settings.show_column_labels {
Some(
self.settings
.columns
.iter()
.map(|c| c.name.clone())
.collect(),
)
} else {
None
};
State {
background: self.settings.background,
column_labels,
splits,
icon_changes,
has_icons: run.segments().iter().any(|s| !s.icon().is_empty()),
show_thin_separators,
show_final_separator,
display_two_rows,
current_split_gradient: self.settings.current_split_gradient,
}
}
pub fn settings_description(&self) -> SettingsDescription {
let mut settings = SettingsDescription::with_fields(vec![
Field::new("Background".into(), self.settings.background.into()),
Field::new(
"Total Splits".into(),
Value::UInt(self.settings.visual_split_count as _),
),
Field::new(
"Upcoming Splits".into(),
Value::UInt(self.settings.split_preview_count as _),
),
Field::new(
"Show Thin Separators".into(),
self.settings.show_thin_separators.into(),
),
Field::new(
"Show Separator Before Last Split".into(),
self.settings.separator_last_split.into(),
),
Field::new(
"Always Show Last Split".into(),
self.settings.always_show_last_split.into(),
),
Field::new(
"Fill with Blank Space if Not Enough Splits".into(),
self.settings.fill_with_blank_space.into(),
),
Field::new(
"Display 2 Rows".into(),
self.settings.display_two_rows.into(),
),
Field::new(
"Current Split Gradient".into(),
self.settings.current_split_gradient.into(),
),
Field::new(
"Show Column Labels".into(),
self.settings.show_column_labels.into(),
),
Field::new(
"Columns".into(),
Value::UInt(self.settings.columns.len() as _),
),
]);
settings
.fields
.reserve_exact(SETTINGS_PER_COLUMN * self.settings.columns.len());
for column in &self.settings.columns {
settings
.fields
.push(Field::new("Column Name".into(), column.name.clone().into()));
settings
.fields
.push(Field::new("Start With".into(), column.start_with.into()));
settings
.fields
.push(Field::new("Update With".into(), column.update_with.into()));
settings.fields.push(Field::new(
"Update Trigger".into(),
column.update_trigger.into(),
));
settings.fields.push(Field::new(
"Comparison".into(),
column.comparison_override.clone().into(),
));
settings.fields.push(Field::new(
"Timing Method".into(),
column.timing_method.into(),
));
}
settings
}
pub fn set_value(&mut self, index: usize, value: Value) {
match index {
0 => self.settings.background = value.into(),
1 => self.settings.visual_split_count = value.into_uint().unwrap() as _,
2 => self.settings.split_preview_count = value.into_uint().unwrap() as _,
3 => self.settings.show_thin_separators = value.into(),
4 => self.settings.separator_last_split = value.into(),
5 => self.settings.always_show_last_split = value.into(),
6 => self.settings.fill_with_blank_space = value.into(),
7 => self.settings.display_two_rows = value.into(),
8 => self.settings.current_split_gradient = value.into(),
9 => self.settings.show_column_labels = value.into(),
10 => {
let new_len = value.into_uint().unwrap() as usize;
self.settings.columns.resize(new_len, Default::default());
}
index => {
let index = index - SETTINGS_BEFORE_COLUMNS;
let column_index = index / SETTINGS_PER_COLUMN;
let setting_index = index % SETTINGS_PER_COLUMN;
if let Some(column) = self.settings.columns.get_mut(column_index) {
match setting_index {
0 => column.name = value.into(),
1 => column.start_with = value.into(),
2 => column.update_with = value.into(),
3 => column.update_trigger = value.into(),
4 => column.comparison_override = value.into(),
5 => column.timing_method = value.into(),
_ => unreachable!(),
}
} else {
panic!("Unsupported Setting Index")
}
}
}
}
}