diff --git a/src/bin/edit/draw_editor.rs b/src/bin/edit/draw_editor.rs index ef7194da1465..609624dbd3bd 100644 --- a/src/bin/edit/draw_editor.rs +++ b/src/bin/edit/draw_editor.rs @@ -150,12 +150,12 @@ fn draw_search(ctx: &mut Context, state: &mut State) { } if state.wants_search.kind == StateSearchKind::Replace - && ctx.button("replace-all", loc(LocId::SearchReplaceAll)) + && ctx.button("replace-all", loc(LocId::SearchReplaceAll), ButtonStyle::default()) { action = SearchAction::ReplaceAll; } - if ctx.button("close", loc(LocId::SearchClose)) { + if ctx.button("close", loc(LocId::SearchClose), ButtonStyle::default()) { state.wants_search.kind = StateSearchKind::Hidden; } } @@ -239,18 +239,18 @@ pub fn draw_handle_wants_close(ctx: &mut Context, state: &mut State) { ctx.table_next_row(); ctx.inherit_focus(); - if ctx.button("yes", loc(LocId::UnsavedChangesDialogYes)) { + if ctx.button("yes", loc(LocId::UnsavedChangesDialogYes), ButtonStyle::default().accelerator('S')) { action = Action::Save; } ctx.inherit_focus(); - if ctx.button("no", loc(LocId::UnsavedChangesDialogNo)) { + if ctx.button("no", loc(LocId::UnsavedChangesDialogNo), ButtonStyle::default().accelerator('N')) { action = Action::Discard; } - if ctx.button("cancel", loc(LocId::Cancel)) { + if ctx.button("cancel", loc(LocId::Cancel), ButtonStyle::default()) { action = Action::Cancel; } - // TODO: This should highlight the corresponding letter in the label. + // Handle accelerator shortcuts if ctx.consume_shortcut(vk::S) { action = Action::Save; } else if ctx.consume_shortcut(vk::N) { diff --git a/src/bin/edit/draw_filepicker.rs b/src/bin/edit/draw_filepicker.rs index d5b8ebb4ee7d..faf7ac39439d 100644 --- a/src/bin/edit/draw_filepicker.rs +++ b/src/bin/edit/draw_filepicker.rs @@ -146,10 +146,10 @@ pub fn draw_file_picker(ctx: &mut Context, state: &mut State) { ctx.table_next_row(); ctx.inherit_focus(); - save = ctx.button("yes", loc(LocId::Yes)); + save = ctx.button("yes", loc(LocId::Yes), ButtonStyle::default()); ctx.inherit_focus(); - if ctx.button("no", loc(LocId::No)) { + if ctx.button("no", loc(LocId::No), ButtonStyle::default()) { state.file_picker_overwrite_warning = None; } } diff --git a/src/bin/edit/draw_menubar.rs b/src/bin/edit/draw_menubar.rs index e6d1e2fd2ac1..6314abed6be1 100644 --- a/src/bin/edit/draw_menubar.rs +++ b/src/bin/edit/draw_menubar.rs @@ -157,7 +157,7 @@ pub fn draw_dialog_about(ctx: &mut Context, state: &mut State) { ctx.attr_padding(Rect::three(1, 2, 0)); ctx.attr_position(Position::Center); { - if ctx.button("ok", loc(LocId::Ok)) { + if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) { state.wants_about = false; } ctx.inherit_focus(); diff --git a/src/bin/edit/draw_statusbar.rs b/src/bin/edit/draw_statusbar.rs index ecbc5e4cc21b..2b457dcd91e5 100644 --- a/src/bin/edit/draw_statusbar.rs +++ b/src/bin/edit/draw_statusbar.rs @@ -24,7 +24,7 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) { ctx.table_next_row(); - if ctx.button("newline", if tb.is_crlf() { "CRLF" } else { "LF" }) { + if ctx.button("newline", if tb.is_crlf() { "CRLF" } else { "LF" }, ButtonStyle::default()) { let is_crlf = tb.is_crlf(); tb.normalize_newlines(!is_crlf); } @@ -33,7 +33,7 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) { ctx.steal_focus(); } - state.wants_encoding_picker |= ctx.button("encoding", tb.encoding()); + state.wants_encoding_picker |= ctx.button("encoding", tb.encoding(), ButtonStyle::default()); if state.wants_encoding_picker { if doc.path.is_some() { ctx.block_begin("frame"); @@ -47,11 +47,11 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) { ctx.attr_padding(Rect::two(0, 1)); ctx.attr_border(); { - if ctx.button("reopen", loc(LocId::EncodingReopen)) { + if ctx.button("reopen", loc(LocId::EncodingReopen), ButtonStyle::default()) { state.wants_encoding_change = StateEncodingChange::Reopen; } ctx.focus_on_first_present(); - if ctx.button("convert", loc(LocId::EncodingConvert)) { + if ctx.button("convert", loc(LocId::EncodingConvert), ButtonStyle::default()) { state.wants_encoding_change = StateEncodingChange::Convert; } } @@ -79,6 +79,7 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) { }), tb.tab_size(), ), + ButtonStyle::default() ); if state.wants_indentation_picker { ctx.table_begin("indentation-picker"); @@ -159,7 +160,7 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) { &arena_format!(ctx.arena(), "{}/{}", tb.logical_line_count(), tb.visual_line_count(),), ); - if tb.is_overtype() && ctx.button("overtype", "OVR") { + if tb.is_overtype() && ctx.button("overtype", "OVR", ButtonStyle::default()) { tb.set_overtype(false); ctx.needs_rerender(); } @@ -180,7 +181,7 @@ pub fn draw_statusbar(ctx: &mut Context, state: &mut State) { filename = &filename_buf; } - state.wants_document_picker |= ctx.button("filename", filename); + state.wants_document_picker |= ctx.button("filename", filename, ButtonStyle::default()); ctx.inherit_focus(); ctx.attr_overflow(Overflow::TruncateMiddle); ctx.attr_position(Position::Right); diff --git a/src/bin/edit/main.rs b/src/bin/edit/main.rs index 5ea2802d88ec..f8b0a2d9defe 100644 --- a/src/bin/edit/main.rs +++ b/src/bin/edit/main.rs @@ -452,18 +452,18 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) { ctx.inherit_focus(); if over_limit { - if ctx.button("ok", loc(LocId::Ok)) { + if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) { state.osc_clipboard_seen_generation = generation; } ctx.inherit_focus(); } else { - if ctx.button("always", loc(LocId::Always)) { + if ctx.button("always", loc(LocId::Always), ButtonStyle::default()) { state.osc_clipboard_always_send = true; state.osc_clipboard_seen_generation = generation; state.osc_clipboard_send_generation = generation; } - if ctx.button("yes", loc(LocId::Yes)) { + if ctx.button("yes", loc(LocId::Yes), ButtonStyle::default()) { state.osc_clipboard_seen_generation = generation; state.osc_clipboard_send_generation = generation; } @@ -471,7 +471,7 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) { ctx.inherit_focus(); } - if ctx.button("no", loc(LocId::No)) { + if ctx.button("no", loc(LocId::No), ButtonStyle::default()) { state.osc_clipboard_seen_generation = generation; } if ctx.clipboard().len() >= 10 * LARGE_CLIPBOARD_THRESHOLD { diff --git a/src/bin/edit/state.rs b/src/bin/edit/state.rs index 0c4b792c92bb..619675bfd914 100644 --- a/src/bin/edit/state.rs +++ b/src/bin/edit/state.rs @@ -248,7 +248,7 @@ pub fn draw_error_log(ctx: &mut Context, state: &mut State) { } ctx.block_end(); - if ctx.button("ok", loc(LocId::Ok)) { + if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) { state.error_log_count = 0; } ctx.attr_position(Position::Center); diff --git a/src/tui.rs b/src/tui.rs index ceb6bc6520c0..a28878a01fb7 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -257,6 +257,41 @@ pub enum Overflow { TruncateTail, } +/// Controls the style with which a button label renders +#[derive(Clone, Copy)] +pub struct ButtonStyle { + accelerator: Option, + checked: Option, + bracketed: bool, +} + +impl ButtonStyle { + /// Draw an accelerator label: `[_E_xample button]` or `[Example button(X)]` + /// + /// Must provide an upper-case ASCII character. + pub fn accelerator(self, char: char) -> Self { + Self { accelerator: Some(char), ..self } + } + /// Draw a checkbox prefix: `[▣ Example Button]` + pub fn checked(self, checked: bool) -> Self { + Self { checked: Some(checked), ..self } + } + /// Draw with or without brackets: `[Example Button]` or `Example Button` + pub fn bracketed(self, bracketed: bool) -> Self { + Self { bracketed, ..self } + } +} + +impl Default for ButtonStyle { + fn default() -> Self { + Self { + accelerator: None, + checked: None, + bracketed: true, // Default style for most buttons. Brackets may be disabled e.g. for buttons in menus + } + } +} + /// There's two types of lifetimes the TUI code needs to manage: /// * Across frames /// * Per frame @@ -652,7 +687,7 @@ impl Tui { fn report_context_completion<'a>(&'a mut self, ctx: &mut Context<'a, '_>) { // If this hits, you forgot to block_end() somewhere. The best way to figure // out where is to do a binary search of commenting out code in main.rs. - debug_assert!(ctx.tree.current_node.borrow().stack_parent.is_none()); + debug_assert!(ctx.tree.current_node.borrow().stack_parent.is_none(), "Dangling parent! Did you miss a block_end?"); // End the root node. ctx.block_end(); @@ -1944,17 +1979,12 @@ impl<'a> Context<'a, '_> { /// Creates a button with the given text. /// Returns true if the button was activated. - pub fn button(&mut self, classname: &'static str, text: &str) -> bool { - self.styled_label_begin(classname); + pub fn button(&mut self, classname: &'static str, text: &str, style: ButtonStyle) -> bool { + self.button_label(classname, text, style); self.attr_focusable(); if self.is_focused() { self.attr_reverse(); } - self.styled_label_add_text("["); - self.styled_label_add_text(text); - self.styled_label_add_text("]"); - self.styled_label_end(); - self.button_activated() } @@ -3024,7 +3054,7 @@ impl<'a> Context<'a, '_> { let mixin = self.tree.current_node.borrow().child_count as u64; self.next_block_id_mixin(mixin); - self.menubar_label(text, accelerator, None); + self.button_label("menu_button", text, ButtonStyle::default().accelerator(accelerator).bracketed(false)); self.attr_focusable(); self.attr_padding(Rect::two(0, 1)); @@ -3098,7 +3128,7 @@ impl<'a> Context<'a, '_> { let clicked = self.button_activated() || self.consume_shortcut(InputKey::new(accelerator as u32)); - self.menubar_label(text, accelerator, Some(checked)); + self.button_label("menu_checkbox", text, ButtonStyle::default().bracketed(false).checked(checked).accelerator(accelerator)); self.menubar_shortcut(shortcut); if clicked { @@ -3146,51 +3176,64 @@ impl<'a> Context<'a, '_> { self.table_end(); } - fn menubar_label(&mut self, text: &str, accelerator: char, checked: Option) { - if !accelerator.is_ascii_uppercase() { - self.label("label", text); - return; - } - - let mut off = text.len(); - - for (i, c) in text.bytes().enumerate() { - // Perfect match (uppercase character) --> stop - if c as char == accelerator { - off = i; - break; - } - // Inexact match (lowercase character) --> use first hit - if (c & !0x20) as char == accelerator && off == text.len() { - off = i; - } + /// Renders a button label with an optional accelerator character + /// May also renders a checkbox or square brackets for inline buttons + fn button_label(&mut self, classname: &'static str, text: &str, style: ButtonStyle) { + // Label prefix + self.styled_label_begin(classname); + if style.bracketed { + self.styled_label_add_text("["); } - - self.styled_label_begin("label"); - if let Some(checked) = checked { + if let Some(checked) = style.checked { self.styled_label_add_text(if checked { "▣ " } else { " " }); } + // Label text + match style.accelerator { + Some(accelerator) if accelerator.is_ascii_uppercase() => { + // Complex case: + // Locate the offset of the acclerator character in the label text + let mut off = text.len(); + for (i, c) in text.bytes().enumerate() { + // Perfect match (uppercase character) --> stop + if c as char == accelerator { + off = i; + break; + } + // Inexact match (lowercase character) --> use first hit + if (c & !0x20) as char == accelerator && off == text.len() { + off = i; + } + } - if off < text.len() { - // Add an underline to the accelerator. - self.styled_label_add_text(&text[..off]); - self.styled_label_set_attributes(Attributes::Underlined); - self.styled_label_add_text(&text[off..off + 1]); - self.styled_label_set_attributes(Attributes::None); - self.styled_label_add_text(&text[off + 1..]); - } else { - // Add the accelerator in parentheses and underline it. - let ch = accelerator as u8; - self.styled_label_add_text(text); - self.styled_label_add_text("("); - self.styled_label_set_attributes(Attributes::Underlined); - self.styled_label_add_text(unsafe { str_from_raw_parts(&ch, 1) }); - self.styled_label_set_attributes(Attributes::None); - self.styled_label_add_text(")"); + if off < text.len() { + // Add an underline to the accelerator. + self.styled_label_add_text(&text[..off]); + self.styled_label_set_attributes(Attributes::Underlined); + self.styled_label_add_text(&text[off..off + 1]); + self.styled_label_set_attributes(Attributes::None); + self.styled_label_add_text(&text[off + 1..]); + } else { + // Add the accelerator in parentheses and underline it. + let ch = accelerator as u8; + self.styled_label_add_text(text); + self.styled_label_add_text("("); + self.styled_label_set_attributes(Attributes::Underlined); + self.styled_label_add_text(unsafe { str_from_raw_parts(&ch, 1) }); + self.styled_label_set_attributes(Attributes::None); + self.styled_label_add_text(")"); + } + }, + _ => { + // Simple case: + // no accelerator character + self.styled_label_add_text(text); + }, + } + // Label postfix + if style.bracketed { + self.styled_label_add_text("]"); } - self.styled_label_end(); - self.attr_padding(Rect { left: 0, top: 0, right: 2, bottom: 0 }); } fn menubar_shortcut(&mut self, shortcut: InputKey) { @@ -3216,7 +3259,7 @@ impl<'a> Context<'a, '_> { self.block_begin("shortcut"); self.block_end(); } - self.attr_padding(Rect { left: 0, top: 0, right: 2, bottom: 0 }); + self.attr_padding(Rect { left: 2, top: 0, right: 2, bottom: 0 }); } }