Skip to content

Commit 94c5a14

Browse files
committed
Add Basic Subsplits Support in Splits Component
All the splits without any segment groups behave like they did before and the segment groups now properly open up and close as they are being entered. This is done by transforming the segment groups iterator and implementing an iterator on top that flattens it with a specific segment in focus that determines the group that is supposed to be open. After this flattening almost all the original logic remains, but now with the current segment index and the run's length instead being based on this flattened segment group representation.
1 parent ca7a533 commit 94c5a14

File tree

5 files changed

+178
-51
lines changed

5 files changed

+178
-51
lines changed

capi/bind_gen/src/typescript.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,20 @@ export interface SplitStateJson {
244244
* on.
245245
*/
246246
is_current_split: boolean,
247+
/** Describes whether the segment is part of a segment group. */
248+
is_subsplit: boolean,
249+
/**
250+
* Describes whether the row should be considered an even or an odd row.
251+
* This is useful for visualizing the rows with alternating colors.
252+
*/
253+
is_even: boolean,
247254
/**
248255
* The index of the segment based on all the segments of the run. This may
249256
* differ from the index of this `SplitStateJson` in the
250257
* `SplitsComponentStateJson` object, as there can be a scrolling window,
251-
* showing only a subset of segments. Each index is guaranteed to be unique.
258+
* showing only a subset of segments. Indices are not guaranteed to be
259+
* unique, as they may appear in both group headers and in segments within
260+
* the groups. Only the pair of index and `is_subsplit` is unique.
252261
*/
253262
index: number,
254263
}

src/component/splits/mod.rs

Lines changed: 138 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ use crate::settings::{
1111
};
1212
use crate::timing::formatter::{Delta, Regular, TimeFormatter};
1313
use crate::{
14-
analysis, comparison, CachedImageId, GeneralLayoutSettings, Segment, TimeSpan, Timer,
15-
TimingMethod,
14+
analysis, comparison, run::SegmentGroupsIter, CachedImageId, GeneralLayoutSettings, Segment,
15+
TimeSpan, Timer, TimingMethod,
1616
};
1717
use serde_json::{to_writer, Result};
18-
use std::borrow::Cow;
19-
use std::cmp::{max, min};
20-
use std::io::Write;
18+
use std::{
19+
borrow::Cow,
20+
cmp::{max, min},
21+
io::Write,
22+
iter,
23+
};
2124

2225
#[cfg(test)]
2326
mod tests;
@@ -197,10 +200,17 @@ pub struct SplitState {
197200
/// Describes if this segment is the segment the active attempt is currently
198201
/// on.
199202
pub is_current_split: bool,
203+
/// Describes whether the segment is part of a segment group.
204+
pub is_subsplit: bool,
205+
/// Describes whether the row should be considered an even or an odd row.
206+
/// This is useful for visualizing the rows with alternating colors.
207+
pub is_even: bool,
200208
/// The index of the segment based on all the segments of the run. This may
201209
/// differ from the index of this `SplitState` in the `State` object, as
202-
/// there can be a scrolling window, showing only a subset of segments. Each
203-
/// index is guaranteed to be unique.
210+
/// there can be a scrolling window, showing only a subset of segments.
211+
/// Indices are not guaranteed to be unique, as they may appear in both
212+
/// group headers and in segments within the groups. Only the pair of index
213+
/// and `is_subsplit` is unique.
204214
pub index: usize,
205215
}
206216

