refactor(editor): migrate file and git menus to shared menu
This commit is contained in:
parent
b64a928bee
commit
94f8be6844
|
|
@ -16,11 +16,10 @@
|
|||
//! ### Move STATE, not RENDER code
|
||||
//!
|
||||
//! Many of these types are *declared* under shell-core's `widgets/`
|
||||
//! module — `ExportFormat` in `widgets/export_dialog.rs`,
|
||||
//! `FileMenuChoice` in `widgets/file_menu.rs`. They are data/state
|
||||
//! enums, not rendering code, so their type definitions belong in
|
||||
//! the state layer. The widget *painting / hit-test* code stays in
|
||||
//! shell-core untouched.
|
||||
//! module — for example `ExportFormat` in `widgets/export_dialog.rs`.
|
||||
//! They are data/state enums, not rendering code, so their type
|
||||
//! definitions belong in the state layer. The widget *painting /
|
||||
//! hit-test* code stays in shell-core untouched.
|
||||
//!
|
||||
//! All types here are plain data (enums + structs of primitives /
|
||||
//! strings / ids), so `op-editor-core` stays wasm32-clean.
|
||||
|
|
@ -54,19 +53,6 @@ pub use crate::property_panel_state::{
|
|||
PaddingEditMode, PropertyTab,
|
||||
};
|
||||
|
||||
/// File-menu choices. State enum ported from shell-core's
|
||||
/// `widgets/file_menu::FileMenuChoice`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FileMenuChoice {
|
||||
NewFile,
|
||||
OpenFile,
|
||||
Save,
|
||||
SaveAs,
|
||||
ExportImage,
|
||||
OpenRecent(usize),
|
||||
ClearRecent,
|
||||
}
|
||||
|
||||
/// File-menu actions the host runner has to handle (rfd dialogs +
|
||||
/// serde live host-side, not here). `ExportImage` opens the picker;
|
||||
/// `ExportImageConfirm` commits.
|
||||
|
|
@ -504,6 +490,8 @@ pub struct GitPanelState {
|
|||
/// (switch-tracked / clear-author / remote-settings › / SSH-keys › /
|
||||
/// close-repo). Mirrors the TS header's local `overflowOpen`.
|
||||
pub overflow_open: bool,
|
||||
/// Shared interaction state for the top-level overflow menu rows.
|
||||
pub overflow_menu: jian_widgets::components::menu::MenuState,
|
||||
/// Which view the overflow popover is showing — the top-level menu
|
||||
/// or one of its subviews (remote settings). Resets to `Menu` each
|
||||
/// time the popover closes. Mirrors the TS header's `overflowView`.
|
||||
|
|
@ -511,6 +499,8 @@ pub struct GitPanelState {
|
|||
/// Ready-state header: whether the branch-picker dropdown (opened
|
||||
/// from the `⎇ <branch> ▾` button) is open.
|
||||
pub branch_picker_open: bool,
|
||||
/// Shared interaction state for branch-picker dropdown rows.
|
||||
pub branch_picker_menu: jian_widgets::components::menu::MenuState,
|
||||
/// Current branch name of that repository.
|
||||
pub branch: Option<String>,
|
||||
/// All local branch names, sorted — the panel lists them for
|
||||
|
|
@ -803,8 +793,9 @@ pub struct EditorUiState {
|
|||
// --- File menu --------------------------------------------------
|
||||
/// File-menu dropdown open (anchored under folder + chevron).
|
||||
pub file_menu_open: bool,
|
||||
/// File-menu row currently hovered — drives the per-row tint.
|
||||
pub file_menu_hover: Option<FileMenuChoice>,
|
||||
/// Shared file-menu interaction state; `hover = None` means no
|
||||
/// actionable row hovered.
|
||||
pub file_menu: jian_widgets::components::menu::MenuState,
|
||||
/// Pending file-menu action for the host runner to handle.
|
||||
pub pending_file_action: Option<FileAction>,
|
||||
/// Recent files (head = newest, cap 10).
|
||||
|
|
@ -1194,7 +1185,7 @@ impl Default for EditorUiState {
|
|||
locale: Locale::ZhCn,
|
||||
locale_picker: jian_widgets::components::select::SelectState::default(),
|
||||
file_menu_open: false,
|
||||
file_menu_hover: None,
|
||||
file_menu: Default::default(),
|
||||
pending_file_action: None,
|
||||
recent_files: Vec::new(),
|
||||
file_name_display: None,
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ pub use design_md::{extract_design_md_from_document, generate_design_md, parse_d
|
|||
pub use design_md_button_state::DesignMdButton;
|
||||
pub use editor_ui_state::{
|
||||
BooleanOp, CloneField, CloneFormState, CommitDiffPatch, CommitDiffSummary, CommitDiffView,
|
||||
DesignMdRequest, EditorUiState, ExportFormat, FileAction, FileMenuChoice, FillType, FlexLayout,
|
||||
DesignMdRequest, EditorUiState, ExportFormat, FileAction, FillType, FlexLayout,
|
||||
GitBranchPickerMode, GitCandidateFile, GitCommitSummary, GitDiffTarget, GitDiffView,
|
||||
GitFileEntry, GitOverflowView, GitPanelAction, GitPanelState, ImageAdjustmentField,
|
||||
ImageFillMode, LayerContextMenuState, Locale, MergeConflictRow, MergeResolveFile,
|
||||
|
|
|
|||
|
|
@ -27,25 +27,6 @@ pub fn translate(ui: &EditorUiState, key: &'static str) -> &'static str {
|
|||
crate::i18n::translate(ui.locale, key)
|
||||
}
|
||||
|
||||
/// Map an `op_editor_core::FileMenuChoice` onto the widget-layer
|
||||
/// `widgets::file_menu::FileMenuChoice`. Variant-identical; bridges
|
||||
/// the file-menu hover state.
|
||||
pub fn doc_file_menu_choice(
|
||||
c: op_editor_core::FileMenuChoice,
|
||||
) -> crate::widgets::file_menu::FileMenuChoice {
|
||||
use crate::widgets::file_menu::FileMenuChoice as D;
|
||||
use op_editor_core::FileMenuChoice as O;
|
||||
match c {
|
||||
O::NewFile => D::NewFile,
|
||||
O::OpenFile => D::OpenFile,
|
||||
O::Save => D::Save,
|
||||
O::SaveAs => D::SaveAs,
|
||||
O::ExportImage => D::ExportImage,
|
||||
O::OpenRecent(i) => D::OpenRecent(i),
|
||||
O::ClearRecent => D::ClearRecent,
|
||||
}
|
||||
}
|
||||
|
||||
/// Map an `op_editor_core::ExportFormat` onto the widget-layer
|
||||
/// `widgets::export_dialog::ExportFormat`. Variant-identical.
|
||||
pub fn doc_export_format(
|
||||
|
|
@ -67,29 +48,8 @@ pub fn doc_export_format(
|
|||
// The host feeds widget hit-test results back into `EditorState`'s
|
||||
// `editor_ui_state`. Most widget hit-tests already emit canonical
|
||||
// `op_editor_core` types (`Tool`, `AlignAction`, `PropertyFocus`, …)
|
||||
// so no conversion is needed. The three enums below stay widget-local
|
||||
// (`file_menu` / `shape_picker` / `export_dialog` own them) and so
|
||||
// still need a one-arm-per-variant bridge into the canonical
|
||||
// `editor_ui_state` enums the hover / format state fields hold.
|
||||
|
||||
/// Map the widget-layer `widgets::file_menu::FileMenuChoice` onto the
|
||||
/// canonical `op_editor_core::FileMenuChoice`. Reverse of
|
||||
/// [`doc_file_menu_choice`].
|
||||
pub fn file_menu_choice(
|
||||
c: crate::widgets::file_menu::FileMenuChoice,
|
||||
) -> op_editor_core::FileMenuChoice {
|
||||
use crate::widgets::file_menu::FileMenuChoice as W;
|
||||
use op_editor_core::FileMenuChoice as O;
|
||||
match c {
|
||||
W::NewFile => O::NewFile,
|
||||
W::OpenFile => O::OpenFile,
|
||||
W::Save => O::Save,
|
||||
W::SaveAs => O::SaveAs,
|
||||
W::ExportImage => O::ExportImage,
|
||||
W::OpenRecent(i) => O::OpenRecent(i),
|
||||
W::ClearRecent => O::ClearRecent,
|
||||
}
|
||||
}
|
||||
// so no conversion is needed. The remaining widget-local enums below
|
||||
// still need a one-arm-per-variant bridge into canonical state fields.
|
||||
|
||||
/// Map the widget-layer `widgets::toolbar::ToolbarAction` onto the
|
||||
/// canonical `op_editor_core::ToolbarAction`. Variant-identical;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@
|
|||
//! "Recent files" header + entries, finally Clear history.
|
||||
|
||||
use crate::theme::Theme;
|
||||
use crate::widgets::editor_state_ext::{doc_file_menu_choice, theme_for};
|
||||
use crate::widgets::editor_state_ext::theme_for;
|
||||
use crate::widgets::icons::{draw_icon, Icon};
|
||||
use crate::widgets::{LayoutBox, LayoutCx, PaintCx, Widget, WidgetId};
|
||||
use crate::{Color, Point2D, Rect, TextLayout};
|
||||
pub use jian_widgets::components::menu::MenuHit;
|
||||
use op_editor_core::editor_ui_state::EditorUiState;
|
||||
|
||||
/// Resolve a file-menu row label via `op-i18n`. The Rust file menu
|
||||
|
|
@ -54,11 +55,9 @@ pub struct FileMenu<'a> {
|
|||
pub theme: Theme,
|
||||
ui: &'a EditorUiState,
|
||||
pub recent: Vec<RecentEntry>,
|
||||
/// Mirrors `Document.ui.file_menu_hover` — populated by the
|
||||
/// host on cursor-move so paint can tint the row under the
|
||||
/// cursor. `None` = no hover (cursor outside the menu, or no
|
||||
/// movement since the menu opened).
|
||||
pub hovered: Option<FileMenuChoice>,
|
||||
/// Shared interaction state populated by the host on cursor-move.
|
||||
/// `hover` stores an actionable row index.
|
||||
pub menu: jian_widgets::components::menu::MenuState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -74,7 +73,7 @@ impl<'a> FileMenu<'a> {
|
|||
theme: theme_for(ui),
|
||||
ui,
|
||||
recent,
|
||||
hovered: ui.file_menu_hover.map(doc_file_menu_choice),
|
||||
menu: ui.file_menu.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,51 +122,81 @@ impl<'a> FileMenu<'a> {
|
|||
|
||||
/// Convenience alias: `hit_test` is reused for hover dispatch
|
||||
/// (same row geometry, no separate code path needed).
|
||||
pub fn hovered_at(&self, panel: Rect, point: Point2D) -> Option<FileMenuChoice> {
|
||||
self.hit_test(panel, point)
|
||||
pub fn hovered_at(&self, panel: Rect, point: Point2D) -> Option<usize> {
|
||||
match self.hit(panel, point) {
|
||||
MenuHit::Row(idx) => Some(idx),
|
||||
MenuHit::Inside | MenuHit::Outside => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `point` is in screen space; return the activated row, or None
|
||||
/// for clicks on dividers / headers / outside the menu.
|
||||
pub fn hit_test(&self, panel: Rect, point: Point2D) -> Option<FileMenuChoice> {
|
||||
if !(panel).contains(point) {
|
||||
return None;
|
||||
pub fn choice_for_row(&self, row: usize) -> Option<FileMenuChoice> {
|
||||
match row {
|
||||
0 => Some(FileMenuChoice::NewFile),
|
||||
1 => Some(FileMenuChoice::OpenFile),
|
||||
2 => Some(FileMenuChoice::Save),
|
||||
3 => Some(FileMenuChoice::SaveAs),
|
||||
4 => Some(FileMenuChoice::ExportImage),
|
||||
row if row < 5 + self.recent.len() => Some(FileMenuChoice::OpenRecent(row - 5)),
|
||||
row if !self.recent.is_empty() && row == 5 + self.recent.len() => {
|
||||
Some(FileMenuChoice::ClearRecent)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hit(&self, panel: Rect, point: Point2D) -> MenuHit {
|
||||
if !(panel).contains(point) {
|
||||
return MenuHit::Outside;
|
||||
}
|
||||
let mut row = 0usize;
|
||||
let mut y = panel.origin.y + PAD_Y;
|
||||
for choice in [FileMenuChoice::NewFile, FileMenuChoice::OpenFile] {
|
||||
for _ in 0..2 {
|
||||
if row_hit(panel.origin.x, y, point) {
|
||||
return Some(choice);
|
||||
return MenuHit::Row(row);
|
||||
}
|
||||
y += ROW_HEIGHT;
|
||||
row += 1;
|
||||
}
|
||||
y += DIVIDER_GAP * 2.0 + 1.0;
|
||||
for choice in [FileMenuChoice::Save, FileMenuChoice::SaveAs] {
|
||||
for _ in 0..2 {
|
||||
if row_hit(panel.origin.x, y, point) {
|
||||
return Some(choice);
|
||||
return MenuHit::Row(row);
|
||||
}
|
||||
y += ROW_HEIGHT;
|
||||
row += 1;
|
||||
}
|
||||
y += DIVIDER_GAP * 2.0 + 1.0;
|
||||
if row_hit(panel.origin.x, y, point) {
|
||||
return Some(FileMenuChoice::ExportImage);
|
||||
return MenuHit::Row(row);
|
||||
}
|
||||
y += ROW_HEIGHT;
|
||||
row += 1;
|
||||
y += DIVIDER_GAP * 2.0 + 1.0;
|
||||
y += HEADER_HEIGHT;
|
||||
for (i, _) in self.recent.iter().enumerate() {
|
||||
for _ in self.recent.iter() {
|
||||
if row_hit(panel.origin.x, y, point) {
|
||||
return Some(FileMenuChoice::OpenRecent(i));
|
||||
return MenuHit::Row(row);
|
||||
}
|
||||
y += ROW_HEIGHT;
|
||||
row += 1;
|
||||
}
|
||||
if self.recent.is_empty() {
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
y += DIVIDER_GAP * 2.0 + 1.0;
|
||||
if !self.recent.is_empty() && row_hit(panel.origin.x, y, point) {
|
||||
return Some(FileMenuChoice::ClearRecent);
|
||||
return MenuHit::Row(row);
|
||||
}
|
||||
MenuHit::Inside
|
||||
}
|
||||
|
||||
/// `point` is in screen space; return the activated row, or None
|
||||
/// for clicks on dividers / headers / outside the menu.
|
||||
pub fn hit_test(&self, panel: Rect, point: Point2D) -> Option<FileMenuChoice> {
|
||||
match self.hit(panel, point) {
|
||||
MenuHit::Row(idx) => self.choice_for_row(idx),
|
||||
MenuHit::Inside | MenuHit::Outside => None,
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +241,7 @@ impl<'a> Widget for FileMenu<'a> {
|
|||
cx.backend.fill_round_rect(rect, 10.0, self.theme.card);
|
||||
cx.backend
|
||||
.stroke_round_rect(rect, 10.0, self.theme.border, 1.0);
|
||||
let h = |c: FileMenuChoice| self.hovered == Some(c);
|
||||
let h = |row: usize| self.menu.hover == Some(row);
|
||||
let mut y = rect.origin.y + PAD_Y;
|
||||
paint_row(
|
||||
cx,
|
||||
|
|
@ -222,7 +251,7 @@ impl<'a> Widget for FileMenu<'a> {
|
|||
Icon::Plus,
|
||||
t(self.ui, "new"),
|
||||
"⌘N",
|
||||
h(FileMenuChoice::NewFile),
|
||||
h(0),
|
||||
);
|
||||
y += ROW_HEIGHT;
|
||||
paint_row(
|
||||
|
|
@ -233,7 +262,7 @@ impl<'a> Widget for FileMenu<'a> {
|
|||
Icon::FolderOpen,
|
||||
t(self.ui, "open"),
|
||||
"⌘O",
|
||||
h(FileMenuChoice::OpenFile),
|
||||
h(1),
|
||||
);
|
||||
y += ROW_HEIGHT;
|
||||
y = paint_divider(cx, &self.theme, rect, y);
|
||||
|
|
@ -245,7 +274,7 @@ impl<'a> Widget for FileMenu<'a> {
|
|||
Icon::Save,
|
||||
t(self.ui, "save"),
|
||||
"⌘S",
|
||||
h(FileMenuChoice::Save),
|
||||
h(2),
|
||||
);
|
||||
y += ROW_HEIGHT;
|
||||
paint_row(
|
||||
|
|
@ -256,7 +285,7 @@ impl<'a> Widget for FileMenu<'a> {
|
|||
Icon::Save,
|
||||
t(self.ui, "saveAs"),
|
||||
"⌘⇧S",
|
||||
h(FileMenuChoice::SaveAs),
|
||||
h(3),
|
||||
);
|
||||
y += ROW_HEIGHT;
|
||||
y = paint_divider(cx, &self.theme, rect, y);
|
||||
|
|
@ -268,7 +297,7 @@ impl<'a> Widget for FileMenu<'a> {
|
|||
Icon::Download,
|
||||
t(self.ui, "exportImage"),
|
||||
"⌘⇧P",
|
||||
h(FileMenuChoice::ExportImage),
|
||||
h(4),
|
||||
);
|
||||
y += ROW_HEIGHT;
|
||||
y = paint_divider(cx, &self.theme, rect, y);
|
||||
|
|
@ -285,14 +314,7 @@ impl<'a> Widget for FileMenu<'a> {
|
|||
y += ROW_HEIGHT;
|
||||
} else {
|
||||
for (i, entry) in self.recent.iter().enumerate() {
|
||||
paint_recent_row(
|
||||
cx,
|
||||
&self.theme,
|
||||
rect.origin.x,
|
||||
y,
|
||||
entry,
|
||||
h(FileMenuChoice::OpenRecent(i)),
|
||||
);
|
||||
paint_recent_row(cx, &self.theme, rect.origin.x, y, entry, h(5 + i));
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
|
@ -316,7 +338,7 @@ impl<'a> Widget for FileMenu<'a> {
|
|||
Icon::Trash,
|
||||
t(self.ui, "clearHistory"),
|
||||
"",
|
||||
h(FileMenuChoice::ClearRecent),
|
||||
h(5 + self.recent.len()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -576,3 +598,64 @@ fn format_age(ui: &EditorUiState, elapsed_secs: u64) -> String {
|
|||
.replace("{{count}}", &(elapsed_secs / 86400).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use jian_widgets::components::menu::MenuHit;
|
||||
|
||||
fn menu_panel(menu: &FileMenu<'_>) -> Rect {
|
||||
Rect {
|
||||
origin: Point2D::new(100.0, 50.0),
|
||||
size: Point2D::new(MENU_WIDTH, menu.height()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_uses_shared_menu_state_protocol() {
|
||||
let mut ui = EditorUiState::default();
|
||||
ui.file_menu.hover = Some(5);
|
||||
let menu = FileMenu::for_editor_ui(
|
||||
&ui,
|
||||
vec![
|
||||
RecentEntry {
|
||||
name: "one.op".to_string(),
|
||||
age: "now".to_string(),
|
||||
},
|
||||
RecentEntry {
|
||||
name: "two.op".to_string(),
|
||||
age: "now".to_string(),
|
||||
},
|
||||
],
|
||||
);
|
||||
assert_eq!(menu.menu.hover, Some(5));
|
||||
|
||||
let panel = menu_panel(&menu);
|
||||
let divider = DIVIDER_GAP * 2.0 + 1.0;
|
||||
let recent_y = panel.origin.y
|
||||
+ PAD_Y
|
||||
+ ROW_HEIGHT * 2.0
|
||||
+ divider
|
||||
+ ROW_HEIGHT * 2.0
|
||||
+ divider
|
||||
+ ROW_HEIGHT
|
||||
+ divider
|
||||
+ HEADER_HEIGHT
|
||||
+ ROW_HEIGHT * 0.5;
|
||||
assert_eq!(
|
||||
menu.hit(panel, Point2D::new(panel.origin.x + 20.0, recent_y)),
|
||||
MenuHit::Row(5)
|
||||
);
|
||||
assert_eq!(menu.choice_for_row(5), Some(FileMenuChoice::OpenRecent(0)));
|
||||
|
||||
let header_y = recent_y - ROW_HEIGHT * 0.5 - HEADER_HEIGHT * 0.5;
|
||||
assert_eq!(
|
||||
menu.hit(panel, Point2D::new(panel.origin.x + 20.0, header_y)),
|
||||
MenuHit::Inside
|
||||
);
|
||||
assert_eq!(
|
||||
menu.hit(panel, Point2D::new(panel.origin.x - 1.0, header_y)),
|
||||
MenuHit::Outside
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use crate::theme::Theme;
|
|||
use crate::widgets::editor_state_ext::theme_for;
|
||||
use crate::widgets::PaintCx;
|
||||
use crate::{Color, Point2D, Rect};
|
||||
pub use jian_widgets::components::menu::MenuHit;
|
||||
pub use jian_widgets::components::select::SelectHit;
|
||||
use op_editor_core::{EditorState, GitButton, GitPanelState};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use crate::widgets::git_panel::{contains, truncate, GitPanel, GitPanelHit, PAD};
|
|||
use crate::widgets::icons::{draw_icon, Icon};
|
||||
use crate::widgets::PaintCx;
|
||||
use crate::{Color, Point2D, Rect};
|
||||
use jian_widgets::components::menu::MenuHit;
|
||||
use op_editor_core::{GitBranchPickerMode, GitOverflowView};
|
||||
|
||||
/// Dropdown row height (TS menu item ≈ 28 px).
|
||||
|
|
@ -298,6 +299,9 @@ impl GitPanel<'_> {
|
|||
for (i, row) in rows.iter().enumerate() {
|
||||
let bi = if merging { candidates[i] } else { i };
|
||||
let is_current = self.state.branches.get(bi) == self.state.branch.as_ref();
|
||||
if self.state.branch_picker_menu.hover == Some(i) && !is_current {
|
||||
cx.backend.fill_round_rect(*row, 6.0, t.button_hover);
|
||||
}
|
||||
let name = truncate(
|
||||
self.state
|
||||
.branches
|
||||
|
|
@ -477,6 +481,22 @@ impl GitPanel<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn branch_picker_menu_hit(&self, panel_rect: Rect, point: Point2D) -> MenuHit {
|
||||
let panel = self.branch_picker_panel(panel_rect);
|
||||
if !contains(panel, point) {
|
||||
return MenuHit::Outside;
|
||||
}
|
||||
if self.state.branch_picker_mode == GitBranchPickerMode::Create {
|
||||
return MenuHit::Inside;
|
||||
}
|
||||
for (i, row) in self.branch_picker_row_rects(panel_rect).iter().enumerate() {
|
||||
if contains(*row, point) {
|
||||
return MenuHit::Row(i);
|
||||
}
|
||||
}
|
||||
MenuHit::Inside
|
||||
}
|
||||
|
||||
// ── Overflow menu ────────────────────────────────────────────────
|
||||
|
||||
/// The overflow `…` menu rect, anchored below the overflow button
|
||||
|
|
@ -520,8 +540,10 @@ impl GitPanel<'_> {
|
|||
cx.backend.fill_round_rect(panel, 8.0, t.popover);
|
||||
cx.backend.stroke_round_rect(panel, 8.0, t.border, 1.0);
|
||||
let rows = self.overflow_row_rects(panel_rect);
|
||||
for (item, row) in self.overflow_items().iter().zip(rows.iter()) {
|
||||
self.wash_if_hovered(cx, *row, 6.0, item.hit);
|
||||
for (i, (item, row)) in self.overflow_items().iter().zip(rows.iter()).enumerate() {
|
||||
if self.state.overflow_menu.hover == Some(i) {
|
||||
cx.backend.fill_round_rect(*row, 6.0, t.button_hover);
|
||||
}
|
||||
// Leaf icon (TS size=13 strokeWidth=1.75, muted).
|
||||
draw_icon(
|
||||
cx.backend,
|
||||
|
|
@ -569,20 +591,24 @@ impl GitPanel<'_> {
|
|||
/// Hit-test the overflow menu. `None` when the point is outside the
|
||||
/// popover (the caller then closes it + falls through).
|
||||
pub(super) fn overflow_hit(&self, panel_rect: Rect, point: Point2D) -> Option<GitPanelHit> {
|
||||
match self.overflow_menu_hit(panel_rect, point) {
|
||||
MenuHit::Row(idx) => self.overflow_items().get(idx).map(|item| item.hit),
|
||||
MenuHit::Inside => Some(GitPanelHit::Inside),
|
||||
MenuHit::Outside => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overflow_menu_hit(&self, panel_rect: Rect, point: Point2D) -> MenuHit {
|
||||
let panel = self.overflow_panel(panel_rect);
|
||||
if !contains(panel, point) {
|
||||
return None;
|
||||
return MenuHit::Outside;
|
||||
}
|
||||
for (item, row) in self
|
||||
.overflow_items()
|
||||
.iter()
|
||||
.zip(self.overflow_row_rects(panel_rect).iter())
|
||||
{
|
||||
for (i, row) in self.overflow_row_rects(panel_rect).iter().enumerate() {
|
||||
if contains(*row, point) {
|
||||
return Some(item.hit);
|
||||
return MenuHit::Row(i);
|
||||
}
|
||||
}
|
||||
Some(GitPanelHit::Inside)
|
||||
MenuHit::Inside
|
||||
}
|
||||
|
||||
/// Paint whichever overflow view is active — the menu or a subview.
|
||||
|
|
|
|||
|
|
@ -453,6 +453,50 @@ fn overflow_menu_maps_its_entries() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_menus_use_shared_menu_state_protocol() {
|
||||
use jian_widgets::components::menu::{MenuHit, MenuState};
|
||||
|
||||
let s = state_with(GitPanelState {
|
||||
branch: Some("main".to_string()),
|
||||
overflow_open: true,
|
||||
overflow_menu: MenuState { hover: Some(2) },
|
||||
..open_repo()
|
||||
});
|
||||
let panel = GitPanel::for_editor(&s).unwrap();
|
||||
let rect = panel_rect(&panel);
|
||||
let rows = panel.overflow_row_rects(rect);
|
||||
assert_eq!(panel.state.overflow_menu.hover, Some(2));
|
||||
assert_eq!(
|
||||
panel.overflow_menu_hit(rect, centre(rows[2])),
|
||||
MenuHit::Row(2)
|
||||
);
|
||||
assert_eq!(
|
||||
panel.overflow_menu_hit(rect, Point2D::new(rows[2].origin.x, rows[2].origin.y - 4.0)),
|
||||
MenuHit::Inside
|
||||
);
|
||||
|
||||
let s = state_with(GitPanelState {
|
||||
branch: Some("main".to_string()),
|
||||
branches: vec!["main".to_string(), "feature".to_string()],
|
||||
branch_picker_open: true,
|
||||
branch_picker_menu: MenuState { hover: Some(1) },
|
||||
..open_repo()
|
||||
});
|
||||
let panel = GitPanel::for_editor(&s).unwrap();
|
||||
let rect = panel_rect(&panel);
|
||||
let rows = panel.branch_picker_row_rects(rect);
|
||||
assert_eq!(panel.state.branch_picker_menu.hover, Some(1));
|
||||
assert_eq!(
|
||||
panel.branch_picker_menu_hit(rect, centre(rows[1])),
|
||||
MenuHit::Row(1)
|
||||
);
|
||||
assert_eq!(
|
||||
panel.branch_picker_menu_hit(rect, Point2D::new(rows[0].origin.x, rows[0].origin.y - 4.0)),
|
||||
MenuHit::Inside
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracked_picker_maps_rows_and_actions() {
|
||||
let s = state_with(GitPanelState {
|
||||
|
|
|
|||
|
|
@ -891,7 +891,7 @@ impl ApplicationHandler<DesktopEvent> for DesktopApp {
|
|||
) {
|
||||
let eui = &mut self.host.editor_state_mut().editor_ui;
|
||||
eui.file_menu_open = false;
|
||||
eui.file_menu_hover = None;
|
||||
eui.file_menu.hover = None;
|
||||
eui.export_dialog_open = true;
|
||||
self.host.mark_editor_state_dirty();
|
||||
self.request_redraw(true);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,11 @@ fn variable_row_input_keeps_resume_time_redraws_active() {
|
|||
app.host.set_now_ms(240);
|
||||
app.host.editor_state_mut().editor_ui.variable_row_focus =
|
||||
Some(op_editor_core::editor_ui_state::VariableRowFocus::Name(0));
|
||||
app.host.editor_state_mut().ui.property_caret_anchor_ms = 240;
|
||||
app.host
|
||||
.editor_state_mut()
|
||||
.editor_ui
|
||||
.variable_row_input
|
||||
.touch(240);
|
||||
|
||||
assert!(app.resume_time_needs_redraw());
|
||||
assert_eq!(app.host.next_animation_deadline_ms(), Some(740));
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ impl WidgetHostNative {
|
|||
{
|
||||
let ui = &mut self.editor_state.editor_ui;
|
||||
changed |= ui.canvas_hover_node.take().is_some();
|
||||
changed |= ui.file_menu_hover.take().is_some();
|
||||
changed |= ui.file_menu.hover.take().is_some();
|
||||
changed |= ui.locale_picker.hover.take().is_some();
|
||||
changed |= ui.shape_picker.hover.take().is_some();
|
||||
changed |= ui.fill_type_picker.hover.take().is_some();
|
||||
|
|
@ -146,11 +146,9 @@ impl WidgetHostNative {
|
|||
.unwrap_or(0);
|
||||
let menu = FileMenu::from_editor_ui(&self.editor_state.editor_ui, now_secs);
|
||||
let panel = menu.rect_at(anchor);
|
||||
let new_hover = menu
|
||||
.hovered_at(panel, Point2D::new(x, y))
|
||||
.map(op_editor_ui::widgets::editor_state_ext::file_menu_choice);
|
||||
if new_hover != self.editor_state.editor_ui.file_menu_hover {
|
||||
self.editor_state.editor_ui.file_menu_hover = new_hover;
|
||||
let new_hover = menu.hovered_at(panel, Point2D::new(x, y));
|
||||
if new_hover != self.editor_state.editor_ui.file_menu.hover {
|
||||
self.editor_state.editor_ui.file_menu.hover = new_hover;
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
use super::helpers::GIT_PANEL_CARET_GAP;
|
||||
use super::{CursorHint, WidgetHostNative};
|
||||
use op_editor_core::{GitFileEntry, GitPanelAction};
|
||||
use op_editor_ui::widgets::{GitPanel, TopBar, TOP_BAR_HEIGHT};
|
||||
use op_editor_ui::widgets::{GitPanel, GitPanelHit, TopBar, TOP_BAR_HEIGHT};
|
||||
use op_editor_ui::{Point2D, Rect};
|
||||
|
||||
/// A host with the Git panel open in its no-repo onboarding state
|
||||
|
|
@ -23,6 +23,24 @@ fn host_with_git_panel_open() -> WidgetHostNative {
|
|||
host
|
||||
}
|
||||
|
||||
fn find_git_hit(panel: &GitPanel<'_>, body: Rect, target: GitPanelHit) -> Point2D {
|
||||
let mut y = body.origin.y;
|
||||
let max_y = body.origin.y + body.size.y + 140.0;
|
||||
while y <= max_y {
|
||||
let mut x = body.origin.x;
|
||||
let max_x = body.origin.x + body.size.x;
|
||||
while x <= max_x {
|
||||
let point = Point2D::new(x, y);
|
||||
if panel.hit_test(body, point) == Some(target) {
|
||||
return point;
|
||||
}
|
||||
x += 4.0;
|
||||
}
|
||||
y += 4.0;
|
||||
}
|
||||
panic!("could not find git hit target {target:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_git_popover_is_modal_and_dismisses_on_any_outside_press() {
|
||||
let mut host = WidgetHostNative::new();
|
||||
|
|
@ -368,6 +386,49 @@ fn init_card_hover_tracks_the_card_index_and_not_allowed_cursor() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_popover_row_hover_uses_shared_menu_state() {
|
||||
let mut host = WidgetHostNative::new();
|
||||
let (vw, vh) = (1400.0, 900.0);
|
||||
host.last_viewport_w = vw;
|
||||
host.last_viewport_h = vh;
|
||||
{
|
||||
let panel = &mut host.editor_state_mut().editor_ui.git_panel;
|
||||
panel.open = true;
|
||||
panel.loading = false;
|
||||
panel.in_repo = true;
|
||||
panel.branch = Some("main".to_string());
|
||||
panel.overflow_open = true;
|
||||
}
|
||||
let body = host.git_panel_rect(vw, vh).expect("panel open");
|
||||
let panel = GitPanel::for_editor(host.editor_state()).expect("panel widget");
|
||||
let point = find_git_hit(&panel, body, GitPanelHit::OverflowRemoteSettings);
|
||||
assert!(host.update_git_panel_ready_hover(point.x, point.y));
|
||||
assert_eq!(
|
||||
host.editor_state().editor_ui.git_panel.overflow_menu.hover,
|
||||
Some(2)
|
||||
);
|
||||
|
||||
{
|
||||
let panel = &mut host.editor_state_mut().editor_ui.git_panel;
|
||||
panel.overflow_open = false;
|
||||
panel.overflow_menu.hover = None;
|
||||
panel.branch_picker_open = true;
|
||||
panel.branches = vec!["main".to_string(), "feature".to_string()];
|
||||
}
|
||||
let panel = GitPanel::for_editor(host.editor_state()).expect("panel widget");
|
||||
let point = find_git_hit(&panel, body, GitPanelHit::SwitchBranch(1));
|
||||
assert!(host.update_git_panel_ready_hover(point.x, point.y));
|
||||
assert_eq!(
|
||||
host.editor_state()
|
||||
.editor_ui
|
||||
.git_panel
|
||||
.branch_picker_menu
|
||||
.hover,
|
||||
Some(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_wizard_owns_keyboard_and_enter() {
|
||||
use op_editor_core::{CloneField, CloneFormState, GitPanelAction};
|
||||
|
|
|
|||
|
|
@ -185,7 +185,9 @@ impl WidgetHostNative {
|
|||
// Toggle the branch-picker dropdown; close the overflow
|
||||
// menu so only one ready-state popover is open at a time.
|
||||
panel.branch_picker_open = !panel.branch_picker_open;
|
||||
panel.branch_picker_menu.hover = None;
|
||||
panel.overflow_open = false;
|
||||
panel.overflow_menu.hover = None;
|
||||
panel.overflow_view = GitOverflowView::Menu;
|
||||
panel.close_tracked_picker();
|
||||
// Always (re)open on the branch list — a prior session's
|
||||
|
|
@ -198,22 +200,27 @@ impl WidgetHostNative {
|
|||
// Always (re)open on the top-level menu so a prior
|
||||
// session's subview never leaks back in.
|
||||
panel.overflow_open = !panel.overflow_open;
|
||||
panel.overflow_menu.hover = None;
|
||||
panel.overflow_view = GitOverflowView::Menu;
|
||||
panel.branch_picker_open = false;
|
||||
panel.branch_picker_menu.hover = None;
|
||||
panel.close_tracked_picker();
|
||||
}
|
||||
Some(GitPanelHit::OverflowRemoteSettings) => {
|
||||
panel.overflow_menu.hover = None;
|
||||
panel.overflow_view = GitOverflowView::RemoteSettings;
|
||||
panel.close_tracked_picker();
|
||||
}
|
||||
Some(GitPanelHit::OverflowSshKeys) => {
|
||||
// Open the SSH-keys subview (host enumerates the stored keys).
|
||||
panel.overflow_menu.hover = None;
|
||||
panel.pending_action = Some(GitPanelAction::EnterSshKeys);
|
||||
panel.close_tracked_picker();
|
||||
}
|
||||
Some(GitPanelHit::SshGenerateKey) => {
|
||||
panel.pending_action = Some(GitPanelAction::SetupSshAuth);
|
||||
panel.overflow_open = false;
|
||||
panel.overflow_menu.hover = None;
|
||||
panel.overflow_view = GitOverflowView::Menu;
|
||||
}
|
||||
Some(GitPanelHit::SshImportKey) => {
|
||||
|
|
@ -229,18 +236,21 @@ impl WidgetHostNative {
|
|||
Some(GitPanelHit::OverflowSwitchTracked) => {
|
||||
// Host enumerates the repo's `.op` candidates, then flips the
|
||||
// subview to the tracked-file picker.
|
||||
panel.overflow_menu.hover = None;
|
||||
panel.open_tracked_picker();
|
||||
panel.pending_action = Some(GitPanelAction::EnterTrackedPicker);
|
||||
}
|
||||
Some(GitPanelHit::OverflowClearAuthor) => {
|
||||
panel.pending_action = Some(GitPanelAction::ClearAuthor);
|
||||
panel.overflow_open = false;
|
||||
panel.overflow_menu.hover = None;
|
||||
panel.overflow_view = GitOverflowView::Menu;
|
||||
panel.close_tracked_picker();
|
||||
}
|
||||
Some(GitPanelHit::OverflowCloseRepo) => {
|
||||
panel.pending_action = Some(GitPanelAction::CloseRepo);
|
||||
panel.overflow_open = false;
|
||||
panel.overflow_menu.hover = None;
|
||||
panel.overflow_view = GitOverflowView::Menu;
|
||||
panel.close_tracked_picker();
|
||||
}
|
||||
|
|
@ -269,9 +279,11 @@ impl WidgetHostNative {
|
|||
Some(GitPanelHit::DismissPopover) => {
|
||||
// Click outside an open popover — close it + swallow.
|
||||
panel.branch_picker_open = false;
|
||||
panel.branch_picker_menu.hover = None;
|
||||
panel.branch_picker_mode = GitBranchPickerMode::List;
|
||||
panel.branch_create_input.set_text("");
|
||||
panel.overflow_open = false;
|
||||
panel.overflow_menu.hover = None;
|
||||
panel.overflow_view = GitOverflowView::Menu;
|
||||
panel.close_tracked_picker();
|
||||
panel.defocus_text_inputs();
|
||||
|
|
@ -288,14 +300,17 @@ impl WidgetHostNative {
|
|||
}
|
||||
// Close the branch-picker dropdown after a pick.
|
||||
panel.branch_picker_open = false;
|
||||
panel.branch_picker_menu.hover = None;
|
||||
}
|
||||
Some(GitPanelHit::MergeBranch(index)) => {
|
||||
if let Some(name) = panel.branches.get(index).cloned() {
|
||||
panel.pending_action = Some(GitPanelAction::MergeBranch(name));
|
||||
}
|
||||
panel.branch_picker_open = false;
|
||||
panel.branch_picker_menu.hover = None;
|
||||
}
|
||||
Some(GitPanelHit::BranchCreateMode) => {
|
||||
panel.branch_picker_menu.hover = None;
|
||||
panel.branch_picker_mode = GitBranchPickerMode::Create;
|
||||
panel.branch_create_input.set_text("");
|
||||
panel.branch_create_input.touch(now);
|
||||
|
|
@ -305,6 +320,7 @@ impl WidgetHostNative {
|
|||
panel.https_focused = false;
|
||||
}
|
||||
Some(GitPanelHit::BranchMergeMode) => {
|
||||
panel.branch_picker_menu.hover = None;
|
||||
panel.branch_picker_mode = GitBranchPickerMode::Merge;
|
||||
panel.branch_create_focused = false;
|
||||
}
|
||||
|
|
@ -323,11 +339,13 @@ impl WidgetHostNative {
|
|||
panel.branch_create_input.set_text("");
|
||||
panel.branch_create_focused = false;
|
||||
panel.branch_picker_open = false;
|
||||
panel.branch_picker_menu.hover = None;
|
||||
}
|
||||
}
|
||||
Some(GitPanelHit::BranchPickerCancel) => {
|
||||
// Step a create / merge sub-mode back to the branch list.
|
||||
panel.branch_picker_mode = GitBranchPickerMode::List;
|
||||
panel.branch_picker_menu.hover = None;
|
||||
panel.branch_create_input.set_text("");
|
||||
panel.branch_create_focused = false;
|
||||
}
|
||||
|
|
@ -478,7 +496,9 @@ impl WidgetHostNative {
|
|||
panel.open = false;
|
||||
panel.defocus_text_inputs();
|
||||
panel.branch_picker_open = false;
|
||||
panel.branch_picker_menu.hover = None;
|
||||
panel.overflow_open = false;
|
||||
panel.overflow_menu.hover = None;
|
||||
panel.overflow_view = op_editor_core::GitOverflowView::Menu;
|
||||
panel.close_tracked_picker();
|
||||
panel.diff = None;
|
||||
|
|
|
|||
|
|
@ -1208,6 +1208,7 @@ impl WidgetHostNative {
|
|||
panel.branch_create_input.set_text("");
|
||||
panel.branch_create_focused = false;
|
||||
panel.branch_picker_open = false;
|
||||
panel.branch_picker_menu.hover = None;
|
||||
}
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
|
|
@ -1376,6 +1377,7 @@ impl WidgetHostNative {
|
|||
{
|
||||
let panel = &mut self.editor_state.editor_ui.git_panel;
|
||||
panel.branch_picker_mode = op_editor_core::GitBranchPickerMode::List;
|
||||
panel.branch_picker_menu.hover = None;
|
||||
panel.branch_create_input.set_text("");
|
||||
panel.branch_create_focused = false;
|
||||
self.mark_dirty();
|
||||
|
|
@ -1439,7 +1441,7 @@ impl WidgetHostNative {
|
|||
}
|
||||
if self.editor_state.editor_ui.file_menu_open {
|
||||
self.editor_state.editor_ui.file_menu_open = false;
|
||||
self.editor_state.editor_ui.file_menu_hover = None;
|
||||
self.editor_state.editor_ui.file_menu.hover = None;
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,34 @@ impl WidgetHostNative {
|
|||
}
|
||||
})
|
||||
});
|
||||
let branch_picker_open = self.editor_state.editor_ui.git_panel.branch_picker_open;
|
||||
let branch_picker_hover = panel_body.and_then(|body| {
|
||||
GitPanel::for_editor(&self.editor_state).and_then(|p| {
|
||||
if !branch_picker_open {
|
||||
return None;
|
||||
}
|
||||
match p.branch_picker_menu_hit(body, point) {
|
||||
op_editor_ui::widgets::git_panel::MenuHit::Row(idx) => Some(idx),
|
||||
op_editor_ui::widgets::git_panel::MenuHit::Inside
|
||||
| op_editor_ui::widgets::git_panel::MenuHit::Outside => None,
|
||||
}
|
||||
})
|
||||
});
|
||||
let overflow_menu_open = self.editor_state.editor_ui.git_panel.overflow_open
|
||||
&& self.editor_state.editor_ui.git_panel.overflow_view
|
||||
== op_editor_core::GitOverflowView::Menu;
|
||||
let overflow_menu_hover = panel_body.and_then(|body| {
|
||||
GitPanel::for_editor(&self.editor_state).and_then(|p| {
|
||||
if !overflow_menu_open {
|
||||
return None;
|
||||
}
|
||||
match p.overflow_menu_hit(body, point) {
|
||||
op_editor_ui::widgets::git_panel::MenuHit::Row(idx) => Some(idx),
|
||||
op_editor_ui::widgets::git_panel::MenuHit::Inside
|
||||
| op_editor_ui::widgets::git_panel::MenuHit::Outside => None,
|
||||
}
|
||||
})
|
||||
});
|
||||
// The `⎇ <branch> ▾` trigger keeps its own bool wash; the plain
|
||||
// action buttons (pull / push / overflow / commit / milestone /
|
||||
// refresh) light up via `button_hover`.
|
||||
|
|
@ -130,6 +158,25 @@ impl WidgetHostNative {
|
|||
self.editor_state.editor_ui.git_panel.tracked_picker.hover = tracked_picker_hover;
|
||||
changed = true;
|
||||
}
|
||||
if branch_picker_hover
|
||||
!= self
|
||||
.editor_state
|
||||
.editor_ui
|
||||
.git_panel
|
||||
.branch_picker_menu
|
||||
.hover
|
||||
{
|
||||
self.editor_state
|
||||
.editor_ui
|
||||
.git_panel
|
||||
.branch_picker_menu
|
||||
.hover = branch_picker_hover;
|
||||
changed = true;
|
||||
}
|
||||
if overflow_menu_hover != self.editor_state.editor_ui.git_panel.overflow_menu.hover {
|
||||
self.editor_state.editor_ui.git_panel.overflow_menu.hover = overflow_menu_hover;
|
||||
changed = true;
|
||||
}
|
||||
if changed {
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -453,6 +453,7 @@ impl WidgetHostNative {
|
|||
}
|
||||
TopBarHit::ToggleFileMenu => {
|
||||
self.editor_state.editor_ui.file_menu_open ^= true;
|
||||
self.editor_state.editor_ui.file_menu.hover = None;
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -587,7 +587,7 @@ impl WidgetHostNative {
|
|||
viewport_width: f32,
|
||||
) {
|
||||
use op_editor_core::editor_ui_state::FileAction;
|
||||
use op_editor_ui::widgets::file_menu::{FileMenu, FileMenuChoice};
|
||||
use op_editor_ui::widgets::file_menu::{FileMenu, FileMenuChoice, MenuHit};
|
||||
use op_editor_ui::widgets::top_bar::TopBar;
|
||||
self.refresh_layout_scene();
|
||||
let top_bar_rect = op_editor_ui::Rect {
|
||||
|
|
@ -602,23 +602,34 @@ impl WidgetHostNative {
|
|||
.unwrap_or(0);
|
||||
let menu = FileMenu::from_editor_ui(&self.editor_state.editor_ui, now_secs);
|
||||
let menu_rect = menu.rect_at(anchor);
|
||||
if let Some(choice) = menu.hit_test(menu_rect, op_editor_ui::Point2D::new(x, y)) {
|
||||
self.editor_state.editor_ui.pending_file_action = Some(match choice {
|
||||
FileMenuChoice::NewFile => FileAction::New,
|
||||
FileMenuChoice::OpenFile => FileAction::Open,
|
||||
FileMenuChoice::Save => FileAction::Save,
|
||||
FileMenuChoice::SaveAs => FileAction::SaveAs,
|
||||
FileMenuChoice::ExportImage => FileAction::ExportImage,
|
||||
FileMenuChoice::OpenRecent(i) => FileAction::OpenRecent(i),
|
||||
FileMenuChoice::ClearRecent => FileAction::ClearRecent,
|
||||
});
|
||||
} else {
|
||||
// Miss — the dismissing click is a blank press.
|
||||
self.blur_text_inputs_on_blank_press();
|
||||
let point = op_editor_ui::Point2D::new(x, y);
|
||||
match menu.hit(menu_rect, point) {
|
||||
MenuHit::Row(row) => {
|
||||
let Some(choice) = menu.choice_for_row(row) else {
|
||||
return;
|
||||
};
|
||||
self.editor_state.editor_ui.pending_file_action = Some(match choice {
|
||||
FileMenuChoice::NewFile => FileAction::New,
|
||||
FileMenuChoice::OpenFile => FileAction::Open,
|
||||
FileMenuChoice::Save => FileAction::Save,
|
||||
FileMenuChoice::SaveAs => FileAction::SaveAs,
|
||||
FileMenuChoice::ExportImage => FileAction::ExportImage,
|
||||
FileMenuChoice::OpenRecent(i) => FileAction::OpenRecent(i),
|
||||
FileMenuChoice::ClearRecent => FileAction::ClearRecent,
|
||||
});
|
||||
self.editor_state.editor_ui.file_menu_open = false;
|
||||
self.editor_state.editor_ui.file_menu.hover = None;
|
||||
self.mark_dirty();
|
||||
}
|
||||
MenuHit::Inside => {}
|
||||
MenuHit::Outside => {
|
||||
// Miss — the dismissing click is a blank press.
|
||||
self.blur_text_inputs_on_blank_press();
|
||||
self.editor_state.editor_ui.file_menu_open = false;
|
||||
self.editor_state.editor_ui.file_menu.hover = None;
|
||||
self.mark_dirty();
|
||||
}
|
||||
}
|
||||
self.editor_state.editor_ui.file_menu_open = false;
|
||||
self.editor_state.editor_ui.file_menu_hover = None;
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
/// Commit a pending effect-parameter edit (Effects section's
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ impl WidgetHost {
|
|||
viewport_width: f32,
|
||||
) {
|
||||
use op_editor_core::editor_ui_state::FileAction;
|
||||
use op_editor_ui::widgets::file_menu::{FileMenu, FileMenuChoice};
|
||||
use op_editor_ui::widgets::file_menu::{FileMenu, FileMenuChoice, MenuHit};
|
||||
use op_editor_ui::widgets::top_bar::TopBar;
|
||||
self.refresh_layout_scene();
|
||||
let top_bar_rect = op_editor_ui::Rect {
|
||||
|
|
@ -31,23 +31,34 @@ impl WidgetHost {
|
|||
// clock paints the same menu rows.
|
||||
let menu = FileMenu::from_editor_ui(&self.editor_state.editor_ui, 0);
|
||||
let menu_rect = menu.rect_at(anchor);
|
||||
if let Some(choice) = menu.hit_test(menu_rect, op_editor_ui::Point2D::new(x, y)) {
|
||||
self.editor_state.editor_ui.pending_file_action = Some(match choice {
|
||||
FileMenuChoice::NewFile => FileAction::New,
|
||||
FileMenuChoice::OpenFile => FileAction::Open,
|
||||
FileMenuChoice::Save => FileAction::Save,
|
||||
FileMenuChoice::SaveAs => FileAction::SaveAs,
|
||||
FileMenuChoice::ExportImage => FileAction::ExportImage,
|
||||
FileMenuChoice::OpenRecent(i) => FileAction::OpenRecent(i),
|
||||
FileMenuChoice::ClearRecent => FileAction::ClearRecent,
|
||||
});
|
||||
} else {
|
||||
// Miss — the dismissing click is a blank press.
|
||||
self.blur_text_inputs_on_blank_press();
|
||||
let point = op_editor_ui::Point2D::new(x, y);
|
||||
match menu.hit(menu_rect, point) {
|
||||
MenuHit::Row(row) => {
|
||||
let Some(choice) = menu.choice_for_row(row) else {
|
||||
return;
|
||||
};
|
||||
self.editor_state.editor_ui.pending_file_action = Some(match choice {
|
||||
FileMenuChoice::NewFile => FileAction::New,
|
||||
FileMenuChoice::OpenFile => FileAction::Open,
|
||||
FileMenuChoice::Save => FileAction::Save,
|
||||
FileMenuChoice::SaveAs => FileAction::SaveAs,
|
||||
FileMenuChoice::ExportImage => FileAction::ExportImage,
|
||||
FileMenuChoice::OpenRecent(i) => FileAction::OpenRecent(i),
|
||||
FileMenuChoice::ClearRecent => FileAction::ClearRecent,
|
||||
});
|
||||
self.editor_state.editor_ui.file_menu_open = false;
|
||||
self.editor_state.editor_ui.file_menu.hover = None;
|
||||
self.mark_dirty();
|
||||
}
|
||||
MenuHit::Inside => {}
|
||||
MenuHit::Outside => {
|
||||
// Miss — the dismissing click is a blank press.
|
||||
self.blur_text_inputs_on_blank_press();
|
||||
self.editor_state.editor_ui.file_menu_open = false;
|
||||
self.editor_state.editor_ui.file_menu.hover = None;
|
||||
self.mark_dirty();
|
||||
}
|
||||
}
|
||||
self.editor_state.editor_ui.file_menu_open = false;
|
||||
self.editor_state.editor_ui.file_menu_hover = None;
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
/// Export-dialog press dispatcher.
|
||||
|
|
|
|||
|
|
@ -599,7 +599,7 @@ impl WidgetHost {
|
|||
}
|
||||
if self.editor_state.editor_ui.file_menu_open {
|
||||
self.editor_state.editor_ui.file_menu_open = false;
|
||||
self.editor_state.editor_ui.file_menu_hover = None;
|
||||
self.editor_state.editor_ui.file_menu.hover = None;
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,11 +130,9 @@ impl WidgetHost {
|
|||
// to age (see `dispatch_file_menu_press`).
|
||||
let menu = FileMenu::from_editor_ui(&self.editor_state.editor_ui, 0);
|
||||
let panel = menu.rect_at(anchor);
|
||||
let new_hover = menu
|
||||
.hovered_at(panel, Point2D::new(x, y))
|
||||
.map(op_editor_ui::widgets::editor_state_ext::file_menu_choice);
|
||||
if new_hover != self.editor_state.editor_ui.file_menu_hover {
|
||||
self.editor_state.editor_ui.file_menu_hover = new_hover;
|
||||
let new_hover = menu.hovered_at(panel, Point2D::new(x, y));
|
||||
if new_hover != self.editor_state.editor_ui.file_menu.hover {
|
||||
self.editor_state.editor_ui.file_menu.hover = new_hover;
|
||||
self.mark_dirty();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -323,6 +323,7 @@ impl WidgetHost {
|
|||
}
|
||||
TopBarHit::ToggleFileMenu => {
|
||||
self.editor_state.editor_ui.file_menu_open ^= true;
|
||||
self.editor_state.editor_ui.file_menu.hover = None;
|
||||
}
|
||||
TopBarHit::OpenFigmaImport => {
|
||||
self.editor_state.editor_ui.figma_import_open = true;
|
||||
|
|
|
|||
Loading…
Reference in a new issue