use crate::{
platform::prelude::*,
settings::{
self, CachedImageId, Color, Field, Gradient, ImageData, ListGradient, SettingsDescription,
Value,
},
timing::{formatter::Accuracy, Snapshot},
util::{Clear, ClearVec},
GeneralLayoutSettings,
};
use core::cmp::{max, min};
use serde::{Deserialize, Serialize};
#[cfg(test)]
mod tests;
mod column;
pub use column::{
ColumnKind, ColumnSettings, ColumnStartWith, ColumnState, ColumnUpdateTrigger,
ColumnUpdateWith, TimeColumn, VariableColumn,
};
const SETTINGS_BEFORE_COLUMNS: usize = 15;
const SETTINGS_PER_TIME_COLUMN: usize = 6;
const SETTINGS_PER_VARIABLE_COLUMN: usize = 2;
#[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 split_time_accuracy: Accuracy,
pub segment_time_accuracy: Accuracy,
pub delta_time_accuracy: Accuracy,
pub delta_drop_decimals: bool,
pub show_column_labels: bool,
pub columns: Vec<ColumnSettings>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SplitState {
pub name: String,
pub columns: ClearVec<ColumnState>,
pub is_current_split: bool,
pub index: usize,
}
impl Clear for SplitState {
fn clear(&mut self) {
self.name.clear();
self.columns.clear();
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct IconChange {
pub segment_index: usize,
pub icon: ImageData,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct State {
pub background: ListGradient,
pub column_labels: Option<ClearVec<String>>,
pub splits: ClearVec<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::rgba(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::rgba(51.0 / 255.0, 115.0 / 255.0, 244.0 / 255.0, 1.0),
Color::rgba(21.0 / 255.0, 53.0 / 255.0, 116.0 / 255.0, 1.0),
),
split_time_accuracy: Accuracy::Seconds,
segment_time_accuracy: Accuracy::Hundredths,
delta_time_accuracy: Accuracy::Tenths,
delta_drop_decimals: true,
show_column_labels: false,
columns: vec![
ColumnSettings {
name: String::from("Time"),
kind: ColumnKind::Time(TimeColumn {
start_with: ColumnStartWith::ComparisonTime,
update_with: ColumnUpdateWith::SplitTime,
update_trigger: ColumnUpdateTrigger::OnEndingSegment,
comparison_override: None,
timing_method: None,
}),
},
ColumnSettings {
name: String::from("+/−"),
kind: ColumnKind::Time(TimeColumn {
start_with: ColumnStartWith::Empty,
update_with: ColumnUpdateWith::Delta,
update_trigger: ColumnUpdateTrigger::Contextual,
comparison_override: None,
timing_method: None,
}),
},
],
}
}
}
#[cfg(feature = "std")]
impl State {
pub fn write_json<W>(&self, writer: W) -> serde_json::Result<()>
where
W: std::io::Write,
{
serde_json::to_writer(writer, self)
}
}
impl Component {
pub fn new() -> Self {
Default::default()
}
pub fn with_settings(settings: Settings) -> Self {
Self {
settings,
..Default::default()
}
}
pub const 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 const fn name(&self) -> &'static str {
"Splits"
}
pub fn update_state(
&mut self,
state: &mut State,
timer: &Snapshot<'_>,
layout_settings: &GeneralLayoutSettings,
) {
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 locked_last_split = isize::from(self.settings.always_show_last_split);
let skip_count = min(
current_split.map_or(0, |current_split| {
max(
0,
current_split as isize
+ self.settings.split_preview_count as isize
+ locked_last_split
+ 1
- visual_split_count 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 - locked_last_split as usize;
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;
state.background = self.settings.background;
if self.settings.show_column_labels {
let column_labels = state.column_labels.get_or_insert_with(Default::default);
column_labels.clear();
for c in &self.settings.columns {
column_labels.push().push_str(&c.name);
}
} else {
state.column_labels = None;
}
let icon_changes = &mut state.icon_changes;
icon_changes.clear();
state.splits.clear();
for ((i, segment), icon_id) in 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())
})
{
let state = state.splits.push_with(|| SplitState {
name: String::new(),
columns: ClearVec::new(),
is_current_split: false,
index: 0,
});
if let Some(icon_change) = icon_id.update_with(Some(segment.icon())) {
icon_changes.push(IconChange {
segment_index: i,
icon: icon_change.into(),
});
}
state.name.push_str(segment.name());
for column in columns {
column::update_state(
state.columns.push_with(|| ColumnState {
value: String::new(),
semantic_color: Default::default(),
visual_color: Color::transparent(),
updates_frequently: false,
}),
column,
timer,
&self.settings,
layout_settings,
segment,
i,
current_split,
method,
);
}
state.is_current_split = Some(i) == current_split;
state.index = i;
}
if fill_with_blank_space && state.splits.len() < visual_split_count {
let blank_split_count = visual_split_count - state.splits.len();
for i in 0..blank_split_count {
let state = state.splits.push_with(|| SplitState {
name: String::new(),
columns: ClearVec::new(),
is_current_split: false,
index: 0,
});
state.is_current_split = false;
state.index = (usize::max_value() ^ 1) - 2 * i;
}
}
state.has_icons = run.segments().iter().any(|s| !s.icon().is_empty());
state.show_thin_separators = show_thin_separators;
state.show_final_separator = show_final_separator;
state.display_two_rows = display_two_rows;
state.current_split_gradient = self.settings.current_split_gradient;
}
pub fn state(
&mut self,
timer: &Snapshot<'_>,
layout_settings: &GeneralLayoutSettings,
) -> State {
let mut state = Default::default();
self.update_state(&mut state, timer, layout_settings);
state
}
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(
"Split Time Accuracy".into(),
self.settings.split_time_accuracy.into(),
),
Field::new(
"Segment Time Accuracy".into(),
self.settings.segment_time_accuracy.into(),
),
Field::new(
"Delta Time Accuracy".into(),
self.settings.delta_time_accuracy.into(),
),
Field::new(
"Drop Delta Decimals When Showing Minutes".into(),
self.settings.delta_drop_decimals.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(
self.settings
.columns
.iter()
.map(|column| match column.kind {
ColumnKind::Variable(_) => SETTINGS_PER_VARIABLE_COLUMN,
ColumnKind::Time(_) => SETTINGS_PER_TIME_COLUMN,
})
.sum(),
);
for column in &self.settings.columns {
settings
.fields
.push(Field::new("Column Name".into(), column.name.clone().into()));
match &column.kind {
ColumnKind::Variable(column) => {
settings.fields.push(Field::new(
"Column Type".into(),
settings::ColumnKind::Variable.into(),
));
settings.fields.push(Field::new(
"Variable Name".into(),
column.variable_name.clone().into(),
));
}
ColumnKind::Time(column) => {
settings.fields.push(Field::new(
"Column Type".into(),
settings::ColumnKind::Time.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.split_time_accuracy = value.into(),
10 => self.settings.segment_time_accuracy = value.into(),
11 => self.settings.delta_time_accuracy = value.into(),
12 => self.settings.delta_drop_decimals = value.into(),
13 => self.settings.show_column_labels = value.into(),
14 => {
let new_len = value.into_uint().unwrap() as usize;
self.settings.columns.resize(new_len, Default::default());
}
index => {
let mut index = index - SETTINGS_BEFORE_COLUMNS;
for column in &mut self.settings.columns {
if index < 2 {
match index {
0 => column.name = value.into(),
_ => {
column.kind = match settings::ColumnKind::from(value) {
settings::ColumnKind::Time => {
ColumnKind::Time(Default::default())
}
settings::ColumnKind::Variable => {
ColumnKind::Variable(Default::default())
}
}
}
}
return;
}
index -= 2;
match &mut column.kind {
ColumnKind::Variable(column) => {
if index < 1 {
column.variable_name = value.into();
return;
}
index -= 1;
}
ColumnKind::Time(column) => {
if index < 5 {
match index {
0 => column.start_with = value.into(),
1 => column.update_with = value.into(),
2 => column.update_trigger = value.into(),
3 => column.comparison_override = value.into(),
_ => column.timing_method = value.into(),
}
return;
}
index -= 5;
}
}
}
panic!("Unsupported Setting Index")
}
}
}
}