use std::rc::Rc;
use crate::menu::PopupMenu;
use crate::{App, Context, Entity, Window};
use super::InputState;
use super::{Copy, Cut, Paste, Redo, SelectAll, Undo};
pub type InputContextMenuBuilder =
Rc<dyn Fn(PopupMenu, Entity<InputState>, &mut Window, &mut App) -> PopupMenu>;
impl InputState {
pub fn build_default_context_menu(
menu: PopupMenu,
state: &Entity<InputState>,
cx: &App,
) -> PopupMenu {
let (has_selection, is_empty, can_undo, can_redo, disabled) = {
let snapshot = state.read(cx);
(
snapshot.has_selection(),
snapshot.is_empty(),
snapshot.can_undo(),
snapshot.can_redo(),
snapshot.is_disabled() || snapshot.is_read_only(),
)
};
menu.menu_with_disabled("剪切", Box::new(Cut), !has_selection || disabled)
.menu_with_disabled("复制", Box::new(Copy), !has_selection)
.menu_with_disabled("粘贴", Box::new(Paste), disabled)
.menu_with_disabled("全选", Box::new(SelectAll), is_empty)
.separator()
.menu_with_disabled("撤销", Box::new(Undo), !can_undo || disabled)
.menu_with_disabled("重做", Box::new(Redo), !can_redo || disabled)
}
pub(super) fn build_context_menu(
menu: PopupMenu,
state: &Entity<InputState>,
window: &mut Window,
cx: &mut App,
) -> PopupMenu {
let (override_builder, extra_builder) = {
let snapshot = state.read(cx);
(
snapshot.context_menu_override.clone(),
snapshot.context_menu_extra.clone(),
)
};
let menu = match override_builder {
Some(build) => build(menu, state.clone(), window, cx),
None => Self::build_default_context_menu(menu, state, cx),
};
match extra_builder {
Some(build) => build(menu.separator(), state.clone(), window, cx),
None => menu,
}
}
pub fn has_selection(&self) -> bool {
!self.core.selected_range.is_empty()
}
pub fn is_empty(&self) -> bool {
self.core.text.len() == 0
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn can_undo(&self) -> bool {
!self.core.history.undos().is_empty()
}
pub fn can_redo(&self) -> bool {
!self.core.history.redos().is_empty()
}
pub fn show_context_menu(mut self, show: bool) -> Self {
self.context_menu_enabled = show;
self
}
pub fn set_context_menu_enabled(&mut self, show: bool, cx: &mut Context<Self>) {
self.context_menu_enabled = show;
cx.notify();
}
pub fn is_context_menu_enabled(&self) -> bool {
self.context_menu_enabled
}
pub fn context_menu_extra<F>(mut self, builder: F) -> Self
where
F: Fn(PopupMenu, Entity<InputState>, &mut Window, &mut App) -> PopupMenu + 'static,
{
self.context_menu_extra = Some(Rc::new(builder));
self
}
pub fn set_context_menu_extra<F>(&mut self, builder: F, cx: &mut Context<Self>)
where
F: Fn(PopupMenu, Entity<InputState>, &mut Window, &mut App) -> PopupMenu + 'static,
{
self.context_menu_extra = Some(Rc::new(builder));
cx.notify();
}
pub fn context_menu_override<F>(mut self, builder: F) -> Self
where
F: Fn(PopupMenu, Entity<InputState>, &mut Window, &mut App) -> PopupMenu + 'static,
{
self.context_menu_override = Some(Rc::new(builder));
self
}
pub fn set_context_menu_override<F>(&mut self, builder: F, cx: &mut Context<Self>)
where
F: Fn(PopupMenu, Entity<InputState>, &mut Window, &mut App) -> PopupMenu + 'static,
{
self.context_menu_override = Some(Rc::new(builder));
cx.notify();
}
pub fn clear_context_menu_customization(&mut self, cx: &mut Context<Self>) {
self.context_menu_extra = None;
self.context_menu_override = None;
cx.notify();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AppContext as _;
use std::cell::Cell;
struct Probe {
state: Entity<InputState>,
}
impl crate::Render for Probe {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> impl crate::IntoElement {
crate::div()
}
}
#[rgpui::test]
fn default_menu_tracks_state(cx: &mut crate::TestAppContext) {
cx.update(crate::input_ui::init);
cx.update(crate::theme::init);
let (probe, cx) = cx.add_window_view(|window, cx| Probe {
state: cx.new(|cx| InputState::new(window, cx).default_value("hello")),
});
let state = probe.read_with(cx, |probe, _| probe.state.clone());
assert!(!state.read_with(cx, |state, _| state.is_empty()));
assert!(!state.read_with(cx, |state, _| state.has_selection()));
assert!(!state.read_with(cx, |state, _| state.can_undo()));
cx.update(|_, cx| {
let menu = InputState::build_default_context_menu(PopupMenu::new(cx), &state, cx);
assert!(!menu.is_empty());
});
cx.update(|_, cx| {
state.update(cx, |state, cx| state.set_selected_range(0..5, cx));
});
assert!(state.read_with(cx, |state, _| state.has_selection()));
cx.update(|window, cx| {
state.update(cx, |state, cx| state.replace("hi", window, cx));
});
assert!(state.read_with(cx, |state, _| state.can_undo()));
}
#[rgpui::test]
fn custom_builders_are_invoked(cx: &mut crate::TestAppContext) {
cx.update(crate::input_ui::init);
cx.update(crate::theme::init);
let (probe, cx) = cx.add_window_view(|window, cx| Probe {
state: cx.new(|cx| InputState::new(window, cx).default_value("hello")),
});
let state = probe.read_with(cx, |probe, _| probe.state.clone());
let extra_called = Rc::new(Cell::new(false));
let override_called = Rc::new(Cell::new(false));
cx.update(|_, cx| {
state.update(cx, |state, cx| {
state.set_context_menu_extra(
{
let extra_called = extra_called.clone();
move |menu, _, _, _| {
extra_called.set(true);
menu
}
},
cx,
);
state.set_context_menu_override(
{
let override_called = override_called.clone();
move |menu, _, _, _| {
override_called.set(true);
menu.label("自定义")
}
},
cx,
);
});
});
cx.update(|window, cx| {
let menu = InputState::build_context_menu(PopupMenu::new(cx), &state, window, cx);
assert!(!menu.is_empty());
});
assert!(extra_called.get());
assert!(override_called.get());
cx.update(|_, cx| {
state.update(cx, |state, cx| state.clear_context_menu_customization(cx));
});
cx.update(|window, cx| {
let menu = InputState::build_context_menu(PopupMenu::new(cx), &state, window, cx);
assert!(!menu.is_empty());
});
}
struct InputProbe {
state: Entity<InputState>,
}
impl crate::Render for InputProbe {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> impl crate::IntoElement {
super::super::Input::new(&self.state)
.show_context_menu(false)
.context_menu_extra(|menu, _, _, _| menu)
}
}
#[rgpui::test]
fn input_element_writes_menu_config_to_state(cx: &mut crate::TestAppContext) {
cx.update(crate::input_ui::init);
cx.update(crate::theme::init);
let (probe, cx) = cx.add_window_view(|window, cx| InputProbe {
state: cx.new(|cx| InputState::new(window, cx).default_value("hello")),
});
let state = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|window, cx| {
_ = window.draw(cx);
});
let (enabled, has_extra) = state.read_with(cx, |state, _| {
(
state.is_context_menu_enabled(),
state.context_menu_extra.is_some(),
)
});
assert!(!enabled);
assert!(has_extra);
}
}