@@ -379,12 +389,27 @@ impl Component {
379389
let run = timer.run();
380390
self.icon_ids.resize(run.len(), CachedImageId::default());
381391

392+
let current_split = timer.current_split_index();
393+
394+
let mut index_of_segment_in_focus = None;
395+
let mut flattened_count = 0;
396+
for (flattened_index, segment) in flatten(
397+
run.segment_groups_iter(),
398+
current_split.unwrap_or_else(|| run.len()),
399+
)
400+
.enumerate()
401+
{
402+
if segment.in_focus {
403+
index_of_segment_in_focus = Some(flattened_index);
404+
}
405+
flattened_count += 1;
406+
}
407+
382408
let mut visual_split_count = self.settings.visual_split_count;
383409
if visual_split_count == 0 {
384-
visual_split_count = run.len();
410+
visual_split_count = flattened_count;
385411
}
386412

387-
let current_split = timer.current_split_index();
388413
let method = timer.current_timing_method();
389414

390415
let always_show_last_split = if self.settings.always_show_last_split {
@@ -393,27 +418,27 @@ impl Component {
393418
1
394419
};
395420
let skip_count = min(
396-
current_split.map_or(0, |c_s| {
421+
index_of_segment_in_focus.map_or(0, |c_s| {
397422
c_s.saturating_sub(
398423
visual_split_count
399424
.saturating_sub(2)
400425
.saturating_sub(self.settings.split_preview_count)
401426
.saturating_add(always_show_last_split),
402427
) as isize
403428
}),
404-
run.len() as isize - visual_split_count as isize,
429+
flattened_count as isize - visual_split_count as isize,
405430
);
406431
self.scroll_offset = min(
407432
max(self.scroll_offset, -skip_count),
408-
run.len() as isize - skip_count - visual_split_count as isize,
433+
flattened_count as isize - skip_count - visual_split_count as isize,
409434
);
410435
let skip_count = max(0, skip_count + self.scroll_offset) as usize;
411436
let take_count = visual_split_count + always_show_last_split as usize - 1;
412437
let always_show_last_split = self.settings.always_show_last_split;
413438

414439
let show_final_separator = self.settings.separator_last_split
415440
&& always_show_last_split
416-
&& skip_count + take_count + 1 < run.len();
441+
&& skip_count + take_count + 1 < flattened_count;
417442

418443
let Settings {
419444
show_thin_separators,
@@ -425,46 +450,56 @@ impl Component {
425450

426451
let mut icon_changes = Vec::new();
427452

428-
let mut splits: Vec<_> = run
429-
.segments()
430-
.iter()
431-
.enumerate()
432-
.zip(self.icon_ids.iter_mut())
433-
.skip(skip_count)
434-
.filter(|&((i, _), _)| {
435-
i - skip_count < take_count || (always_show_last_split && i + 1 == run.len())
436-
})
437-
.map(|((i, segment), icon_id)| {
438-
let columns = columns
453+
let mut splits = Vec::with_capacity(visual_split_count);
454+
455+
for (flattened_index, segment) in flatten(
456+
run.segment_groups_iter(),
457+
current_split.unwrap_or_else(|| run.len()),
458+
)
459+
.enumerate()
460+
.skip(skip_count)
461+
.filter(|&(i, _)| {
462+
i - skip_count < take_count || (always_show_last_split && i + 1 == flattened_count)
463+
}) {
464+
let columns = if segment.kind
465+
!= FlattenedSegmentGroupItemKind::GroupHeader(SegmentGroupVisibility::Shown)
466+
{
467+
columns
439468
.iter()
440469
.map(|column| {
441470
column_state(
442471
column,
443472
timer,
444473
layout_settings,
445-
segment,
446-
i,
474+
segment.segment,
475+
segment.index,
447476
current_split,
448477
method,
449478
)
450479
})
451-
.collect();
480+
.collect()
481+
} else {
482+
Vec::new()
483+
};
452484

453-
if let Some(icon_change) = icon_id.update_with(Some(segment.icon())) {
454-
icon_changes.push(IconChange {
455-
segment_index: i,
456-
icon: icon_change.to_owned(),
457-
});
458-
}
485+
if let Some(icon_change) =
486+
self.icon_ids[segment.index].update_with(Some(segment.segment.icon()))
487+
{
488+
icon_changes.push(IconChange {
489+
segment_index: segment.index,
490+
icon: icon_change.to_owned(),
491+
});
492+
}
459493

460-
SplitState {
461-
name: segment.name().to_string(),
462-
columns,
463-
is_current_split: Some(i) == current_split,
464-
index: i,
465-
}
466-
})
467-
.collect();
494+
splits.push(SplitState {
495+
name: segment.name.to_string(),
496+
columns,
497+
is_current_split: segment.in_focus,
498+
is_subsplit: segment.kind == FlattenedSegmentGroupItemKind::Subsplit,
499+
is_even: flattened_index % 2 == 0,
500+
index: segment.index,
501+
});
502+
}
468503

469504
if fill_with_blank_space && splits.len() < visual_split_count {
470505
let blank_split_count = visual_split_count - splits.len();
@@ -473,6 +508,8 @@ impl Component {
473508
name: String::new(),
474509
columns: Vec::new(),
475510
is_current_split: false,
511+
is_subsplit: false,
512+
is_even: true,
476513
index: (usize::max_value() ^ 1) - 2 * i,
477514
});
478515
}
@@ -808,3 +845,63 @@ impl ColumnUpdateWith {
808845
}
809846
}
810847
}
848+
849+
#[derive(Copy, Clone, PartialEq)]
850+
enum SegmentGroupVisibility {
851+
Collapsed,
852+
Shown,
853+
}
854+
855+
#[derive(Copy, Clone, PartialEq)]
856+
enum FlattenedSegmentGroupItemKind {
857+
GroupHeader(SegmentGroupVisibility),
858+
Subsplit,
859+
}
860+
861+
struct FlattenedSegmentGroupItem<'groups_or_segments, 'segments> {
862+
segment: &'segments Segment,
863+
name: &'groups_or_segments str,
864+
index: usize,
865+
kind: FlattenedSegmentGroupItemKind,
866+
in_focus: bool,
867+
}
868+
869+
fn flatten<'groups_or_segments, 'segments: 'groups_or_segments>(
870+
iter: SegmentGroupsIter<'groups_or_segments, 'segments>,
871+
focus_segment_index: usize,
872+
) -> impl Iterator<Item = FlattenedSegmentGroupItem<'groups_or_segments, 'segments>> {
873+
iter.flat_map(move |group| {
874+
let start_index = group.start_index();
875+
let (children, visibility) =
876+
if group.contains(focus_segment_index) && group.len() > 1 {
877+
(
878+
Some(group.segments().iter().enumerate().map(
879+
move |(local_index, subsplit)| {
880+
let index = start_index + local_index;
881+
FlattenedSegmentGroupItem {
882+
segment: subsplit,
883+
name: subsplit.name(),
884+
index,
885+
kind: FlattenedSegmentGroupItemKind::Subsplit,
886+
in_focus: index == focus_segment_index,
887+
}
888+
},
889+
)),
890+
SegmentGroupVisibility::Shown,
891+
)
892+
} else {
893+
(None, SegmentGroupVisibility::Collapsed)
894+
};
895+
896+
let header_index = start_index + group.len() - 1;
897+
iter::once(FlattenedSegmentGroupItem {
898+
segment: group.ending_segment(),
899+
name: group.name_or_default(),
900+
index: header_index,
901+
kind: FlattenedSegmentGroupItemKind::GroupHeader(visibility),
902+
in_focus: visibility == SegmentGroupVisibility::Collapsed
903+
&& header_index == focus_segment_index,
904+
})
905+
.chain(children.into_iter().flatten())
906+
})
907+
}

src/rendering/component/splits.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@ pub(in crate::rendering) fn render<B: Backend>(
6464
}
6565

6666
let icon_size = split_height - 0.2;
67-
let icon_right = if component.has_icons { 2.0 * MARGIN + icon_size } else { MARGIN };
6867

6968
for (i, split) in component.splits.iter().enumerate() {
69+
let icon_left = if split.is_subsplit { 2.5 * MARGIN } else { MARGIN };
70+
let icon_right = if component.has_icons { icon_left + MARGIN + icon_size } else { icon_left };
71+
7072
if component.show_thin_separators && i + 1 != component.splits.len() {
7173
context.render_rectangle(
7274
[0.0, split_height - 0.05],
@@ -82,13 +84,13 @@ pub(in crate::rendering) fn render<B: Backend>(
8284
&component.current_split_gradient,
8385
);
8486
} else if let Some((even, odd)) = &split_background {
85-
let color = if split.index % 2 == 0 { even } else { odd };
87+
let color = if split.is_even { even } else { odd };
8688
context.render_rectangle([0.0, 0.0], [width, split_height - 0.05], color);
8789
}
8890

8991
{
9092
if let Some(Some(icon)) = split_icons.get(split.index) {
91-
context.render_icon([MARGIN, 0.1 - 0.5 * 0.05], [icon_size, icon_size], icon);
93+
context.render_icon([icon_left, 0.1 - 0.5 * 0.05], [icon_size, icon_size], icon);
9294
}
9395

9496
let mut left_x = width - MARGIN;

src/rendering/software/tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ fn subsplits_layout() {
123123
check_dims(
124124
&layout.state(&timer),
125125
[300, 800],
126-
0xe94bafac,
126+
0x8eaaddc,
127127
"subsplits_layout",
128128
);
129129
}

src/run/segment_groups.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,21 @@ impl SegmentGroups {
5757
}
5858
}
5959

60+
#[derive(Debug)]
6061
pub struct SegmentGroupView<'group, 'segments> {
6162
name: Option<&'group str>,
6263
segments: &'segments [Segment],
6364
ending_segment: &'segments Segment,
65+
start_index: usize,
66+
end_index: usize,
6467
}
6568

6669
impl<'group, 'segments> SegmentGroupView<'group, 'segments> {
67-
pub fn group_name(&self) -> Option<&'group str> {
70+
pub fn name(&self) -> Option<&'group str> {
6871
self.name
6972
}
7073

71-
pub fn group_name_or_default<'a>(&self) -> &'a str
74+
pub fn name_or_default<'a>(&self) -> &'a str
7275
where
7376
'group: 'a,
7477
'segments: 'a,
@@ -84,6 +87,14 @@ impl<'group, 'segments> SegmentGroupView<'group, 'segments> {
8487
self.ending_segment
8588
}
8689

90+
pub fn start_index(&self) -> usize {
91+
self.start_index
92+
}
93+
94+
pub fn contains(&self, index: usize) -> bool {
95+
index >= self.start_index && index < self.end_index
96+
}
97+
8798
pub fn len(&self) -> usize {
8899
self.segments.len()
89100
}
@@ -99,14 +110,20 @@ impl<'groups, 'segments> Iterator for SegmentGroupsIter<'groups, 'segments> {
99110
type Item = SegmentGroupView<'groups, 'segments>;
100111

101112
fn next(&mut self) -> Option<Self::Item> {
102-
let index = self.index;
103-
if self.iter.peek().map_or(true, |group| group.start > index) {
113+
let start_index = self.index;
114+
if self
115+
.iter
116+
.peek()
117+
.map_or(true, |group| group.start > start_index)
118+
{
104119
self.index += 1;
105-
let ending_segment = self.segments.get(index)?;
120+
let ending_segment = self.segments.get(start_index)?;
106121
Some(SegmentGroupView {
107122
name: None,
108123
segments: slice::from_ref(ending_segment),
109124
ending_segment,
125+
start_index,
126+
end_index: self.index,
110127
})
111128
} else {
112129
let group = self.iter.next()?;
@@ -116,6 +133,8 @@ impl<'groups, 'segments> Iterator for SegmentGroupsIter<'groups, 'segments> {
116133
name: group.name.as_ref().map(String::as_str),
117134
segments,
118135
ending_segment: segments.last().unwrap(),
136+
start_index,
137+
end_index: self.index,
119138
})
120139
}
121140
}

0 commit comments

Comments
 (0)