From 0070107039c9be0f887379e18bc861a915c4aa51 Mon Sep 17 00:00:00 2001 From: "KOSMOS, Tzushih.K" <48860861+kisaraki@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:01:49 +0800 Subject: [PATCH] feat: show find and replace in centered dialogs --- crates/edit/src/bin/edit/draw_editor.rs | 183 ++++++++++++++---------- crates/edit/src/bin/edit/main.rs | 6 + 2 files changed, 117 insertions(+), 72 deletions(-) diff --git a/crates/edit/src/bin/edit/draw_editor.rs b/crates/edit/src/bin/edit/draw_editor.rs index ce03bbeb4cb..c1b0493e0b1 100644 --- a/crates/edit/src/bin/edit/draw_editor.rs +++ b/crates/edit/src/bin/edit/draw_editor.rs @@ -12,17 +12,10 @@ use crate::localization::*; use crate::state::*; pub fn draw_editor(ctx: &mut Context, state: &mut State) { - if !matches!(state.wants_search.kind, StateSearchKind::Hidden | StateSearchKind::Disabled) { - draw_search(ctx, state); - } - let size = ctx.size(); - // TODO: The layout code should be able to just figure out the height on its own. - let height_reduction = match state.wants_search.kind { - StateSearchKind::Search => 4, - StateSearchKind::Replace => 5, - _ => 2, - }; + // EN: Search and Replace are centered modals, so only the menu and status rows are reserved. + // 中文:搜尋與取代改為中央對話框,因此只需保留選單列與狀態列。 + let height_reduction = 2; if let Some(doc) = state.documents.active() { ctx.textarea("textarea", doc.buffer.clone()); @@ -35,7 +28,7 @@ pub fn draw_editor(ctx: &mut Context, state: &mut State) { ctx.attr_intrinsic_size(Size { width: 0, height: size.height - height_reduction }); } -fn draw_search(ctx: &mut Context, state: &mut State) { +pub fn draw_dialog_search(ctx: &mut Context, state: &mut State) { if let Err(err) = icu::init() { error_log_add(ctx, state, err.into()); state.wants_search.kind = StateSearchKind::Disabled; @@ -47,6 +40,7 @@ fn draw_search(ctx: &mut Context, state: &mut State) { return; }; + let kind = state.wants_search.kind; let mut action = None; let mut focus = StateSearchKind::Hidden; @@ -58,22 +52,33 @@ fn draw_search(ctx: &mut Context, state: &mut State) { // Otherwise, focus the replace input field, if it exists. if let Some(selection) = doc.buffer.borrow_mut().extract_user_selection(false) { state.search_needle = string_from_utf8_lossy_owned(selection); - focus = state.wants_search.kind; + focus = kind; } } - ctx.block_begin("search"); - ctx.attr_focus_well(); - ctx.attr_background_rgba(ctx.indexed(IndexedColor::White)); - ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::Black)); - { - if ctx.contains_focus() && ctx.consume_shortcut(vk::ESCAPE) { - state.wants_search.kind = StateSearchKind::Hidden; - } + // EN: Render Search and Replace above the complete editor instead of consuming document rows. + // 中文:搜尋與取代顯示於完整編輯器上方,不再占用文件列。 + let title = if kind == StateSearchKind::Replace { + loc(LocId::EditReplace) + } else { + loc(LocId::EditFind) + }; + let mut cancel = false; - ctx.table_begin("needle"); - ctx.table_set_cell_gap(Size { width: 1, height: 0 }); + ctx.modal_begin("search", title); + { + ctx.block_begin("search-content"); + ctx.inherit_focus(); + ctx.attr_padding(Rect::three(1, 2, 1)); { + // EN: F3 continues the search while the modal has focus. + // 中文:對話框取得焦點時,F3 仍可繼續尋找。 + if ctx.contains_focus() && ctx.consume_shortcut(vk::F3) { + action = Some(SearchAction::Search); + } + + ctx.table_begin("search-fields"); + ctx.table_set_cell_gap(Size { width: 1, height: 0 }); { ctx.table_next_row(); ctx.label("label", loc(LocId::SearchNeedleLabel)); @@ -85,75 +90,109 @@ fn draw_search(ctx: &mut Context, state: &mut State) { ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red)); ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite)); } - ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 }); + ctx.attr_intrinsic_size(Size { width: 48, height: 1 }); if focus == StateSearchKind::Search { ctx.steal_focus(); } if ctx.is_focused() && ctx.consume_shortcut(vk::RETURN) { action = Some(SearchAction::Search); } + if kind == StateSearchKind::Replace { + ctx.table_next_row(); + ctx.label("label", loc(LocId::SearchReplacementLabel)); + + ctx.editline("replacement", &mut state.search_replacement); + ctx.attr_intrinsic_size(Size { width: 48, height: 1 }); + if focus == StateSearchKind::Replace { + ctx.steal_focus(); + } + if ctx.is_focused() { + if ctx.consume_shortcut(vk::RETURN) { + action = Some(SearchAction::Replace); + } else if ctx.consume_shortcut(kbmod::CTRL_ALT | vk::RETURN) { + action = Some(SearchAction::ReplaceAll); + } + } + } } + ctx.table_end(); + + ctx.table_begin("search-options"); + ctx.attr_padding(Rect::three(1, 0, 0)); + ctx.table_set_columns(&[48]); + { + let mut change = false; - if state.wants_search.kind == StateSearchKind::Replace { + // EN: Each translated option gets its own row so it cannot widen the modal. + // 中文:每個翻譯後的選項各占一列,避免撐寬對話框。 + ctx.table_next_row(); + change |= ctx.checkbox( + "match-case", + loc(LocId::SearchMatchCase), + &mut state.search_options.match_case, + ); + ctx.table_next_row(); + change |= ctx.checkbox( + "whole-word", + loc(LocId::SearchWholeWord), + &mut state.search_options.whole_word, + ); ctx.table_next_row(); - ctx.label("label", loc(LocId::SearchReplacementLabel)); + change |= ctx.checkbox( + "use-regex", + loc(LocId::SearchUseRegex), + &mut state.search_options.use_regex, + ); + if change { + action = Some(SearchAction::Search); + } + } + ctx.table_end(); - ctx.editline("replacement", &mut state.search_replacement); - ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 }); - if focus == StateSearchKind::Replace { - ctx.steal_focus(); + // EN: Explicit buttons keep all operations discoverable in the modal. + // 中文:以明確按鈕呈現對話框中的所有操作。 + ctx.table_begin("search-actions"); + ctx.inherit_focus(); + ctx.attr_padding(Rect::three(1, 0, 0)); + ctx.attr_position(Position::Center); + ctx.table_set_cell_gap(Size { width: 2, height: 0 }); + { + ctx.table_next_row(); + ctx.inherit_focus(); + if ctx.button("find", loc(LocId::EditFind), ButtonStyle::default()) { + action = Some(SearchAction::Search); } - if ctx.is_focused() { - if ctx.consume_shortcut(vk::RETURN) { + + if kind == StateSearchKind::Replace { + ctx.inherit_focus(); + if ctx.button("replace", loc(LocId::EditReplace), ButtonStyle::default()) { action = Some(SearchAction::Replace); - } else if ctx.consume_shortcut(kbmod::CTRL_ALT | vk::RETURN) { + } + + ctx.inherit_focus(); + if ctx.button( + "replace-all", + loc(LocId::SearchReplaceAll), + ButtonStyle::default(), + ) { action = Some(SearchAction::ReplaceAll); } } - } - } - ctx.table_end(); - - ctx.table_begin("options"); - ctx.table_set_cell_gap(Size { width: 2, height: 0 }); - { - let mut change = false; - let mut change_action = Some(SearchAction::Search); - - ctx.table_next_row(); - - change |= ctx.checkbox( - "match-case", - loc(LocId::SearchMatchCase), - &mut state.search_options.match_case, - ); - change |= ctx.checkbox( - "whole-word", - loc(LocId::SearchWholeWord), - &mut state.search_options.whole_word, - ); - change |= ctx.checkbox( - "use-regex", - loc(LocId::SearchUseRegex), - &mut state.search_options.use_regex, - ); - if state.wants_search.kind == StateSearchKind::Replace - && ctx.button("replace-all", loc(LocId::SearchReplaceAll), ButtonStyle::default()) - { - change = true; - change_action = Some(SearchAction::ReplaceAll); - } - if ctx.button("close", loc(LocId::SearchClose), ButtonStyle::default()) { - state.wants_search.kind = StateSearchKind::Hidden; - } - if change { - action = change_action; + ctx.inherit_focus(); + cancel |= ctx.button("close", loc(LocId::SearchClose), ButtonStyle::default()); } + ctx.table_end(); } - ctx.table_end(); + ctx.block_end(); + } + cancel |= ctx.modal_end(); + + if cancel { + state.wants_search.kind = StateSearchKind::Hidden; + ctx.needs_rerender(); + return; } - ctx.block_end(); if let Some(action) = action { search_execute(ctx, state, action); diff --git a/crates/edit/src/bin/edit/main.rs b/crates/edit/src/bin/edit/main.rs index 6bf63456584..4dd9610ea62 100644 --- a/crates/edit/src/bin/edit/main.rs +++ b/crates/edit/src/bin/edit/main.rs @@ -341,6 +341,12 @@ fn draw(ctx: &mut Context, state: &mut State) { draw_editor(ctx, state); draw_statusbar(ctx, state); + // EN: Draw Search and Replace after the editor so they appear as centered modal dialogs. + // 中文:在編輯器之後繪製搜尋與取代,使其顯示為中央對話框。 + if !matches!(state.wants_search.kind, StateSearchKind::Hidden | StateSearchKind::Disabled) { + draw_dialog_search(ctx, state); + } + if state.wants_close { draw_handle_wants_close(ctx, state); }