Context menus (#543)
Main usage: `response.context_menu(…)` and `ui.menu_button`
This commit is contained in:
parent
b31ca7efc9
commit
46fb9ff09b
10 changed files with 792 additions and 74 deletions
|
@ -11,6 +11,7 @@ use crate::{
|
|||
frame_state::FrameState,
|
||||
input_state::*,
|
||||
layers::GraphicLayers,
|
||||
menu::ContextMenuSystem,
|
||||
mutex::{Mutex, MutexGuard},
|
||||
*,
|
||||
};
|
||||
|
@ -303,6 +304,15 @@ impl CtxRef {
|
|||
pub fn debug_painter(&self) -> Painter {
|
||||
Self::layer_painter(self, LayerId::debug())
|
||||
}
|
||||
|
||||
pub(crate) fn show_context_menu(
|
||||
&self,
|
||||
response: &Response,
|
||||
add_contents: impl FnOnce(&mut Ui),
|
||||
) {
|
||||
self.context_menu_system()
|
||||
.context_menu(response, add_contents);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -329,6 +339,7 @@ pub struct Context {
|
|||
fonts: Option<Arc<Fonts>>,
|
||||
memory: Arc<Mutex<Memory>>,
|
||||
animation_manager: Arc<Mutex<AnimationManager>>,
|
||||
context_menu_system: Arc<Mutex<ContextMenuSystem>>,
|
||||
|
||||
input: InputState,
|
||||
|
||||
|
@ -357,6 +368,7 @@ impl Clone for Context {
|
|||
output: self.output.clone(),
|
||||
paint_stats: self.paint_stats.clone(),
|
||||
repaint_requests: self.repaint_requests.load(SeqCst).into(),
|
||||
context_menu_system: self.context_menu_system.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -375,6 +387,10 @@ impl Context {
|
|||
self.memory.lock()
|
||||
}
|
||||
|
||||
pub(crate) fn context_menu_system(&self) -> MutexGuard<'_, ContextMenuSystem> {
|
||||
self.context_menu_system.lock()
|
||||
}
|
||||
|
||||
pub(crate) fn graphics(&self) -> MutexGuard<'_, GraphicLayers> {
|
||||
self.graphics.lock()
|
||||
}
|
||||
|
|
582
egui/src/menu.rs
582
egui/src/menu.rs
|
@ -6,7 +6,7 @@
|
|||
//! use egui::{menu, Button};
|
||||
//!
|
||||
//! menu::bar(ui, |ui| {
|
||||
//! menu::menu(ui, "File", |ui| {
|
||||
//! ui.menu_button("File", |ui| {
|
||||
//! if ui.button("Open").clicked() {
|
||||
//! // …
|
||||
//! }
|
||||
|
@ -15,25 +15,53 @@
|
|||
//! }
|
||||
//! ```
|
||||
|
||||
use super::{
|
||||
style::{Spacing, WidgetVisuals},
|
||||
Align, CtxRef, Id, InnerResponse, PointerState, Pos2, Rect, Response, Sense, Style, TextStyle,
|
||||
Ui, Vec2,
|
||||
};
|
||||
use crate::{widgets::*, *};
|
||||
use epaint::Stroke;
|
||||
use epaint::{mutex::RwLock, Stroke};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// What is saved between frames.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default))]
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct BarState {
|
||||
open_menu: Option<Id>,
|
||||
open_menu: MenuRootManager,
|
||||
}
|
||||
|
||||
impl BarState {
|
||||
fn load(ctx: &Context, bar_id: &Id) -> Self {
|
||||
*ctx.memory().id_data_temp.get_or_default(*bar_id)
|
||||
ctx.memory()
|
||||
.id_data_temp
|
||||
.get_or_default::<Self>(*bar_id)
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn save(self, ctx: &Context, bar_id: Id) {
|
||||
ctx.memory().id_data_temp.insert(bar_id, self);
|
||||
}
|
||||
/// Show a menu at pointer if right-clicked response.
|
||||
/// Should be called from [`Context`] on a [`Response`]
|
||||
pub fn bar_menu<R>(
|
||||
&mut self,
|
||||
response: &Response,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
MenuRoot::stationary_click_interaction(response, &mut self.open_menu, response.id);
|
||||
self.open_menu.show(response, add_contents)
|
||||
}
|
||||
}
|
||||
impl std::ops::Deref for BarState {
|
||||
type Target = MenuRootManager;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.open_menu
|
||||
}
|
||||
}
|
||||
impl std::ops::DerefMut for BarState {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.open_menu
|
||||
}
|
||||
}
|
||||
|
||||
/// The menu bar goes well in a [`TopBottomPanel::top`],
|
||||
|
@ -58,58 +86,77 @@ pub fn bar<R>(ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResp
|
|||
add_contents(ui)
|
||||
})
|
||||
}
|
||||
|
||||
/// Construct a top level menu in a menu bar. This would be e.g. "File", "Edit" etc.
|
||||
///
|
||||
/// Returns `None` if the menu is not open.
|
||||
pub fn menu<R>(
|
||||
pub fn menu_button<R>(
|
||||
ui: &mut Ui,
|
||||
title: impl ToString,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
menu_impl(ui, title, Box::new(add_contents))
|
||||
) -> InnerResponse<Option<R>> {
|
||||
stationary_menu_impl(ui, title, Box::new(add_contents))
|
||||
}
|
||||
/// Construct a nested sub menu in another menu.
|
||||
///
|
||||
/// Returns `None` if the menu is not open.
|
||||
pub(crate) fn submenu_button<R>(
|
||||
ui: &mut Ui,
|
||||
parent_state: Arc<RwLock<MenuState>>,
|
||||
title: impl ToString,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
SubMenu::new(parent_state, title).show(ui, add_contents)
|
||||
}
|
||||
|
||||
/// wrapper for the contents of every menu.
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub(crate) fn menu_ui<'c, R>(
|
||||
ctx: &CtxRef,
|
||||
menu_id: impl std::hash::Hash,
|
||||
pos: Pos2,
|
||||
menu_state_arc: Arc<RwLock<MenuState>>,
|
||||
mut style: Style,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R + 'c,
|
||||
) -> InnerResponse<R> {
|
||||
let area = Area::new(menu_id)
|
||||
.order(Order::Foreground)
|
||||
.fixed_pos(pos)
|
||||
.interactable(false)
|
||||
.drag_bounds(Rect::EVERYTHING);
|
||||
let frame = Frame::menu(&style);
|
||||
|
||||
area.show(ctx, |ui| {
|
||||
frame
|
||||
.show(ui, |ui| {
|
||||
const DEFAULT_MENU_WIDTH: f32 = 150.0; // TODO: add to ui.spacing
|
||||
ui.set_max_width(DEFAULT_MENU_WIDTH);
|
||||
|
||||
let pos = {
|
||||
let mut menu_state = menu_state_arc.write();
|
||||
menu_state.entry_count = 0;
|
||||
menu_state.rect.min
|
||||
};
|
||||
// style.visuals.widgets.active.bg_fill = Color32::TRANSPARENT;
|
||||
style.visuals.widgets.active.bg_stroke = Stroke::none();
|
||||
// style.visuals.widgets.hovered.bg_fill = Color32::TRANSPARENT;
|
||||
style.visuals.widgets.hovered.bg_stroke = Stroke::none();
|
||||
style.visuals.widgets.inactive.bg_fill = Color32::TRANSPARENT;
|
||||
style.visuals.widgets.inactive.bg_stroke = Stroke::none();
|
||||
let area = Area::new(menu_id)
|
||||
.order(Order::Foreground)
|
||||
.fixed_pos(pos)
|
||||
.interactable(false)
|
||||
.drag_bounds(Rect::EVERYTHING);
|
||||
let frame = Frame::menu(&style);
|
||||
let inner_response = area.show(ctx, |ui| {
|
||||
frame
|
||||
.show(ui, |ui| {
|
||||
const DEFAULT_MENU_WIDTH: f32 = 150.0; // TODO: add to ui.spacing
|
||||
ui.set_max_width(DEFAULT_MENU_WIDTH);
|
||||
ui.set_style(style);
|
||||
ui.set_menu_state(Some(menu_state_arc.clone()));
|
||||
ui.with_layout(Layout::top_down_justified(Align::LEFT), add_contents)
|
||||
.inner
|
||||
})
|
||||
.inner
|
||||
})
|
||||
});
|
||||
menu_state_arc.write().rect = inner_response.response.rect;
|
||||
inner_response
|
||||
}
|
||||
|
||||
/// build a top level menu with a button
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn menu_impl<'c, R>(
|
||||
fn stationary_menu_impl<'c, R>(
|
||||
ui: &mut Ui,
|
||||
title: impl ToString,
|
||||
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
|
||||
) -> Option<R> {
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let title = title.to_string();
|
||||
let bar_id = ui.id();
|
||||
let menu_id = bar_id.with(&title);
|
||||
|
@ -118,43 +165,458 @@ fn menu_impl<'c, R>(
|
|||
|
||||
let mut button = Button::new(title);
|
||||
|
||||
if bar_state.open_menu == Some(menu_id) {
|
||||
if bar_state.open_menu.is_menu_open(menu_id) {
|
||||
button = button.fill(ui.visuals().widgets.open.bg_fill);
|
||||
button = button.stroke(ui.visuals().widgets.open.bg_stroke);
|
||||
}
|
||||
|
||||
let button_response = ui.add(button);
|
||||
if button_response.clicked() {
|
||||
// Toggle
|
||||
if bar_state.open_menu == Some(menu_id) {
|
||||
bar_state.open_menu = None;
|
||||
} else {
|
||||
bar_state.open_menu = Some(menu_id);
|
||||
}
|
||||
} else if button_response.hovered() && bar_state.open_menu.is_some() {
|
||||
bar_state.open_menu = Some(menu_id);
|
||||
}
|
||||
|
||||
let inner = if bar_state.open_menu == Some(menu_id) || ui.ctx().memory().everything_is_visible()
|
||||
{
|
||||
let inner = menu_ui(
|
||||
ui.ctx(),
|
||||
menu_id,
|
||||
button_response.rect.left_bottom(),
|
||||
ui.style().as_ref().clone(),
|
||||
add_contents,
|
||||
)
|
||||
.inner;
|
||||
|
||||
// TODO: this prevents sub-menus in menus. We should fix that.
|
||||
if ui.input().key_pressed(Key::Escape) || button_response.clicked_elsewhere() {
|
||||
bar_state.open_menu = None;
|
||||
}
|
||||
Some(inner)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let inner = bar_state.bar_menu(&button_response, add_contents);
|
||||
|
||||
bar_state.save(ui.ctx(), bar_id);
|
||||
inner
|
||||
InnerResponse::new(inner.map(|r| r.inner), button_response)
|
||||
}
|
||||
|
||||
/// Stores the state for the context menu.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ContextMenuSystem {
|
||||
root: MenuRootManager,
|
||||
}
|
||||
impl ContextMenuSystem {
|
||||
/// Show a menu at pointer if right-clicked response.
|
||||
/// Should be called from [`Context`] on a [`Response`]
|
||||
pub fn context_menu(
|
||||
&mut self,
|
||||
response: &Response,
|
||||
add_contents: impl FnOnce(&mut Ui),
|
||||
) -> Option<InnerResponse<()>> {
|
||||
MenuRoot::context_click_interaction(response, &mut self.root, response.id);
|
||||
self.root.show(response, add_contents)
|
||||
}
|
||||
}
|
||||
impl std::ops::Deref for ContextMenuSystem {
|
||||
type Target = MenuRootManager;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.root
|
||||
}
|
||||
}
|
||||
impl std::ops::DerefMut for ContextMenuSystem {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.root
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores the state for the context menu.
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct MenuRootManager {
|
||||
inner: Option<MenuRoot>,
|
||||
}
|
||||
impl MenuRootManager {
|
||||
/// Show a menu at pointer if right-clicked response.
|
||||
/// Should be called from [`Context`] on a [`Response`]
|
||||
pub fn show<R>(
|
||||
&mut self,
|
||||
response: &Response,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<InnerResponse<R>> {
|
||||
if let Some(root) = self.inner.as_mut() {
|
||||
let (menu_response, inner_response) = root.show(response, add_contents);
|
||||
if let MenuResponse::Close = menu_response {
|
||||
self.inner = None;
|
||||
}
|
||||
inner_response
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn is_menu_open(&self, id: Id) -> bool {
|
||||
self.inner.as_ref().map(|m| m.id) == Some(id)
|
||||
}
|
||||
}
|
||||
impl std::ops::Deref for MenuRootManager {
|
||||
type Target = Option<MenuRoot>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
impl std::ops::DerefMut for MenuRootManager {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Menu root associated with an Id from a Response
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct MenuRoot {
|
||||
pub menu_state: Arc<RwLock<MenuState>>,
|
||||
pub id: Id,
|
||||
}
|
||||
|
||||
impl MenuRoot {
|
||||
pub fn new(position: Pos2, id: Id) -> Self {
|
||||
Self {
|
||||
menu_state: Arc::new(RwLock::new(MenuState::new(position))),
|
||||
id,
|
||||
}
|
||||
}
|
||||
pub fn show<R>(
|
||||
&mut self,
|
||||
response: &Response,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> (MenuResponse, Option<InnerResponse<R>>) {
|
||||
if self.id == response.id {
|
||||
let inner_response =
|
||||
MenuState::show(&response.ctx, &self.menu_state, self.id, add_contents);
|
||||
let mut menu_state = self.menu_state.write();
|
||||
menu_state.rect = inner_response.response.rect;
|
||||
|
||||
if menu_state.response.is_close() {
|
||||
return (MenuResponse::Close, Some(inner_response));
|
||||
}
|
||||
}
|
||||
(MenuResponse::Stay, None)
|
||||
}
|
||||
/// interaction with a stationary menu, i.e. fixed in another Ui
|
||||
fn stationary_interaction(
|
||||
response: &Response,
|
||||
root: &mut MenuRootManager,
|
||||
id: Id,
|
||||
) -> MenuResponse {
|
||||
let pointer = &response.ctx.input().pointer;
|
||||
if (response.clicked() && root.is_menu_open(id))
|
||||
|| response.ctx.input().key_pressed(Key::Escape)
|
||||
{
|
||||
// menu open and button clicked or esc pressed
|
||||
return MenuResponse::Close;
|
||||
} else if (response.clicked() && !root.is_menu_open(id))
|
||||
|| (response.hovered() && root.is_some())
|
||||
{
|
||||
// menu not open and button clicked
|
||||
// or button hovered while other menu is open
|
||||
let pos = response.rect.left_bottom();
|
||||
return MenuResponse::Create(pos, id);
|
||||
} else if pointer.any_pressed() && pointer.primary_down() {
|
||||
if let Some(pos) = pointer.interact_pos() {
|
||||
if let Some(root) = root.inner.as_mut() {
|
||||
if root.id == id {
|
||||
// pressed somewhere while this menu is open
|
||||
let menu_state = root.menu_state.read();
|
||||
let in_menu = menu_state.area_contains(pos);
|
||||
if !in_menu {
|
||||
return MenuResponse::Close;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MenuResponse::Stay
|
||||
}
|
||||
/// interaction with a context menu
|
||||
fn context_interaction(
|
||||
response: &Response,
|
||||
root: &mut Option<MenuRoot>,
|
||||
id: Id,
|
||||
) -> MenuResponse {
|
||||
let response = response.interact(Sense::click());
|
||||
let pointer = &response.ctx.input().pointer;
|
||||
if pointer.any_pressed() {
|
||||
if let Some(pos) = pointer.interact_pos() {
|
||||
let mut destroy = false;
|
||||
let mut in_old_menu = false;
|
||||
if let Some(root) = root {
|
||||
let menu_state = root.menu_state.read();
|
||||
in_old_menu = menu_state.area_contains(pos);
|
||||
destroy = root.id == response.id;
|
||||
}
|
||||
if !in_old_menu {
|
||||
let in_target = response.rect.contains(pos);
|
||||
if in_target && pointer.secondary_down() {
|
||||
return MenuResponse::Create(pos, id);
|
||||
} else if (in_target && pointer.primary_down()) || destroy {
|
||||
return MenuResponse::Close;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MenuResponse::Stay
|
||||
}
|
||||
fn handle_menu_response(root: &mut MenuRootManager, menu_response: MenuResponse) {
|
||||
match menu_response {
|
||||
MenuResponse::Create(pos, id) => {
|
||||
root.inner = Some(MenuRoot::new(pos, id));
|
||||
}
|
||||
MenuResponse::Close => root.inner = None,
|
||||
MenuResponse::Stay => {}
|
||||
}
|
||||
}
|
||||
pub fn context_click_interaction(response: &Response, root: &mut MenuRootManager, id: Id) {
|
||||
let menu_response = Self::context_interaction(response, root, id);
|
||||
Self::handle_menu_response(root, menu_response);
|
||||
}
|
||||
pub fn stationary_click_interaction(response: &Response, root: &mut MenuRootManager, id: Id) {
|
||||
let menu_response = Self::stationary_interaction(response, root, id);
|
||||
Self::handle_menu_response(root, menu_response);
|
||||
}
|
||||
}
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub(crate) enum MenuResponse {
|
||||
Close,
|
||||
Stay,
|
||||
Create(Pos2, Id),
|
||||
}
|
||||
impl MenuResponse {
|
||||
pub fn is_close(&self) -> bool {
|
||||
*self == Self::Close
|
||||
}
|
||||
}
|
||||
pub struct SubMenuButton {
|
||||
text: String,
|
||||
icon: String,
|
||||
index: usize,
|
||||
}
|
||||
impl SubMenuButton {
|
||||
/// The `icon` can be an emoji (e.g. `⏵` right arrow), shown right of the label
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn new(text: impl ToString, icon: impl ToString, index: usize) -> Self {
|
||||
Self {
|
||||
text: text.to_string(),
|
||||
icon: icon.to_string(),
|
||||
index,
|
||||
}
|
||||
}
|
||||
fn visuals<'a>(
|
||||
ui: &'a Ui,
|
||||
response: &'_ Response,
|
||||
menu_state: &'_ MenuState,
|
||||
sub_id: Id,
|
||||
) -> &'a WidgetVisuals {
|
||||
if menu_state.is_open(sub_id) {
|
||||
&ui.style().visuals.widgets.hovered
|
||||
} else {
|
||||
ui.style().interact(response)
|
||||
}
|
||||
}
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
pub fn icon(mut self, icon: impl ToString) -> Self {
|
||||
self.icon = icon.to_string();
|
||||
self
|
||||
}
|
||||
pub(crate) fn show(self, ui: &mut Ui, menu_state: &MenuState, sub_id: Id) -> Response {
|
||||
let SubMenuButton { text, icon, .. } = self;
|
||||
|
||||
let text_style = TextStyle::Button;
|
||||
let sense = Sense::click();
|
||||
|
||||
let button_padding = ui.spacing().button_padding;
|
||||
let total_extra = button_padding + button_padding;
|
||||
let text_available_width = ui.available_width() - total_extra.x;
|
||||
let text_galley = ui
|
||||
.fonts()
|
||||
.layout_delayed_color(text, text_style, text_available_width);
|
||||
|
||||
let icon_available_width = text_available_width - text_galley.size().x;
|
||||
let icon_galley = ui
|
||||
.fonts()
|
||||
.layout_delayed_color(icon, text_style, icon_available_width);
|
||||
let text_and_icon_size = Vec2::new(
|
||||
text_galley.size().x + icon_galley.size().x,
|
||||
text_galley.size().y.max(icon_galley.size().y),
|
||||
);
|
||||
let desired_size = text_and_icon_size + 2.0 * button_padding;
|
||||
|
||||
let (rect, response) = ui.allocate_at_least(desired_size, sense);
|
||||
response.widget_info(|| {
|
||||
crate::WidgetInfo::labeled(crate::WidgetType::Button, &text_galley.text())
|
||||
});
|
||||
|
||||
if ui.clip_rect().intersects(rect) {
|
||||
let visuals = Self::visuals(ui, &response, menu_state, sub_id);
|
||||
let text_pos = Align2::LEFT_CENTER
|
||||
.align_size_within_rect(text_galley.size(), rect.shrink2(button_padding))
|
||||
.min;
|
||||
let icon_pos = Align2::RIGHT_CENTER
|
||||
.align_size_within_rect(icon_galley.size(), rect.shrink2(button_padding))
|
||||
.min;
|
||||
|
||||
ui.painter().rect_filled(
|
||||
rect.expand(visuals.expansion),
|
||||
visuals.corner_radius,
|
||||
visuals.bg_fill,
|
||||
);
|
||||
|
||||
let text_color = visuals.text_color();
|
||||
ui.painter()
|
||||
.galley_with_color(text_pos, text_galley, text_color);
|
||||
ui.painter()
|
||||
.galley_with_color(icon_pos, icon_galley, text_color);
|
||||
}
|
||||
response
|
||||
}
|
||||
}
|
||||
pub struct SubMenu {
|
||||
button: SubMenuButton,
|
||||
parent_state: Arc<RwLock<MenuState>>,
|
||||
}
|
||||
impl SubMenu {
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn new(parent_state: Arc<RwLock<MenuState>>, text: impl ToString) -> Self {
|
||||
let index = parent_state.write().next_entry_index();
|
||||
Self {
|
||||
button: SubMenuButton::new(text, "⏵", index),
|
||||
parent_state,
|
||||
}
|
||||
}
|
||||
pub fn show<R>(
|
||||
self,
|
||||
ui: &mut Ui,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
let sub_id = ui.id().with(self.button.index);
|
||||
let button = self.button.show(ui, &*self.parent_state.read(), sub_id);
|
||||
self.parent_state
|
||||
.write()
|
||||
.submenu_button_interaction(ui, sub_id, &button);
|
||||
let inner = self
|
||||
.parent_state
|
||||
.write()
|
||||
.show_submenu(ui.ctx(), sub_id, add_contents);
|
||||
InnerResponse::new(inner, button)
|
||||
}
|
||||
}
|
||||
pub(crate) struct MenuState {
|
||||
/// The opened sub-menu and its `Id`
|
||||
sub_menu: Option<(Id, Arc<RwLock<MenuState>>)>,
|
||||
/// Bounding box of this menu (without the sub-menu)
|
||||
pub rect: Rect,
|
||||
/// Used to check if any menu in the tree wants to close
|
||||
pub response: MenuResponse,
|
||||
/// Used to hash different `Id`s for sub-menus
|
||||
entry_count: usize,
|
||||
}
|
||||
impl MenuState {
|
||||
pub fn new(position: Pos2) -> Self {
|
||||
Self {
|
||||
rect: Rect::from_min_size(position, Vec2::ZERO),
|
||||
sub_menu: None,
|
||||
response: MenuResponse::Stay,
|
||||
entry_count: 0,
|
||||
}
|
||||
}
|
||||
/// Close menu hierarchy.
|
||||
pub fn close(&mut self) {
|
||||
self.response = MenuResponse::Close;
|
||||
}
|
||||
pub fn show<R>(
|
||||
ctx: &CtxRef,
|
||||
menu_state: &Arc<RwLock<Self>>,
|
||||
id: Id,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<R> {
|
||||
let style = Style {
|
||||
spacing: Spacing {
|
||||
item_spacing: Vec2::ZERO,
|
||||
button_padding: crate::vec2(2.0, 0.0),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let menu_state_arc = menu_state.clone();
|
||||
crate::menu::menu_ui(ctx, id, menu_state_arc, style, add_contents)
|
||||
}
|
||||
fn show_submenu<R>(
|
||||
&mut self,
|
||||
ctx: &CtxRef,
|
||||
id: Id,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> Option<R> {
|
||||
let (sub_response, response) = self.get_submenu(id).map(|sub| {
|
||||
let inner_response = Self::show(ctx, sub, id, add_contents);
|
||||
(sub.read().response, inner_response.inner)
|
||||
})?;
|
||||
self.cascade_close_response(sub_response);
|
||||
Some(response)
|
||||
}
|
||||
/// Check if position is in the menu hierarchy's area.
|
||||
pub fn area_contains(&self, pos: Pos2) -> bool {
|
||||
self.rect.contains(pos)
|
||||
|| self
|
||||
.sub_menu
|
||||
.as_ref()
|
||||
.map_or(false, |(_, sub)| sub.read().area_contains(pos))
|
||||
}
|
||||
fn next_entry_index(&mut self) -> usize {
|
||||
self.entry_count += 1;
|
||||
self.entry_count - 1
|
||||
}
|
||||
/// Sense button interaction opening and closing submenu.
|
||||
fn submenu_button_interaction(&mut self, ui: &mut Ui, sub_id: Id, button: &Response) {
|
||||
let pointer = &ui.input().pointer.clone();
|
||||
let open = self.is_open(sub_id);
|
||||
if self.moving_towards_current_submenu(pointer) {
|
||||
// ensure to repaint once even when pointer is not moving
|
||||
ui.ctx().request_repaint();
|
||||
} else if !open && button.hovered() {
|
||||
let pos = button.rect.right_top();
|
||||
self.open_submenu(sub_id, pos);
|
||||
} else if open && !button.hovered() && !self.hovering_current_submenu(pointer) {
|
||||
self.close_submenu();
|
||||
}
|
||||
}
|
||||
/// Check if `dir` points from `pos` towards left side of `rect`.
|
||||
fn points_at_left_of_rect(pos: Pos2, dir: Vec2, rect: Rect) -> bool {
|
||||
let vel_a = dir.angle();
|
||||
let top_a = (rect.left_top() - pos).angle();
|
||||
let bottom_a = (rect.left_bottom() - pos).angle();
|
||||
bottom_a - vel_a >= 0.0 && top_a - vel_a <= 0.0
|
||||
}
|
||||
/// Check if pointer is moving towards current submenu.
|
||||
fn moving_towards_current_submenu(&self, pointer: &PointerState) -> bool {
|
||||
if pointer.is_still() {
|
||||
return false;
|
||||
}
|
||||
if let Some(sub_menu) = self.get_current_submenu() {
|
||||
if let Some(pos) = pointer.hover_pos() {
|
||||
return Self::points_at_left_of_rect(pos, pointer.velocity(), sub_menu.read().rect);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
/// Check if pointer is hovering current submenu.
|
||||
fn hovering_current_submenu(&self, pointer: &PointerState) -> bool {
|
||||
if let Some(sub_menu) = self.get_current_submenu() {
|
||||
if let Some(pos) = pointer.hover_pos() {
|
||||
return sub_menu.read().area_contains(pos);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
/// Cascade close response to menu root.
|
||||
fn cascade_close_response(&mut self, response: MenuResponse) {
|
||||
if response.is_close() {
|
||||
self.response = response;
|
||||
}
|
||||
}
|
||||
fn is_open(&self, id: Id) -> bool {
|
||||
self.get_sub_id() == Some(id)
|
||||
}
|
||||
fn get_sub_id(&self) -> Option<Id> {
|
||||
self.sub_menu.as_ref().map(|(id, _)| *id)
|
||||
}
|
||||
fn get_current_submenu(&self) -> Option<&Arc<RwLock<MenuState>>> {
|
||||
self.sub_menu.as_ref().map(|(_, sub)| sub)
|
||||
}
|
||||
fn get_submenu(&mut self, id: Id) -> Option<&Arc<RwLock<MenuState>>> {
|
||||
self.sub_menu
|
||||
.as_ref()
|
||||
.and_then(|(k, sub)| if id == *k { Some(sub) } else { None })
|
||||
}
|
||||
/// Open submenu at position, if not already open.
|
||||
fn open_submenu(&mut self, id: Id, pos: Pos2) {
|
||||
if !self.is_open(id) {
|
||||
self.sub_menu = Some((id, Arc::new(RwLock::new(MenuState::new(pos)))));
|
||||
}
|
||||
}
|
||||
fn close_submenu(&mut self) {
|
||||
self.sub_menu = None;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -470,6 +470,11 @@ impl Response {
|
|||
self.ctx.output().events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context_menu(&self, add_contents: impl FnOnce(&mut Ui)) -> &Self {
|
||||
self.ctx.show_context_menu(self, add_contents);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
// #![warn(missing_docs)]
|
||||
|
||||
use epaint::mutex::RwLock;
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
color::*, containers::*, epaint::text::Fonts, layout::*, mutex::MutexGuard, placer::Placer,
|
||||
widgets::*, *,
|
||||
color::*, containers::*, epaint::text::Fonts, layout::*, menu::MenuState, mutex::MutexGuard,
|
||||
placer::Placer, widgets::*, *,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
@ -54,6 +56,9 @@ pub struct Ui {
|
|||
/// If false we are unresponsive to input,
|
||||
/// and all widgets will assume a gray style.
|
||||
enabled: bool,
|
||||
|
||||
/// Indicates whether this Ui belongs to a Menu.
|
||||
menu_state: Option<Arc<RwLock<MenuState>>>,
|
||||
}
|
||||
|
||||
impl Ui {
|
||||
|
@ -73,6 +78,7 @@ impl Ui {
|
|||
style,
|
||||
placer: Placer::new(max_rect, Layout::default()),
|
||||
enabled: true,
|
||||
menu_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -91,7 +97,7 @@ impl Ui {
|
|||
crate::egui_assert!(!max_rect.any_nan());
|
||||
let next_auto_id_source = Id::new(self.next_auto_id_source).with("child").value();
|
||||
self.next_auto_id_source = self.next_auto_id_source.wrapping_add(1);
|
||||
|
||||
let menu_state = self.get_menu_state();
|
||||
Ui {
|
||||
id: self.id.with(id_source),
|
||||
next_auto_id_source,
|
||||
|
@ -99,6 +105,7 @@ impl Ui {
|
|||
style: self.style.clone(),
|
||||
placer: Placer::new(max_rect, layout),
|
||||
enabled: self.enabled,
|
||||
menu_state,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1730,6 +1737,41 @@ impl Ui {
|
|||
self.advance_cursor_after_rect(Rect::from_min_size(top_left, size));
|
||||
result
|
||||
}
|
||||
/// Close menu (with submenus), if any.
|
||||
pub fn close_menu(&mut self) {
|
||||
if let Some(menu_state) = &mut self.menu_state {
|
||||
menu_state.write().close();
|
||||
}
|
||||
self.menu_state = None;
|
||||
}
|
||||
pub(crate) fn get_menu_state(&self) -> Option<Arc<RwLock<MenuState>>> {
|
||||
self.menu_state.clone()
|
||||
}
|
||||
pub(crate) fn set_menu_state(&mut self, menu_state: Option<Arc<RwLock<MenuState>>>) {
|
||||
self.menu_state = menu_state;
|
||||
}
|
||||
#[inline(always)]
|
||||
/// Create a menu button. Creates a button for a sub-menu when the `Ui` is inside a menu.
|
||||
///
|
||||
/// ```
|
||||
/// # let mut ui = egui::Ui::__test();
|
||||
/// ui.menu_button("My menu", |ui| {
|
||||
/// ui.menu_button("My sub-menu", |ui| {
|
||||
/// ui.label("Item");
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
pub fn menu_button<R>(
|
||||
&mut self,
|
||||
title: impl ToString,
|
||||
add_contents: impl FnOnce(&mut Ui) -> R,
|
||||
) -> InnerResponse<Option<R>> {
|
||||
if let Some(menu_state) = self.menu_state.clone() {
|
||||
menu::submenu_button(self, menu_state, title, add_contents)
|
||||
} else {
|
||||
menu::menu_button(self, title, add_contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
|
174
egui_demo_lib/src/apps/demo/context_menu.rs
Normal file
174
egui_demo_lib/src/apps/demo/context_menu.rs
Normal file
|
@ -0,0 +1,174 @@
|
|||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
enum Plot {
|
||||
Sin,
|
||||
Bell,
|
||||
Sigmoid,
|
||||
}
|
||||
|
||||
fn gaussian(x: f64) -> f64 {
|
||||
let var: f64 = 2.0;
|
||||
f64::exp(-(x / var).powi(2)) / (var * f64::sqrt(std::f64::consts::TAU))
|
||||
}
|
||||
fn sigmoid(x: f64) -> f64 {
|
||||
-1.0 + 2.0 / (1.0 + f64::exp(-x))
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct ContextMenus {
|
||||
title: String,
|
||||
plot: Plot,
|
||||
show_axes: [bool; 2],
|
||||
allow_drag: bool,
|
||||
allow_zoom: bool,
|
||||
center_x_axis: bool,
|
||||
center_y_axis: bool,
|
||||
width: f32,
|
||||
height: f32,
|
||||
}
|
||||
|
||||
impl ContextMenus {
|
||||
fn example_plot(&self) -> egui::plot::Plot {
|
||||
use egui::plot::{Line, Value, Values};
|
||||
let n = 128;
|
||||
let line = Line::new(Values::from_values_iter((0..=n).map(|i| {
|
||||
use std::f64::consts::TAU;
|
||||
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
|
||||
match self.plot {
|
||||
Plot::Sin => Value::new(x, x.sin()),
|
||||
Plot::Bell => Value::new(x, 10.0 * gaussian(x)),
|
||||
Plot::Sigmoid => Value::new(x, sigmoid(x)),
|
||||
}
|
||||
})));
|
||||
egui::plot::Plot::new("example_plot")
|
||||
.show_axes(self.show_axes)
|
||||
.allow_drag(self.allow_drag)
|
||||
.allow_zoom(self.allow_zoom)
|
||||
.center_x_axis(self.center_x_axis)
|
||||
.center_x_axis(self.center_y_axis)
|
||||
.line(line)
|
||||
.width(self.width)
|
||||
.height(self.height)
|
||||
.data_aspect(1.0)
|
||||
}
|
||||
fn nested_menus(ui: &mut egui::Ui) {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
let _ = ui.button("Item");
|
||||
});
|
||||
let _ = ui.button("Item");
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
ui.menu_button("SubMenu", |ui| {
|
||||
let _ = ui.button("Item1");
|
||||
let _ = ui.button("Item2");
|
||||
let _ = ui.button("Item3");
|
||||
let _ = ui.button("Item4");
|
||||
if ui.button("Open...").clicked() {
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
let _ = ui.button("Very long text for this item");
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_TITLE: &str = "☰ Context Menus";
|
||||
|
||||
impl Default for ContextMenus {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
title: DEFAULT_TITLE.to_owned(),
|
||||
plot: Plot::Sin,
|
||||
show_axes: [true, true],
|
||||
allow_drag: true,
|
||||
allow_zoom: true,
|
||||
center_x_axis: false,
|
||||
center_y_axis: false,
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl super::Demo for ContextMenus {
|
||||
fn name(&self) -> &'static str {
|
||||
DEFAULT_TITLE
|
||||
}
|
||||
|
||||
fn show(&mut self, ctx: &egui::CtxRef, open: &mut bool) {
|
||||
let Self { title, .. } = self.clone();
|
||||
|
||||
use super::View;
|
||||
let window = egui::Window::new(title)
|
||||
.id(egui::Id::new("demo_context_menus")) // required since we change the title
|
||||
.vscroll(false)
|
||||
.open(open);
|
||||
window.show(ctx, |ui| self.ui(ui));
|
||||
}
|
||||
}
|
||||
|
||||
impl super::View for ContextMenus {
|
||||
fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| ui.text_edit_singleline(&mut self.title));
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(self.example_plot())
|
||||
.on_hover_text("Right click for options")
|
||||
.context_menu(|ui| {
|
||||
ui.menu_button("Plot", |ui| {
|
||||
if ui.radio_value(&mut self.plot, Plot::Sin, "Sin").clicked()
|
||||
|| ui
|
||||
.radio_value(&mut self.plot, Plot::Bell, "Gaussian")
|
||||
.clicked()
|
||||
|| ui
|
||||
.radio_value(&mut self.plot, Plot::Sigmoid, "Sigmoid")
|
||||
.clicked()
|
||||
{
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
egui::Grid::new("button_grid").show(ui, |ui| {
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.width)
|
||||
.speed(1.0)
|
||||
.prefix("Width:"),
|
||||
);
|
||||
ui.add(
|
||||
egui::DragValue::new(&mut self.height)
|
||||
.speed(1.0)
|
||||
.prefix("Height:"),
|
||||
);
|
||||
ui.end_row();
|
||||
ui.checkbox(&mut self.show_axes[0], "x-Axis");
|
||||
ui.checkbox(&mut self.show_axes[1], "y-Axis");
|
||||
ui.end_row();
|
||||
if ui.checkbox(&mut self.allow_drag, "Drag").changed()
|
||||
|| ui.checkbox(&mut self.allow_zoom, "Zoom").changed()
|
||||
{
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.label("Right-click plot to edit it!");
|
||||
ui.separator();
|
||||
ui.horizontal(|ui| {
|
||||
ui.menu_button("Click for menu", Self::nested_menus);
|
||||
ui.button("Right-click for menu")
|
||||
.context_menu(Self::nested_menus);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ impl Default for Demos {
|
|||
Self::from_demos(vec![
|
||||
Box::new(super::code_editor::CodeEditor::default()),
|
||||
Box::new(super::code_example::CodeExample::default()),
|
||||
Box::new(super::context_menu::ContextMenus::default()),
|
||||
Box::new(super::dancing_strings::DancingStrings::default()),
|
||||
Box::new(super::drag_and_drop::DragAndDropDemo::default()),
|
||||
Box::new(super::font_book::FontBook::default()),
|
||||
|
@ -227,9 +228,10 @@ fn show_menu_bar(ui: &mut Ui) {
|
|||
use egui::*;
|
||||
|
||||
menu::bar(ui, |ui| {
|
||||
menu::menu(ui, "File", |ui| {
|
||||
ui.menu_button("File", |ui| {
|
||||
if ui.button("Organize windows").clicked() {
|
||||
ui.ctx().memory().reset_areas();
|
||||
ui.close_menu();
|
||||
}
|
||||
if ui
|
||||
.button("Reset egui memory")
|
||||
|
@ -237,6 +239,7 @@ fn show_menu_bar(ui: &mut Ui) {
|
|||
.clicked()
|
||||
{
|
||||
*ui.ctx().memory() = Default::default();
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -75,12 +75,12 @@ pub fn drop_target<R>(
|
|||
|
||||
InnerResponse::new(ret, response)
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
#[cfg_attr(feature = "persistence", derive(serde::Deserialize, serde::Serialize))]
|
||||
pub struct DragAndDropDemo {
|
||||
/// columns with items
|
||||
columns: Vec<Vec<&'static str>>,
|
||||
columns: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for DragAndDropDemo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
@ -88,7 +88,10 @@ impl Default for DragAndDropDemo {
|
|||
vec!["Item A", "Item B", "Item C"],
|
||||
vec!["Item D", "Item E"],
|
||||
vec!["Item F", "Item G", "Item H"],
|
||||
],
|
||||
]
|
||||
.into_iter()
|
||||
.map(|v| v.into_iter().map(ToString::to_string).collect())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,20 +117,25 @@ impl super::View for DragAndDropDemo {
|
|||
ui.label("This is a proof-of-concept of drag-and-drop in egui.");
|
||||
ui.label("Drag items between columns.");
|
||||
|
||||
let id_source = "my_drag_and_drop_demo";
|
||||
let mut source_col_row = None;
|
||||
let mut drop_col = None;
|
||||
|
||||
ui.columns(self.columns.len(), |uis| {
|
||||
for (col_idx, column) in self.columns.iter().enumerate() {
|
||||
for (col_idx, column) in self.columns.clone().into_iter().enumerate() {
|
||||
let ui = &mut uis[col_idx];
|
||||
let can_accept_what_is_being_dragged = true; // We accept anything being dragged (for now) ¯\_(ツ)_/¯
|
||||
let response = drop_target(ui, can_accept_what_is_being_dragged, |ui| {
|
||||
ui.set_min_size(vec2(64.0, 100.0));
|
||||
|
||||
for (row_idx, &item) in column.iter().enumerate() {
|
||||
let item_id = Id::new("item").with(col_idx).with(row_idx);
|
||||
for (row_idx, item) in column.iter().enumerate() {
|
||||
let item_id = Id::new(id_source).with(col_idx).with(row_idx);
|
||||
drag_source(ui, item_id, |ui| {
|
||||
ui.label(item);
|
||||
let response = ui.add(Label::new(item).sense(Sense::click()));
|
||||
response.context_menu(|ui| {
|
||||
if ui.button("Remove").clicked() {
|
||||
self.columns[col_idx].remove(row_idx);
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if ui.memory().is_being_dragged(item_id) {
|
||||
|
@ -137,6 +145,13 @@ impl super::View for DragAndDropDemo {
|
|||
})
|
||||
.response;
|
||||
|
||||
response.context_menu(|ui| {
|
||||
if ui.button("New Item").clicked() {
|
||||
self.columns[col_idx].push("New Item".to_string());
|
||||
ui.close_menu();
|
||||
}
|
||||
});
|
||||
|
||||
let is_being_dragged = ui.memory().is_anything_being_dragged();
|
||||
if is_being_dragged && can_accept_what_is_being_dragged && response.hovered() {
|
||||
drop_col = Some(col_idx);
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
mod app;
|
||||
pub mod code_editor;
|
||||
pub mod code_example;
|
||||
pub mod context_menu;
|
||||
pub mod dancing_strings;
|
||||
pub mod demo_app_windows;
|
||||
pub mod drag_and_drop;
|
||||
|
|
|
@ -212,6 +212,7 @@ impl WidgetGallery {
|
|||
|
||||
ui.add(doc_link_label("Plot", "plot"));
|
||||
ui.add(example_plot());
|
||||
|
||||
ui.end_row();
|
||||
|
||||
ui.hyperlink_to(
|
||||
|
@ -227,14 +228,14 @@ impl WidgetGallery {
|
|||
}
|
||||
|
||||
fn example_plot() -> egui::plot::Plot {
|
||||
use egui::plot::{Line, Plot, Value, Values};
|
||||
use egui::plot::{Line, Value, Values};
|
||||
let n = 128;
|
||||
let line = Line::new(Values::from_values_iter((0..=n).map(|i| {
|
||||
use std::f64::consts::TAU;
|
||||
let x = egui::remap(i as f64, 0.0..=(n as f64), -TAU..=TAU);
|
||||
let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU);
|
||||
Value::new(x, x.sin())
|
||||
})));
|
||||
Plot::new("example_plot")
|
||||
egui::plot::Plot::new("example_plot")
|
||||
.line(line)
|
||||
.height(32.0)
|
||||
.data_aspect(1.0)
|
||||
|
|
|
@ -87,7 +87,6 @@ impl super::View for WindowOptions {
|
|||
anchor,
|
||||
anchor_offset,
|
||||
} = self;
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("title:");
|
||||
ui.text_edit_singleline(title);
|
||||
|
|
Loading…
Reference in a new issue