components: app-owned text input and menu; use-import style

oakui/component gains the two app-facing components:

text_input: the app's text field — the gpui_elements editing engine
(IME composition, caret, selection, undo) wrapped with the app theme's
colors (text via a text_color refinement on the wrapping div so the
engine's run layout picks it up; selection/caret/placeholder/IME-marked
directly) — and install_text_input_bindings(), which binds the
Backspace/Delete/arrow/Home/End/select-all editing keys into the app
keymap scoped to the EditableText context. The app never installed
them before, so every field accepted IME text but ignored its editing
keys; the bindings are now wired once at OakApp::new. All seven call
sites (ofx_params x2, effect_library, manager, dialogs x3) use the
component instead of gpui_elements directly.

menu: all app menu code consolidates here — the model types are
re-exported, the shared context-menu plumbing (ContextMenuHandle,
ContextMenuTriggered) and the shared segments (edit/clip-edit/in-out/
color-label/new, the viewer context menu, the dynamic language menu)
move in from src/menus, which is deleted; app.rs and every panel
import from the component. Inline fully-qualified crate paths in
non-use positions are replaced with use imports.
This commit is contained in:
2026-08-22 16:44:52 +08:00
parent e7483ba735
commit 7e2b3cb4b8
20 changed files with 331 additions and 184 deletions
+2 -1
View File
@@ -826,7 +826,8 @@ mod tests {
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
crate::i18n::set_language_code("en-US");
fn collect(menu: &gpui_widgets::menu::Menu, out: &mut Vec<usize>) {
use crate::oakui::component::menu;
fn collect(menu: &menu::Menu, out: &mut Vec<usize>) {
for item in &menu.items {
out.push(item.id);
if let Some(sub) = &item.submenu {
+17 -12
View File
@@ -53,7 +53,8 @@ use gpui::{
use gpui_widgets::audio_meter::{AudioLevelMeter, MeterOrientation};
use gpui_widgets::dialog::progress::{progress_dialog, ProgressContent};
use gpui_widgets::dialog::{DialogButton, Modal, ModalEvent, ModalOptions};
use gpui_widgets::menu::{Menu, MenuBar, MenuBarEntry, MenuBarEvent, MenuItem};
use crate::oakui::component::menu::{self, Menu, MenuBar, MenuBarEntry, MenuBarEvent, MenuItem};
use crate::oakui::component::text_input::install_text_input_bindings;
use gpui_widgets::theme::{apply_theme, OakTheme};
use gpui_widgets::viewer::PlaybackClock;
@@ -417,6 +418,10 @@ impl<E: AppEngine> OakApp<E> {
// the shell's `on_action` listeners — the same path the menu clicks
// take through `on_menu`.
cx.bind_keys(crate::actions::key_bindings());
// The text-input editing keys (Backspace/Delete/arrows/Home/End/
// select-all …) are scoped to the `EditableText` key context; without
// them every field accepts IME text but ignores its editing keys.
install_text_input_bindings(cx);
// --- dock ----------------------------------------------------------
let dock = cx.new(|cx| {
@@ -763,7 +768,7 @@ impl<E: AppEngine> OakApp<E> {
/// through the same path the keyboard shortcuts use, so a menu click
/// and a key press can never diverge.
fn on_menu(&mut self, item: usize, cx: &mut Context<Self>) {
if let Some(index) = crate::menus::shared::language_item_index(item) {
if let Some(index) = menu::language_item_index(item) {
let languages = crate::i18n::available_languages();
if let Some(code) = languages.get(index) {
let code = code.clone();
@@ -784,11 +789,11 @@ impl<E: AppEngine> OakApp<E> {
/// `PanelEvent::Focused`).
fn wire_panel_context_menu<P>(cx: &mut Context<Self>, panel: &Entity<P>, id: PanelId)
where
P: gpui::EventEmitter<crate::menus::context::ContextMenuTriggered>,
P: gpui::EventEmitter<menu::ContextMenuTriggered>,
{
cx.subscribe(
panel,
move |this, _panel, event: &crate::menus::context::ContextMenuTriggered, cx| {
move |this, _panel, event: &menu::ContextMenuTriggered, cx| {
this.focused_panel = Some(id);
this.on_menu(event.item, cx);
},
@@ -2346,10 +2351,10 @@ impl MenuState {
}
/// One menu item straight from the registry — delegates to
/// [`menus::shared::action_item`](crate::menus::shared::action_item) so the
/// [`menu::action_item`](menu::action_item) so the
/// menu bar and the context menus build items the same way.
fn menu_item(action: ActionId) -> MenuItem {
crate::menus::shared::action_item(action)
menu::action_item(action)
}
/// Builds the menu bar entries (文件/编辑/视图/回放/序列/窗口/工具/帮助) from
@@ -2478,7 +2483,7 @@ fn make_menus(state: MenuState) -> Vec<MenuBarEntry> {
tr("menu.view"),
Menu::new(vec![
menu_item(A::ThemeDark).with_submenu(theme_submenu),
crate::menus::shared::language_menu(),
menu::language_menu(),
menu_item(A::ZoomIn).separated(),
menu_item(A::ZoomOut),
menu_item(A::IncreaseTrackHeight),
@@ -2786,12 +2791,12 @@ mod tests {
let zh = submenu
.items
.iter()
.find(|i| i.id == crate::menus::shared::LANG_ITEM_BASE + zh_index)
.find(|i| i.id == menu::LANG_ITEM_BASE + zh_index)
.expect("zh item");
let en = submenu
.items
.iter()
.find(|i| i.id == crate::menus::shared::LANG_ITEM_BASE + en_index)
.find(|i| i.id == menu::LANG_ITEM_BASE + en_index)
.expect("en item");
assert_eq!(zh.label, "简体中文 (zh-CN)");
assert_eq!(en.label, "English (en-US)");
@@ -2803,12 +2808,12 @@ mod tests {
let zh = submenu
.items
.iter()
.find(|i| i.id == crate::menus::shared::LANG_ITEM_BASE + zh_index)
.find(|i| i.id == menu::LANG_ITEM_BASE + zh_index)
.expect("zh item");
let en = submenu
.items
.iter()
.find(|i| i.id == crate::menus::shared::LANG_ITEM_BASE + en_index)
.find(|i| i.id == menu::LANG_ITEM_BASE + en_index)
.expect("en item");
assert_eq!(zh.checked, Some(true), "zh-CN is active → checked");
assert_eq!(en.checked, Some(false));
@@ -2826,7 +2831,7 @@ mod tests {
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
let dark_item = |dark: bool| -> gpui_widgets::menu::MenuItem {
let dark_item = |dark: bool| -> menu::MenuItem {
let entries = make_menus(MenuState::new(dark));
let view = entries
.iter()
+5 -4
View File
@@ -31,12 +31,13 @@ use gpui::{
div, px, App, Context, ElementId, Entity, EventEmitter, Focusable, FocusHandle, Keystroke,
PathPromptOptions, Render, SharedString, Window,
};
use gpui_elements::editable_text::{text_input, EditableTextState, StringStorage, TextChanged};
use gpui_elements::editable_text::{EditableTextState, StringStorage, TextChanged};
use gpui_widgets::checkbox::{CheckBox, CheckBoxEvent, CheckState};
use gpui_widgets::combo_box::{ComboBox, ComboBoxEvent, ComboBoxOption};
use gpui_widgets::slider::SliderModel;
use gpui_widgets::spinbox::{SpinBox, SpinBoxEvent};
use gpui_widgets::value::ValueKind;
use crate::oakui::component::text_input;
use crate::actions::ActionId;
use crate::i18n;
@@ -748,7 +749,7 @@ impl Render for PathField {
.px_2()
.py_1()
.child(
text_input("gpui-widgets-export-path")
text_input("gpui-widgets-export-path", cx)
.state(weak)
.accepts_input(true),
)
@@ -1720,7 +1721,7 @@ impl Render for KeyboardTabContent {
.bg(colors.background)
.px_2()
.py_1()
.child(text_input("keyboard-search-input").state(weak).accepts_input(true));
.child(text_input("keyboard-search-input", cx).state(weak).accepts_input(true));
// The grouped, filtered action list.
let mut list = div()
@@ -2221,7 +2222,7 @@ impl Render for ActionSearchContent {
.bg(colors.background)
.px_2()
.py_1()
.child(text_input("action-search-input").state(weak).accepts_input(true)),
.child(text_input("action-search-input", cx).state(weak).accepts_input(true)),
)
.child(list)
}
-1
View File
@@ -61,7 +61,6 @@ pub mod app;
pub mod dialogs;
pub mod i18n;
pub mod manager;
pub mod menus;
pub mod oakui;
pub mod panels;
+3 -2
View File
@@ -36,7 +36,8 @@ use gpui::{
div, App, ClickEvent, Context, ElementId, Entity, EventEmitter, Hsla, Render, SharedString,
Window,
};
use gpui_elements::editable_text::{text_input, EditableTextState, StringStorage};
use gpui_elements::editable_text::{EditableTextState, StringStorage};
use crate::oakui::component::text_input;
use crate::i18n;
use crate::oakui::{AppEngine, LibraryProject};
@@ -438,7 +439,7 @@ impl Render for NamePrompt {
.px_2()
.py_1()
.child(
text_input("gpui-widgets-rename-field")
text_input("gpui-widgets-rename-field", cx)
.state(weak)
.accepts_input(true),
),
-81
View File
@@ -1,81 +0,0 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! The context-menu plumbing every panel shares: each panel owns one
//! [`ContextMenuHandle`], which wraps the
//! [`ContextMenu`](gpui_widgets::menu::ContextMenu) popup entity and splits
//! its item activations in two — items whose id belongs to the action
//! registry ([`crate::actions::entry_for_menu_id`]) are re-emitted as
//! [`ContextMenuTriggered`] so the app shell routes them through the same
//! dispatch path the menu bar uses, and everything else (the local ids from
//! [`super::shared::LOCAL_ID_BASE`] up) goes to the panel's own handler.
//! This is the Rust counterpart of the C++ panels wiring shared
//! `MenuShared` actions and widget-local slots into one `QMenu`.
use gpui::{App, AppContext, Context, Entity, EventEmitter, Pixels, Point, Window};
use gpui_widgets::menu::{ContextMenu, ContextMenuEvent, Menu};
/// A registry-backed context-menu item was triggered: the panel re-emits it
/// so the app shell dispatches it like a menu-bar click (after pointing
/// `focused_panel` at the panel, since a right-click does not emit
/// `PanelEvent::Focused`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContextMenuTriggered {
/// The triggered item's id (an action registry menu id).
pub item: usize,
}
/// A panel's context menu: owns the popup entity, re-emits registry items as
/// [`ContextMenuTriggered`] and hands local items to the panel.
pub struct ContextMenuHandle {
menu: Entity<ContextMenu>,
}
impl ContextMenuHandle {
/// Create the popup entity and subscribe to it. `on_local_item` handles
/// every triggered item that is not in the action registry (color
/// labels, panel-specific placeholders, …).
pub fn new<P, F>(on_local_item: F, window: &mut Window, cx: &mut Context<P>) -> Self
where
P: EventEmitter<ContextMenuTriggered>,
F: Fn(&mut P, usize, &mut Context<P>) + 'static,
{
let menu = cx.new(|cx| ContextMenu::new(0, window, cx));
cx.subscribe(
&menu,
move |panel: &mut P, _menu, event: &ContextMenuEvent, cx| {
if crate::actions::entry_for_menu_id(event.item).is_some() {
cx.emit(ContextMenuTriggered { item: event.item });
} else {
on_local_item(panel, event.item, cx);
}
},
)
.detach();
Self { menu }
}
/// Open the menu at `position` (window coordinates).
pub fn show(&self, position: Point<Pixels>, menu: Menu, cx: &mut App) {
self.menu.update(cx, |menu_view, cx| menu_view.show(position, menu, cx));
}
/// The popup entity, to be rendered as a child of the panel so the
/// anchored popup can paint above it.
pub fn widget(&self) -> Entity<ContextMenu> {
self.menu.clone()
}
}
-33
View File
@@ -1,33 +0,0 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! The right-click menu layer: the Rust counterpart of the C++ context-menu
//! system (`MenuShared` segments + the per-widget `show_context_menu`
//! methods, `app/widget/menu/menushared.cpp` and friends).
//!
//! * [`context`] — the shared plumbing every panel needs to own a
//! [`ContextMenu`](gpui_widgets::menu::ContextMenu): entity creation,
//! event subscription and show/hide, plus the
//! [`ContextMenuTriggered`](context::ContextMenuTriggered) event the
//! panels emit so the shell routes registry items through the same
//! dispatch path the menu bar uses.
//! * [`shared`] — the shared menu segments (edit / clip-edit / in-out /
//! color label / new), built from the action registry
//! ([`crate::actions`]) so ids, labels and shortcut annotations can never
//! diverge from the menu bar.
pub mod context;
pub mod shared;
@@ -14,19 +14,86 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! The shared context-menu segments: the Rust counterpart of the C++
//! `MenuShared` item groups (`add_items_for_edit_menu`,
//! `add_items_for_clip_edit_menu`, `add_items_for_in_out_menu`,
//! `add_items_for_new_menu` and the `ColorLabelMenu`,
//! `app/widget/menu/menushared.cpp` + `app/widget/colorlabelmenu/`).
//! The app's menu component: the menu bar and the context menus.
//!
//! Registry-backed items are built from [`crate::actions`] exactly like the
//! menu bar, so ids, labels and shortcut annotations can never diverge.
//! Local (non-registry) items — the 16 color labels today — live at
//! [`LOCAL_ID_BASE`] and above; [`crate::actions::entry_for_menu_id`] is
//! what splits the two worlds at dispatch time.
//! All app menu code lives here instead of reaching into gpui_widgets
//! directly: the rendering-engine types are re-exported
//! ([`Menu`], [`MenuItem`], [`MenuBar`], …), the shared context-menu
//! plumbing ([`ContextMenuHandle`], [`ContextMenuTriggered`]) and the
//! shared menu segments (edit / clip-edit / in-out / color label / new,
//! plus the viewer context menu) are built from the action registry
//! ([`crate::actions`]) exactly like the menu bar, so ids, labels and
//! shortcut annotations can never diverge.
//!
//! Local (non-registry) items — the color labels and the dynamic
//! language items — live at [`LOCAL_ID_BASE`] and above;
//! [`crate::actions::entry_for_menu_id`] splits the two worlds at
//! dispatch time.
use gpui_widgets::menu::{Menu, MenuItem};
pub use gpui_widgets::menu::{
ContextMenu, ContextMenuEvent, Menu, MenuBar, MenuBarEntry, MenuBarEvent, MenuItem,
};
// ---------------------------------------------------------------------------
// Context-menu plumbing
// ---------------------------------------------------------------------------
use gpui::{App, AppContext, Context, Entity, EventEmitter, Pixels, Point, Window};
/// A registry-backed context-menu item was triggered: the panel re-emits it
/// so the app shell dispatches it like a menu-bar click (after pointing
/// `focused_panel` at the panel, since a right-click does not emit
/// `PanelEvent::Focused`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContextMenuTriggered {
/// The triggered item's id (an action registry menu id).
pub item: usize,
}
/// A panel's context menu: owns the popup entity, re-emits registry items as
/// [`ContextMenuTriggered`] and hands local items to the panel.
pub struct ContextMenuHandle {
menu: Entity<ContextMenu>,
}
impl ContextMenuHandle {
/// Create the popup entity and subscribe to it. `on_local_item` handles
/// every triggered item that is not in the action registry (color
/// labels, panel-specific placeholders, …).
pub fn new<P, F>(on_local_item: F, window: &mut Window, cx: &mut Context<P>) -> Self
where
P: EventEmitter<ContextMenuTriggered>,
F: Fn(&mut P, usize, &mut Context<P>) + 'static,
{
let menu = cx.new(|cx| ContextMenu::new(0, window, cx));
cx.subscribe(
&menu,
move |panel: &mut P, _menu, event: &ContextMenuEvent, cx| {
if crate::actions::entry_for_menu_id(event.item).is_some() {
cx.emit(ContextMenuTriggered { item: event.item });
} else {
on_local_item(panel, event.item, cx);
}
},
)
.detach();
Self { menu }
}
/// Open the menu at `position` (window coordinates).
pub fn show(&self, position: Point<Pixels>, menu: Menu, cx: &mut App) {
self.menu.update(cx, |menu_view, cx| menu_view.show(position, menu, cx));
}
/// The popup entity, to be rendered as a child of the panel so the
/// anchored popup can paint above it.
pub fn widget(&self) -> Entity<ContextMenu> {
self.menu.clone()
}
}
// ---------------------------------------------------------------------------
// Shared menu segments
// ---------------------------------------------------------------------------
use crate::actions::ActionId;
+41
View File
@@ -0,0 +1,41 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! The app's reusable UI components — the presentation layer above raw
//! gpui. Components own their theme wiring (colors come from the app
//! theme, not hard-coded defaults), their key bindings and their
//! interaction rules, so call sites stay declarative:
//!
//! ```
//! use crate::oakui::component::{text_input, TextInput};
//!
//! // in a render():
//! child(
//! text_input("my-field", window, cx)
//! .state(self.value.downgrade())
//! .accepts_input(true),
//! )
//! ```
//!
//! Submodules:
//! - [`text_input`]: the text field (IME, caret/selection, editing keys).
//! - [`menu`]: menus (menu bar entries and context menus).
pub mod menu;
pub mod text_input;
pub use menu::{ContextMenu, Menu, MenuItem, MenuBar};
pub use text_input::{install_text_input_bindings, text_input, TextInput};
+141
View File
@@ -0,0 +1,141 @@
// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! The app's text input component.
//!
//! Wraps the gpui_elements editing engine (IME composition, caret,
//! selection, undo history) with the app's own presentation rules:
//!
//! - **Theme colors**: text, caret, selection, placeholder and the IME
//! marked underline all come from `App::default_colors` (the app theme)
//! instead of the engine's hard-coded defaults. The field's text color
//! is applied through a wrapping `div().text_color(…)` refinement so
//! the engine's run layout (which reads `Window::text_style`) picks it
//! up.
//! - **Editing key bindings**: [`install_text_input_bindings`] installs
//! the engine's Backspace/Delete/arrow/Home/End/select-all bindings
//! into the app keymap, scoped to the `EditableText` key context. The
//! app previously never installed them, so every field accepted IME
//! text but every editing key was a no-op.
//!
//! IME text insertion itself flows through the engine's
//! `EntityInputHandler` (registered by the element via
//! `Window::handle_input`), so composing text, marked ranges and the
//! candidate-window position all work once the field is focused.
use std::sync::Arc;
use gpui::{
colors::{Colors, DefaultColors},
div, App, Div, ElementId, Hsla, IntoElement, ParentElement, Styled, WeakEntity, Window,
};
use gpui_elements::editable_text::actions::{default_bindings, DEFAULT_INPUT_CONTEXT};
use gpui_elements::editable_text::{EditableTextElement, EditableTextState};
/// Install the text-input editing key bindings into the app keymap.
///
/// The editing keys (Backspace, Delete, arrows, Home/End, select-all,
/// …) are gpui actions bound to the `EditableText` key context; without
/// them the fields accept IME text but every editing key is a no-op.
/// Call once at app startup, alongside the registry key bindings.
pub fn install_text_input_bindings(cx: &mut App) {
cx.bind_keys(default_bindings().as_keybindings(Some(DEFAULT_INPUT_CONTEXT)));
}
/// The app's text input component.
///
/// A `div` that carries the theme text color refinement around the
/// gpui_elements editing element, with the theme's selection / caret /
/// placeholder / IME-marked colors applied directly. Build with
/// [`text_input`], then chain the engine's element options (`.state`,
/// `.accepts_input`, …).
pub struct TextInput {
element: EditableTextElement,
text_color: Hsla,
selection: Hsla,
caret: Hsla,
placeholder: Hsla,
}
impl TextInput {
/// Bind the field to an existing editing state entity (the caller
/// reads the value / subscribes to changes through it).
pub fn state(mut self, state: WeakEntity<EditableTextState>) -> Self {
self.element = self.element.state(state);
self
}
/// Whether the field accepts input at all (disabled fields render
/// without the input handler attached).
pub fn accepts_input(mut self, enabled: bool) -> Self {
self.element = self.element.accepts_input(enabled);
self
}
/// Placeholder shown while the field is empty.
pub fn placeholder(mut self, text: impl Into<gpui::SharedString>) -> Self {
self.element = self.element.placeholder(text);
self
}
}
impl IntoElement for TextInput {
type Element = Div;
fn into_element(self) -> Div {
let Self {
element,
text_color,
selection,
caret,
placeholder,
} = self;
div()
// The text_color refinement is pushed onto the window text
// style during paint, so the engine's run layout (which reads
// `Window::text_style().color`) renders in the theme text
// color — the field reads like every other label in the app.
.text_color(text_color)
.child(
element
.placeholder_color(placeholder)
.selection_color(selection)
.caret_color(caret)
.marked_color(text_color),
)
}
}
/// Build the app's text input with the current theme's colors.
///
/// `cx` is used for the theme colors; the returned component needs no
/// further styling to match the rest of the UI.
pub fn text_input(id: impl Into<ElementId>, cx: &App) -> TextInput {
let colors = theme_colors(cx);
TextInput {
element: gpui_elements::editable_text::text_input(id),
text_color: colors.text.into(),
selection: colors.selected.into(),
caret: colors.text.into(),
placeholder: colors.disabled.into(),
}
}
/// The theme colors for the app (falls back to the defaults when no
/// theme has been applied yet).
fn theme_colors(cx: &App) -> Arc<Colors> {
cx.default_colors().clone()
}
+1
View File
@@ -40,6 +40,7 @@
//! * [`timecode`] — timecode / duration / fps / resolution formatting (pure,
//! unit tested).
pub mod component;
pub mod displaycolor;
pub mod effectchain;
pub mod engine;
+3 -2
View File
@@ -26,7 +26,8 @@ use gpui::{
div, prelude::*, AnyElement, App, ClickEvent, Context, Entity, EventEmitter, MouseButton,
Render, SharedString, Window,
};
use gpui_elements::editable_text::{text_input, EditableTextState, StringStorage, TextChanged};
use gpui_elements::editable_text::{EditableTextState, StringStorage, TextChanged};
use crate::oakui::component::text_input;
use crate::i18n;
use crate::oakui::AppEngine;
@@ -162,7 +163,7 @@ impl<E: AppEngine> Render for EffectLibraryPanel<E> {
.border_b_1()
.border_color(colors.border)
.child(
text_input("effect-library-search")
text_input("effect-library-search", cx)
.state(self.search.downgrade())
.accepts_input(true),
),
+1 -1
View File
@@ -27,7 +27,7 @@ use gpui::{
div, prelude::*, px, AnyElement, App, Context, ElementId, Entity, EventEmitter, MouseButton,
MouseDownEvent, Render, SharedString, Window,
};
use gpui_widgets::menu::{ContextMenu, ContextMenuEvent, Menu, MenuItem};
use crate::oakui::component::menu::{ContextMenu, ContextMenuEvent, Menu, MenuItem};
use crate::oakui::AppEngine;
use crate::panels::commands::PanelCommandHandler;
+2 -2
View File
@@ -26,9 +26,9 @@ use gpui::{
div, prelude::*, AnyElement, App, Context, Entity, EventEmitter, MouseButton, Render,
SharedString, Window,
};
use gpui_widgets::menu::{Menu, MenuItem};
use crate::oakui::component::menu::{Menu, MenuItem};
use crate::menus::context::{ContextMenuHandle, ContextMenuTriggered};
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
use crate::oakui::AppEngine;
use crate::panels::commands::PanelCommandHandler;
use crate::panels::ids::INSPECTOR;
+6 -6
View File
@@ -37,10 +37,10 @@ use gpui::{
div, point, prelude::*, px, AnyElement, App, Bounds, ClickEvent, Context, Entity,
EventEmitter, MouseButton, Pixels, Point, Render, SharedString, Window,
};
use gpui_widgets::menu::{Menu, MenuItem};
use crate::oakui::component::menu::{Menu, MenuItem};
use crate::menus::context::{ContextMenuHandle, ContextMenuTriggered};
use crate::menus::shared;
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
use crate::oakui::component::menu;
use crate::oakui::{AppEngine, NodeLibraryEntry};
use crate::panels::commands::PanelCommandHandler;
use crate::panels::ids::NODE_EDITOR;
@@ -156,7 +156,7 @@ impl<E: AppEngine> NodeEditorPanel<E> {
/// Handles the node editor's local (non-registry) context-menu items.
fn on_local_menu_item(&mut self, item: usize, cx: &mut Context<Self>) {
if let Some(color) = shared::color_label_index(item) {
if let Some(color) = menu::color_label_index(item) {
println!("[node editor] set node color label to {color}");
return;
}
@@ -442,13 +442,13 @@ const LOCAL_ADD_NODE_BASE: usize = 2420;
/// viewer/parameter-editor reveals and properties (the C++ node branch).
pub(crate) fn node_menu() -> Menu {
use crate::i18n::tr;
let mut items = shared::edit_section(false);
let mut items = menu::edit_section(false);
if let Some(last) = items.last_mut() {
last.separator_after = true;
}
items.push(MenuItem::new(LOCAL_GROUP, tr("node.context.group")));
items.push(MenuItem::new(LOCAL_UNGROUP, tr("node.context.ungroup")));
items.push(shared::color_label_item(None).separated());
items.push(menu::color_label_item(None).separated());
items.push(MenuItem::new(LOCAL_OPEN_IN_VIEWER, tr("node.context.open_in_viewer")));
items.push(MenuItem::new(
LOCAL_SHOW_IN_PARAM_EDITOR,
+4 -3
View File
@@ -42,6 +42,7 @@
//! the widget values from the engine snapshot on every render.
use std::sync::Arc;
use crate::oakui::component::text_input;
use gpui::effect_stack::EffectId;
use gpui::colors::DefaultColors;
@@ -53,7 +54,7 @@ use gpui::{
Anchor, App, Bounds, ElementId, Hsla, KeyDownEvent, MouseButton, MouseDownEvent, MouseUpEvent,
Point, Pixels, Rgba, anchored, canvas, deferred, fill,
};
use gpui_elements::editable_text::{text_input, EditableTextState, StringStorage};
use gpui_elements::editable_text::{EditableTextState, StringStorage};
use gpui_widgets::checkbox::{CheckBox, CheckBoxEvent, CheckState};
use gpui_widgets::combo_box::{ComboBox, ComboBoxEvent, ComboBoxOption};
use gpui_widgets::slider::{Slider, SliderEvent, SliderModel};
@@ -912,7 +913,7 @@ impl<E: AppEngine> Render for OfxParamsView<E> {
.bg(colors.background)
.px_2()
.py_1()
.child(text_input(format!("ofx-param-{}", control.input_id)).state(weak).accepts_input(true)),
.child(text_input(format!("ofx-param-{}", control.input_id), cx).state(weak).accepts_input(true)),
)
.child(
// Explicit commit: reads the field and pushes the
@@ -1267,7 +1268,7 @@ impl OfxColorPicker {
.px_2()
.py_1()
.child(
text_input(format!("ofx-color-hex-{control}"))
text_input(format!("ofx-color-hex-{control}"), cx)
.state(hex_weak)
.accepts_input(true),
),
+4 -3
View File
@@ -31,7 +31,8 @@ use gpui_widgets::scopes::{ChromaDataSource, Histogram, LumaDataSource, Vectorsc
use gpui_widgets::viewer::{InteractPointerKind, PlaybackClock, ViewerEvent, ViewerWidget};
use crate::actions::ActionId;
use crate::menus::context::{ContextMenuHandle, ContextMenuTriggered};
use crate::oakui::component::menu;
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
use crate::oakui::ofx::InteractViewport;
use crate::oakui::timecode::{format_fps, format_resolution};
use crate::oakui::{AppEngine, Monitor};
@@ -206,7 +207,7 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
/// Handles the viewer's local (non-registry) context-menu items.
fn on_local_menu_item(&mut self, item: usize, cx: &mut Context<Self>) {
use crate::menus::shared as shared_menu;
use crate::oakui::component::menu as shared_menu;
let divider = match item {
shared_menu::LOCAL_VIEWER_RES_FULL => Some(1),
shared_menu::LOCAL_VIEWER_RES_HALF => Some(2),
@@ -550,7 +551,7 @@ impl<E: AppEngine> Render for ProgramViewerPanel<E> {
let divider = this.engine.read(cx).playback_divider();
this.context_menu.show(
event.position,
crate::menus::shared::viewer_menu(divider),
menu::viewer_menu(divider),
cx,
);
})
+6 -6
View File
@@ -25,12 +25,12 @@ use gpui::{
div, px, prelude::*, AnyElement, App, Context, Entity, EventEmitter, MouseButton,
PathPromptOptions, Pixels, Point, Render, SharedString, Window,
};
use gpui_widgets::menu::{Menu, MenuItem};
use crate::oakui::component::menu::{Menu, MenuItem};
use gpui_widgets::project_explorer::{ProjectExplorer, ProjectExplorerEvent};
use crate::actions::ActionId;
use crate::menus::context::{ContextMenuHandle, ContextMenuTriggered};
use crate::menus::shared;
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
use crate::oakui::component::menu;
use crate::oakui::AppEngine;
use crate::panels::commands::PanelCommandHandler;
use crate::panels::ids::PROJECT;
@@ -311,7 +311,7 @@ fn proxy_submenu(row: Option<&crate::oakui::engine::ProxyFootageRow>) -> Menu {
use_proxy,
reveal,
delete,
shared::action_item(ActionId::ProxySettings).separated(),
menu::action_item(ActionId::ProxySettings).separated(),
])
}
@@ -319,8 +319,8 @@ fn proxy_submenu(row: Option<&crate::oakui::engine::ProxyFootageRow>) -> Menu {
pub(crate) fn blank_menu() -> Menu {
Menu::new(vec![
MenuItem::new(0, crate::i18n::tr("project.context.new"))
.with_submenu(Menu::new(shared::new_section())),
shared::action_item(ActionId::Import),
.with_submenu(Menu::new(menu::new_section())),
menu::action_item(ActionId::Import),
])
}
+4 -3
View File
@@ -26,7 +26,8 @@ use gpui::{
use gpui_widgets::viewer::{ViewerEvent, ViewerWidget};
use crate::actions::ActionId;
use crate::menus::context::{ContextMenuHandle, ContextMenuTriggered};
use crate::oakui::component::menu;
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
use crate::oakui::timecode::{format_fps, format_resolution};
use crate::oakui::{AppEngine, Monitor};
use crate::panels::commands::{self as panel_commands, PanelCommandHandler};
@@ -86,7 +87,7 @@ impl<E: AppEngine> SourceViewerPanel<E> {
/// Handles the viewer's local (non-registry) context-menu items.
fn on_local_menu_item(&mut self, item: usize, cx: &mut Context<Self>) {
use crate::menus::shared as shared_menu;
use crate::oakui::component::menu as shared_menu;
let divider = match item {
shared_menu::LOCAL_VIEWER_RES_FULL => Some(1),
shared_menu::LOCAL_VIEWER_RES_HALF => Some(2),
@@ -161,7 +162,7 @@ impl<E: AppEngine> Render for SourceViewerPanel<E> {
let divider = this.engine.read(cx).playback_divider();
this.context_menu.show(
event.position,
crate::menus::shared::viewer_menu(divider),
menu::viewer_menu(divider),
cx,
);
})
+13 -13
View File
@@ -50,7 +50,7 @@ use gpui::{
};
use gpui::{AnyElement, App, ClickEvent, DragMoveEvent, EventEmitter, Render, SharedString};
use gpui_widgets::checkbox::{CheckBox, CheckBoxEvent, CheckState};
use gpui_widgets::menu::{Menu, MenuItem};
use crate::oakui::component::menu::{Menu, MenuItem};
use gpui_widgets::viewer::PlaybackClock;
use gpui_widgets::project_explorer::FootageDrag;
use gpui_widgets::slider::{Slider, SliderEvent, SliderModel};
@@ -59,8 +59,8 @@ use gpui_widgets::value::ValueKind;
use crate::actions::ActionId;
use crate::i18n;
use crate::menus::context::{ContextMenuHandle, ContextMenuTriggered};
use crate::menus::shared;
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
use crate::oakui::component::menu;
use crate::oakui::icons;
use crate::oakui::{AppEngine, Monitor};
use crate::panels::commands::{self as panel_commands, PanelCommandHandler};
@@ -269,7 +269,7 @@ impl<E: AppEngine> TimelinePanel<E> {
// Color labels apply to the selected clips; the engine has no
// clip-color surface yet, so they log for now (kept visible so the
// wiring is testable in the demo).
if let Some(color) = shared::color_label_index(item) {
if let Some(color) = menu::color_label_index(item) {
println!("[timeline] set clip color label to {color}");
return;
}
@@ -1078,29 +1078,29 @@ pub(crate) fn clip_menu(
proxy: &[crate::oakui::engine::ProxyFootageRow],
multicam: Option<MulticamMenuState>,
) -> Menu {
let mut items = shared::edit_section(true);
let mut items = menu::edit_section(true);
// The C++ puts a separator between the edit section and the color
// labels, and another after them.
if let Some(last) = items.last_mut() {
last.separator_after = true;
}
items.push(shared::color_label_item(None).separated());
items.push(menu::color_label_item(None).separated());
// Synchronize group (registry actions; enabled at ≥ 2 eligible clips,
// the C++ `get_selected_source_sync_clips` / `_waveform_sync_clips`
// counts).
let sync_enabled = sync.source_time >= 2;
let wave_enabled = sync.waveform >= 2;
let mut source_time = shared::action_item(ActionId::SyncBySourceTime);
let mut source_time = menu::action_item(ActionId::SyncBySourceTime);
if !sync_enabled {
source_time = source_time.disabled();
}
items.push(source_time);
let mut waveform = shared::action_item(ActionId::SyncByWaveform);
let mut waveform = menu::action_item(ActionId::SyncByWaveform);
if !wave_enabled {
waveform = waveform.disabled();
}
items.push(waveform);
let mut waveform_speed = shared::action_item(ActionId::SyncByWaveformSpeed).separated();
let mut waveform_speed = menu::action_item(ActionId::SyncByWaveformSpeed).separated();
if !wave_enabled {
waveform_speed = waveform_speed.disabled();
}
@@ -1144,7 +1144,7 @@ pub(crate) fn clip_menu(
use_proxy,
reveal,
delete,
shared::action_item(ActionId::ProxySettings).separated(),
menu::action_item(ActionId::ProxySettings).separated(),
]);
items.push(MenuItem::new(0, i18n::tr("timeline.context.proxy")).with_submenu(proxy_menu));
// Reveal / multi-cam entries (the C++ shows them only when the clip is
@@ -1218,8 +1218,8 @@ pub(crate) fn track_head_menu() -> Menu {
/// The marker context menu (`SeekableWidget`): color labels, the plain
/// edit section and marker properties.
pub(crate) fn marker_menu() -> Menu {
let mut items = vec![shared::color_label_item(None).separated()];
let mut edit_items = shared::edit_section(false);
let mut items = vec![menu::color_label_item(None).separated()];
let mut edit_items = menu::edit_section(false);
// Separator before the trailing "Properties" entry (the C++ layout).
if let Some(last) = edit_items.last_mut() {
last.separator_after = true;
@@ -1418,7 +1418,7 @@ mod tests {
.expect("color label item");
assert_eq!(
color.submenu.as_ref().unwrap().items.len(),
shared::COLOR_LABEL_COUNT
menu::COLOR_LABEL_COUNT
);
// The synchronize entries are the registry actions and stay