diff --git a/capi/src/run_editor.rs b/capi/src/run_editor.rs index d8e8ce7c..8e32a751 100644 --- a/capi/src/run_editor.rs +++ b/capi/src/run_editor.rs @@ -338,6 +338,27 @@ pub extern "C" fn RunEditor_select_segment_group(this: &mut RunEditor, group_ind this.select_segment_group(group_index).is_ok() } +/// Toggles the native segment group with the provided index as part of the +/// current selection without clearing other selected rows. Returns if +/// the group doesn't exist. +#[unsafe(no_mangle)] +pub extern "C" fn RunEditor_toggle_segment_group_selection( + this: &mut RunEditor, + group_index: usize, +) -> bool { + this.toggle_segment_group_selection(group_index).is_ok() +} + +/// Extends the current selection through the entire native segment group with +/// the provided index. Returns if the group doesn't exist. +#[unsafe(no_mangle)] +pub extern "C" fn RunEditor_select_segment_group_range( + this: &mut RunEditor, + group_index: usize, +) -> bool { + this.select_segment_group_range(group_index).is_ok() +} + /// Removes the selected native segment groups, while keeping all segments. #[unsafe(no_mangle)] pub extern "C" fn RunEditor_remove_selected_segment_groups(this: &mut RunEditor) -> bool { diff --git a/capi/src/splits_component.rs b/capi/src/splits_component.rs index 20b0bb9c..c9bb1079 100644 --- a/capi/src/splits_component.rs +++ b/capi/src/splits_component.rs @@ -110,14 +110,12 @@ pub extern "C" fn SplitsComponent_set_always_show_last_split( this.settings_mut().always_show_last_split = always_show_last_split; } -/// If the last segment is to always be shown, this determines whether to -/// show a more pronounced separator in front of the last segment, if it is -/// not directly adjacent to the segment shown right before it in the -/// scrolling window. +/// Whether to show a pronounced separator before a row when one or more rows +/// immediately before it are omitted from the scrolling window. #[unsafe(no_mangle)] -pub extern "C" fn SplitsComponent_set_separator_last_split( +pub extern "C" fn SplitsComponent_set_show_gap_separators( this: &mut SplitsComponent, - separator_last_split: bool, + show_gap_separators: bool, ) { - this.settings_mut().separator_last_split = separator_last_split; + this.settings_mut().show_gap_separators = show_gap_separators; } diff --git a/src/component/splits/mod.rs b/src/component/splits/mod.rs index 0bf2d40d..ecf745b4 100644 --- a/src/component/splits/mod.rs +++ b/src/component/splits/mod.rs @@ -75,11 +75,10 @@ pub struct Settings { /// Specifies whether thin separators should be shown between the individual /// segments shown by the component. pub show_thin_separators: bool, - /// If the last segment is to always be shown, this determines whether to - /// show a more pronounced separator in front of the last segment, if it is - /// not directly adjacent to the segment shown right before it in the - /// scrolling window. - pub separator_last_split: bool, + /// Whether to show a pronounced separator before a row when one or more + /// rows immediately before it are omitted from the scrolling window. + #[serde(alias = "separator_last_split")] + pub show_gap_separators: bool, /// If not every segment is shown in the scrolling window of segments, then /// this determines whether the final segment is always to be shown, as it /// contains valuable information about the total duration of the chosen @@ -250,7 +249,7 @@ impl Settings { visual_split_count: 16, split_preview_count: 1, show_thin_separators: true, - separator_last_split: true, + show_gap_separators: true, always_show_last_split: true, fill_with_blank_space: true, display_two_rows: false, @@ -508,15 +507,12 @@ impl Component { // A gap between projected row indices means that the scrolling // window omitted one or more logical rows. Communicate that gap on // the following row so every renderer can show the same pronounced - // separator without reconstructing the scrolling logic. The - // existing setting continues to control gaps before the final row; - // gaps within the list always identify hidden group content. + // separator without reconstructing the scrolling logic. Apply the + // setting here, where all omissions are known, so it consistently + // controls gaps inside groups and before the locked final row. let has_gap = previous_displayed_index .is_some_and(|previous_index| displayed_index > previous_index + 1); - let is_final_row = displayed_index + 1 == displayed_len; - state.show_separator_before = has_gap - && (!is_final_row - || (always_show_last_split && self.settings.separator_last_split)); + state.show_separator_before = self.settings.show_gap_separators && has_gap; previous_displayed_index = Some(displayed_index); state.icon = *image_cache @@ -647,13 +643,11 @@ impl Component { self.settings.show_thin_separators.into(), ), Field::new( - Text::SplitsShowSeparatorBeforeLastSplit + Text::SplitsShowGapSeparators.resolve(lang).into(), + Text::SplitsShowGapSeparatorsDescription .resolve(lang) .into(), - Text::SplitsShowSeparatorBeforeLastSplitDescription - .resolve(lang) - .into(), - self.settings.separator_last_split.into(), + self.settings.show_gap_separators.into(), ), Field::new( Text::SplitsAlwaysShowLastSplit.resolve(lang).into(), @@ -820,7 +814,7 @@ impl Component { 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(), + 4 => self.settings.show_gap_separators = 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(), diff --git a/src/component/splits/tests/mod.rs b/src/component/splits/tests/mod.rs index 5fe6bf3b..b7ddbf3c 100644 --- a/src/component/splits/tests/mod.rs +++ b/src/component/splits/tests/mod.rs @@ -415,6 +415,31 @@ fn current_group_header_stays_visible_when_group_rows_scroll() { .collect::>(), [false, true, false, true] ); + + component.settings_mut().show_gap_separators = false; + let state = component.state( + &mut image_cache, + &timer.snapshot(), + &Default::default(), + Lang::English, + ); + assert!( + state + .splits + .iter() + .all(|split| !split.show_separator_before) + ); +} + +#[test] +fn old_last_split_separator_setting_deserializes_as_gap_separators() { + let settings: Settings = + serde_json::from_value(serde_json::json!({ "separator_last_split": false })).unwrap(); + assert!(!settings.show_gap_separators); + + let serialized = serde_json::to_value(settings).unwrap(); + assert_eq!(serialized["show_gap_separators"], false); + assert!(serialized.get("separator_last_split").is_none()); } #[test] diff --git a/src/layout/parser/splits.rs b/src/layout/parser/splits.rs index 8f5b2945..9e5a1608 100644 --- a/src/layout/parser/splits.rs +++ b/src/layout/parser/splits.rs @@ -42,7 +42,10 @@ pub fn settings(reader: &mut Reader, component: &mut Component) -> Result<()> { } "ShowBlankSplits" => parse_bool(reader, |b| settings.fill_with_blank_space = b), "SeparatorLastSplit" => { - parse_bool(reader, |b| settings.separator_last_split = b) + // LiveSplit's XML format still uses the historical + // name. Map it to the generalized setting while keeping + // that external format backwards compatible. + parse_bool(reader, |b| settings.show_gap_separators = b) } "Display2Rows" => parse_bool(reader, |b| settings.display_two_rows = b), "ShowColumnLabels" => parse_bool(reader, |b| settings.show_column_labels = b), diff --git a/src/localization/brazilian_portuguese.rs b/src/localization/brazilian_portuguese.rs index 14224fd4..e316bec7 100644 --- a/src/localization/brazilian_portuguese.rs +++ b/src/localization/brazilian_portuguese.rs @@ -416,9 +416,9 @@ pub const fn resolve(text: Text) -> &'static str { Text::SplitsShowThinSeparatorsDescription => { "Indica se separadores finos devem ser mostrados entre as linhas de segmentos." } - Text::SplitsShowSeparatorBeforeLastSplit => "Mostrar separador antes do último split", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "Se o último segmento for sempre mostrado, isto determina se mostrar um separador mais pronunciado antes do último segmento, caso não seja adjacente ao segmento anterior na janela." + Text::SplitsShowGapSeparators => "Mostrar separadores de lacunas", + Text::SplitsShowGapSeparatorsDescription => { + "Determina se um separador mais destacado deve ser exibido antes de uma linha quando uma ou mais linhas imediatamente anteriores são omitidas da janela de rolagem." } Text::SplitsAlwaysShowLastSplit => "Mostrar sempre o último split", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/chinese_simplified.rs b/src/localization/chinese_simplified.rs index 1ace4e04..accf177e 100644 --- a/src/localization/chinese_simplified.rs +++ b/src/localization/chinese_simplified.rs @@ -302,9 +302,9 @@ pub const fn resolve(text: Text) -> &'static str { } Text::SplitsShowThinSeparators => "显示细分隔线", Text::SplitsShowThinSeparatorsDescription => "指定是否在分段行之间显示细分隔线。", - Text::SplitsShowSeparatorBeforeLastSplit => "在最后一段前显示分隔线", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "若最后一段始终显示,则当其不与上一段相邻时,是否在其前显示更明显的分隔线。" + Text::SplitsShowGapSeparators => "显示省略行分隔线", + Text::SplitsShowGapSeparatorsDescription => { + "指定当滚动窗口中紧邻的一行或多行被省略时,是否在下一行前显示更明显的分隔线。" } Text::SplitsAlwaysShowLastSplit => "始终显示最后一段", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/dutch.rs b/src/localization/dutch.rs index a6e1e141..b3837071 100644 --- a/src/localization/dutch.rs +++ b/src/localization/dutch.rs @@ -416,9 +416,9 @@ pub const fn resolve(text: Text) -> &'static str { Text::SplitsShowThinSeparatorsDescription => { "Geeft aan of dunne scheiders tussen segmentrijen moeten worden getoond." } - Text::SplitsShowSeparatorBeforeLastSplit => "Scheider voor laatste split tonen", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "Als het laatste segment altijd wordt getoond, bepaalt dit of een duidelijke scheider vóór het laatste segment wordt getoond wanneer het niet direct aansluit op het vorige segment in het venster." + Text::SplitsShowGapSeparators => "Scheiders bij overgeslagen rijen tonen", + Text::SplitsShowGapSeparatorsDescription => { + "Bepaalt of een duidelijkere scheider vóór een rij wordt getoond wanneer één of meer direct voorafgaande rijen uit het scrollvenster zijn weggelaten." } Text::SplitsAlwaysShowLastSplit => "Laatste split altijd tonen", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/english.rs b/src/localization/english.rs index 33ccf58a..cdcf9862 100644 --- a/src/localization/english.rs +++ b/src/localization/english.rs @@ -414,9 +414,9 @@ pub const fn resolve(text: Text) -> &'static str { Text::SplitsShowThinSeparatorsDescription => { "Specifies whether thin separators should be shown between the individual segment rows." } - Text::SplitsShowSeparatorBeforeLastSplit => "Show Separator Before Last Split", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "If the last segment is to always be shown, this determines whether to show a more pronounced separator in front of the last segment, if it is not directly adjacent to the segment shown right before it in the scrolling window." + Text::SplitsShowGapSeparators => "Show Gap Separators", + Text::SplitsShowGapSeparatorsDescription => { + "Whether to show a pronounced separator before a row when one or more rows immediately before it are omitted from the scrolling window." } Text::SplitsAlwaysShowLastSplit => "Always Show Last Split", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/french.rs b/src/localization/french.rs index 330d5a10..b2d8d2f6 100644 --- a/src/localization/french.rs +++ b/src/localization/french.rs @@ -424,9 +424,9 @@ pub const fn resolve(text: Text) -> &'static str { Text::SplitsShowThinSeparatorsDescription => { "Indique si des séparateurs fins doivent être affichés entre les lignes de segments." } - Text::SplitsShowSeparatorBeforeLastSplit => "Afficher un séparateur avant le dernier split", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "Si le dernier segment est toujours affiché, indique si un séparateur plus prononcé doit être montré avant lui lorsqu’il n’est pas adjacent au segment précédent." + Text::SplitsShowGapSeparators => "Afficher les séparateurs d’écart", + Text::SplitsShowGapSeparatorsDescription => { + "Indique si un séparateur plus prononcé doit être affiché avant une ligne lorsqu’une ou plusieurs lignes juste avant sont omises de la fenêtre de défilement." } Text::SplitsAlwaysShowLastSplit => "Toujours afficher le dernier split", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/german.rs b/src/localization/german.rs index a2b4becc..88a18b0d 100644 --- a/src/localization/german.rs +++ b/src/localization/german.rs @@ -440,9 +440,9 @@ pub const fn resolve(text: Text) -> &'static str { Text::SplitsShowThinSeparatorsDescription => { "Legt fest, ob dünne Trennlinien zwischen den einzelnen Segmentzeilen angezeigt werden." } - Text::SplitsShowSeparatorBeforeLastSplit => "Trennlinie vor letztem Split anzeigen", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "Wenn das letzte Segment immer angezeigt werden soll, bestimmt dies, ob vor dem letzten Segment eine deutlichere Trennlinie angezeigt wird, wenn es im Scrollfenster nicht direkt neben dem vorherigen Segment liegt." + Text::SplitsShowGapSeparators => "Trennlinien bei Lücken anzeigen", + Text::SplitsShowGapSeparatorsDescription => { + "Legt fest, ob vor einer Zeile eine deutlichere Trennlinie angezeigt wird, wenn im Scrollfenster unmittelbar davor eine oder mehrere Zeilen ausgelassen werden." } Text::SplitsAlwaysShowLastSplit => "Letzten Split immer anzeigen", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/italian.rs b/src/localization/italian.rs index d68dbac3..844fe441 100644 --- a/src/localization/italian.rs +++ b/src/localization/italian.rs @@ -416,9 +416,9 @@ pub const fn resolve(text: Text) -> &'static str { Text::SplitsShowThinSeparatorsDescription => { "Indica se mostrare separatori sottili tra le righe dei segmenti." } - Text::SplitsShowSeparatorBeforeLastSplit => "Mostra separatore prima dell’ultimo split", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "Se l’ultimo segmento deve essere sempre mostrato, questa opzione determina se mostrare un separatore più marcato prima dell’ultimo segmento quando non è adiacente al precedente nella finestra scorrevole." + Text::SplitsShowGapSeparators => "Mostra separatori per le righe omesse", + Text::SplitsShowGapSeparatorsDescription => { + "Determina se mostrare un separatore più marcato prima di una riga quando una o più righe immediatamente precedenti sono omesse dalla finestra scorrevole." } Text::SplitsAlwaysShowLastSplit => "Mostra sempre l’ultimo split", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/japanese.rs b/src/localization/japanese.rs index 1e93e83e..f8c204b1 100644 --- a/src/localization/japanese.rs +++ b/src/localization/japanese.rs @@ -380,9 +380,9 @@ pub const fn resolve(text: Text) -> &'static str { } Text::SplitsShowThinSeparators => "細い区切り線を表示", Text::SplitsShowThinSeparatorsDescription => "区間の間に細い区切り線を表示するかどうか。", - Text::SplitsShowSeparatorBeforeLastSplit => "最後のスプリット前の区切り線を表示", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "最後の区間を常に表示する場合、スクロール窓で直前の区間と隣接しないときに強調区切り線を表示するかどうかを指定します。" + Text::SplitsShowGapSeparators => "省略箇所の区切り線を表示", + Text::SplitsShowGapSeparatorsDescription => { + "スクロール窓で直前の1行以上が省略されている場合、その次の行の前に強調区切り線を表示するかどうかを指定します。" } Text::SplitsAlwaysShowLastSplit => "最後のスプリットを常に表示", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/korean.rs b/src/localization/korean.rs index d42bd283..bd553dbd 100644 --- a/src/localization/korean.rs +++ b/src/localization/korean.rs @@ -376,9 +376,9 @@ pub const fn resolve(text: Text) -> &'static str { Text::SplitsShowThinSeparatorsDescription => { "세그먼트 행 사이에 얇은 구분선을 표시할지 지정합니다." } - Text::SplitsShowSeparatorBeforeLastSplit => "마지막 스플릿 앞 구분선 표시", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "마지막 세그먼트를 항상 표시하는 경우, 스크롤 창에서 바로 앞 세그먼트와 인접하지 않을 때 더 두드러진 구분선을 표시할지 지정합니다." + Text::SplitsShowGapSeparators => "생략 구간 구분선 표시", + Text::SplitsShowGapSeparatorsDescription => { + "스크롤 창에서 바로 앞의 한 개 이상의 행이 생략된 경우 다음 행 앞에 더 두드러진 구분선을 표시할지 지정합니다." } Text::SplitsAlwaysShowLastSplit => "마지막 스플릿 항상 표시", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/mod.rs b/src/localization/mod.rs index 1d35fa94..956cb91b 100644 --- a/src/localization/mod.rs +++ b/src/localization/mod.rs @@ -827,8 +827,8 @@ pub(crate) enum Text { SplitsUpcomingSegmentsDescription, SplitsShowThinSeparators, SplitsShowThinSeparatorsDescription, - SplitsShowSeparatorBeforeLastSplit, - SplitsShowSeparatorBeforeLastSplitDescription, + SplitsShowGapSeparators, + SplitsShowGapSeparatorsDescription, SplitsAlwaysShowLastSplit, SplitsAlwaysShowLastSplitDescription, SplitsFillWithBlankSpace, diff --git a/src/localization/polish.rs b/src/localization/polish.rs index e9eca8e7..d571ee0e 100644 --- a/src/localization/polish.rs +++ b/src/localization/polish.rs @@ -402,9 +402,9 @@ pub const fn resolve(text: Text) -> &'static str { Text::SplitsShowThinSeparatorsDescription => { "Określa, czy wyświetlać cienkie separatory pomiędzy poszczególnymi wierszami segmentów." } - Text::SplitsShowSeparatorBeforeLastSplit => "Pokaż separator przed ostatnim splitem", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "Jeśli ostatni segment ma być zawsze widoczny, to określa, czy pokazywać bardziej wyraźny separator przed ostatnim segmentem, jeśli nie sąsiaduje bezpośrednio z poprzednim segmentem w oknie przewijania." + Text::SplitsShowGapSeparators => "Pokaż separatory luk", + Text::SplitsShowGapSeparatorsDescription => { + "Określa, czy przed wierszem pokazywać bardziej wyraźny separator, gdy w oknie przewijania pominięto co najmniej jeden bezpośrednio poprzedzający wiersz." } Text::SplitsAlwaysShowLastSplit => "Zawsze pokazuj ostatni split", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/portuguese.rs b/src/localization/portuguese.rs index d50e7242..7aecca9d 100644 --- a/src/localization/portuguese.rs +++ b/src/localization/portuguese.rs @@ -416,9 +416,9 @@ pub const fn resolve(text: Text) -> &'static str { Text::SplitsShowThinSeparatorsDescription => { "Indica se separadores finos devem ser mostrados entre as linhas de segmentos." } - Text::SplitsShowSeparatorBeforeLastSplit => "Mostrar separador antes do último split", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "Se o último segmento for sempre mostrado, isto determina se mostrar um separador mais pronunciado antes do último segmento, caso não seja adjacente ao segmento anterior na janela." + Text::SplitsShowGapSeparators => "Mostrar separadores de lacunas", + Text::SplitsShowGapSeparatorsDescription => { + "Determina se deve ser mostrado um separador mais pronunciado antes de uma linha quando uma ou mais linhas imediatamente anteriores são omitidas da janela de deslocamento." } Text::SplitsAlwaysShowLastSplit => "Mostrar sempre o último split", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/russian.rs b/src/localization/russian.rs index b1abb282..3b884962 100644 --- a/src/localization/russian.rs +++ b/src/localization/russian.rs @@ -406,11 +406,9 @@ pub const fn resolve(text: Text) -> &'static str { Text::SplitsShowThinSeparatorsDescription => { "Определяет, следует ли показывать тонкие разделители между строками сегментов." } - Text::SplitsShowSeparatorBeforeLastSplit => { - "Показывать разделитель перед последним сегментом" - } - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "Если последний сегмент всегда показывается, определяет, показывать ли более заметный разделитель перед ним, если он не рядом с предыдущим сегментом." + Text::SplitsShowGapSeparators => "Показывать разделители пропусков", + Text::SplitsShowGapSeparatorsDescription => { + "Определяет, показывать ли более заметный разделитель перед строкой, если непосредственно перед ней в прокручиваемом окне пропущена одна или несколько строк." } Text::SplitsAlwaysShowLastSplit => "Всегда показывать последний сегмент", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/localization/spanish.rs b/src/localization/spanish.rs index c6e275f3..af0a143c 100644 --- a/src/localization/spanish.rs +++ b/src/localization/spanish.rs @@ -416,9 +416,9 @@ pub const fn resolve(text: Text) -> &'static str { Text::SplitsShowThinSeparatorsDescription => { "Indica si deben mostrarse separadores finos entre las filas de segmentos." } - Text::SplitsShowSeparatorBeforeLastSplit => "Mostrar separador antes del último split", - Text::SplitsShowSeparatorBeforeLastSplitDescription => { - "Si el último segmento siempre se muestra, esto determina si se muestra un separador más marcado antes del último segmento cuando no está adyacente al anterior en la ventana." + Text::SplitsShowGapSeparators => "Mostrar separadores de huecos", + Text::SplitsShowGapSeparatorsDescription => { + "Determina si se muestra un separador más marcado antes de una fila cuando se omiten una o más filas inmediatamente anteriores en la ventana de desplazamiento." } Text::SplitsAlwaysShowLastSplit => "Mostrar siempre el último split", Text::SplitsAlwaysShowLastSplitDescription => { diff --git a/src/run/editor/mod.rs b/src/run/editor/mod.rs index 76713c66..c4adaf24 100644 --- a/src/run/editor/mod.rs +++ b/src/run/editor/mod.rs @@ -1181,6 +1181,67 @@ impl Editor { Ok(()) } + /// Toggles whether every segment in the group with the provided index is + /// part of the current selection. Other selected segments remain selected. + /// If the group is the entire selection, it stays selected because the Run + /// Editor always requires at least one selected segment. + pub fn toggle_segment_group_selection( + &mut self, + group_index: usize, + ) -> Result<(), InvalidSegmentGroupIndexError> { + let Some(group) = self.run.segment_groups().groups().get(group_index) else { + return Err(InvalidSegmentGroupIndexError); + }; + let (start, end) = (group.start(), group.end()); + let group_is_selected = (start..end).all(|index| self.selected_segments.contains(&index)); + + if group_is_selected { + // Removing the only selected visual row would break the editor's + // non-empty selection invariant. Matching `unselect`, keep the + // current selection unchanged in that case. + if self.selected_segments.len() == end - start { + return Ok(()); + } + self.selected_segments + .retain(|&index| index < start || index >= end); + } else { + // Select through the normal primitive so the final segment in the + // group becomes active, just like exclusive group selection. + for index in start..end { + self.select_additionally(index); + } + } + Ok(()) + } + + /// Extends the current selection through the entire group with the + /// provided index. The group's far edge becomes active. If the active + /// segment is already inside the group, all remaining group segments are + /// added instead. + pub fn select_segment_group_range( + &mut self, + group_index: usize, + ) -> Result<(), InvalidSegmentGroupIndexError> { + let Some(group) = self.run.segment_groups().groups().get(group_index) else { + return Err(InvalidSegmentGroupIndexError); + }; + let (start, end) = (group.start(), group.end()); + let active = self.active_segment_index(); + + if active < start { + self.select_range(end - 1); + } else if active >= end { + self.select_range(start); + } else { + // A range to either boundary alone would omit the other side when + // the anchor is inside the group, so add the complete visual row. + for index in start..end { + self.select_additionally(index); + } + } + Ok(()) + } + /// Creates a segment group from the currently selected contiguous segment /// rows and reports why creation failed. A group needs at least one /// segment. diff --git a/src/run/editor/tests/mod.rs b/src/run/editor/tests/mod.rs index 921d97b0..1a710483 100644 --- a/src/run/editor/tests/mod.rs +++ b/src/run/editor/tests/mod.rs @@ -315,6 +315,84 @@ fn selecting_segment_group_uses_its_state_identity() { assert!(segment_group(&state, 0).selected); } +#[test] +fn toggling_segment_group_selection_preserves_other_whole_groups() { + let mut run = Run::new(); + for name in ["A1", "A2", "B1", "B2", "Outro"] { + run.push_segment(Segment::new(name)); + } + + let mut editor = Editor::new(run).unwrap(); + editor.select_range(1); + editor + .create_segment_group_from_selection(Some("Group A")) + .unwrap(); + editor.select_only(2); + editor.select_range(3); + editor + .create_segment_group_from_selection(Some("Group B")) + .unwrap(); + + editor.select_segment_group(0).unwrap(); + editor.toggle_segment_group_selection(1).unwrap(); + + let mut image_cache = ImageCache::new(); + let state = editor.state(&mut image_cache, Lang::English); + assert!(segment_group(&state, 0).selected); + assert!(segment_group(&state, 1).selected); + assert_eq!(segment(&state, 3).selected, SelectionState::Active); + + editor.toggle_segment_group_selection(0).unwrap(); + let state = editor.state(&mut image_cache, Lang::English); + assert!(!segment_group(&state, 0).selected); + assert!(segment_group(&state, 1).selected); + + // Toggling the only remaining complete group must not violate the editor's + // invariant that at least one segment is always selected. + editor.toggle_segment_group_selection(1).unwrap(); + let state = editor.state(&mut image_cache, Lang::English); + assert!(segment_group(&state, 1).selected); + assert_eq!( + editor.toggle_segment_group_selection(2), + Err(InvalidSegmentGroupIndexError) + ); +} + +#[test] +fn range_selecting_segment_group_includes_the_whole_group() { + let mut run = Run::new(); + for name in ["Intro", "A1", "A2", "Outro"] { + run.push_segment(Segment::new(name)); + } + + let mut editor = Editor::new(run).unwrap(); + editor.select_only(1); + editor.select_range(2); + editor + .create_segment_group_from_selection(Some("Group")) + .unwrap(); + + editor.select_only(0); + editor.select_segment_group_range(0).unwrap(); + + let mut image_cache = ImageCache::new(); + let state = editor.state(&mut image_cache, Lang::English); + assert_eq!(segment(&state, 0).selected, SelectionState::Selected); + assert!(segment_group(&state, 0).selected); + assert_eq!(segment(&state, 2).selected, SelectionState::Active); + + editor.select_only(3); + editor.select_segment_group_range(0).unwrap(); + let state = editor.state(&mut image_cache, Lang::English); + assert!(segment_group(&state, 0).selected); + assert_eq!(segment(&state, 1).selected, SelectionState::Active); + assert_eq!(segment(&state, 3).selected, SelectionState::Selected); + assert_eq!( + editor.select_segment_group_range(1), + Err(InvalidSegmentGroupIndexError) + ); +} + #[test] fn segment_group_icons_can_be_explicit_or_inherited() { let mut run = Run::new();