wgui: windowing: close_if_clicked_outside support, context menus
This commit is contained in:
11
Cargo.toml
11
Cargo.toml
@@ -5,6 +5,17 @@ strip = "none"
|
|||||||
debug-assertions = true
|
debug-assertions = true
|
||||||
incremental = true
|
incremental = true
|
||||||
|
|
||||||
|
# to be used in case if you don't want debug features
|
||||||
|
# (faster incremental compilation, about 15x smaller binary size compared to dev)
|
||||||
|
# --profile=plain
|
||||||
|
[profile.plain]
|
||||||
|
inherits = "dev"
|
||||||
|
opt-level = 1
|
||||||
|
debug = false
|
||||||
|
strip = true
|
||||||
|
debug-assertions = true
|
||||||
|
incremental = true
|
||||||
|
|
||||||
[profile.release-with-debug]
|
[profile.release-with-debug]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
debug = true
|
debug = true
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use wgui::{
|
|||||||
parser::{Fetchable, ParseDocumentParams, ParserState},
|
parser::{Fetchable, ParseDocumentParams, ParserState},
|
||||||
task::Tasks,
|
task::Tasks,
|
||||||
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
|
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
|
||||||
windowing::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
|
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra, WguiWindowPlacement},
|
||||||
};
|
};
|
||||||
use wlx_common::{audio, dash_interface::BoxDashInterface, timestep::Timestep};
|
use wlx_common::{audio, dash_interface::BoxDashInterface, timestep::Timestep};
|
||||||
|
|
||||||
@@ -379,13 +379,14 @@ impl<T: 'static> Frontend<T> {
|
|||||||
|
|
||||||
fn action_show_audio_settings(&mut self) -> anyhow::Result<()> {
|
fn action_show_audio_settings(&mut self) -> anyhow::Result<()> {
|
||||||
self.window_audio_settings.open(&mut WguiWindowParams {
|
self.window_audio_settings.open(&mut WguiWindowParams {
|
||||||
globals: self.globals.clone(),
|
globals: &self.globals,
|
||||||
position: Vec2::new(64.0, 64.0),
|
position: Vec2::new(64.0, 64.0),
|
||||||
layout: &mut self.layout,
|
layout: &mut self.layout,
|
||||||
title: Translation::from_translation_key("AUDIO.SETTINGS"),
|
|
||||||
extra: WguiWindowParamsExtra {
|
extra: WguiWindowParamsExtra {
|
||||||
fixed_width: Some(400.0),
|
fixed_width: Some(400.0),
|
||||||
placement: WguiWindowPlacement::BottomLeft,
|
placement: WguiWindowPlacement::BottomLeft,
|
||||||
|
close_if_clicked_outside: true,
|
||||||
|
title: Some(Translation::from_translation_key("AUDIO.SETTINGS")),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
<rectangle macro="rect">
|
<rectangle macro="rect">
|
||||||
<label id="label_current_option" text="Click any of these buttons" size="20" weight="bold" />
|
<label id="label_current_option" text="Click any of these buttons" size="20" weight="bold" />
|
||||||
<div gap="4">
|
<div gap="4">
|
||||||
<Button id="button_red" text="Red button" width="150" height="32" color="#FF0000" tooltip="I'm at the top" tooltip_side="top" />
|
<Button id="button_red" text="Red button" width="150" height="32" color="#FF0000" tooltip_str="I'm at the top" tooltip_side="top" />
|
||||||
<Button id="button_aqua" text="Aqua button" width="150" height="32" color="#00FFFF" tooltip="I'm at the bottom" tooltip_side="bottom" />
|
<Button id="button_aqua" text="Aqua button" width="150" height="32" color="#00FFFF" tooltip_str="I'm at the bottom" tooltip_side="bottom" />
|
||||||
<Button id="button_yellow" text="Yellow button" width="150" height="32" color="#FFFF00" tooltip="TESTBED.HELLO_WORLD" tooltip_side="right" />
|
<Button id="button_yellow" text="Yellow button" width="150" height="32" color="#FFFF00" tooltip="TESTBED.HELLO_WORLD" tooltip_side="right" />
|
||||||
</div>
|
</div>
|
||||||
<div gap="4">
|
<div gap="4">
|
||||||
@@ -50,6 +50,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</rectangle>
|
</rectangle>
|
||||||
|
|
||||||
|
<rectangle macro="rect">
|
||||||
|
<label text="Context menu test" />
|
||||||
|
<Button id="button_context_menu" text="Show context menu" />
|
||||||
|
</rectangle>
|
||||||
|
|
||||||
<rectangle macro="rect">
|
<rectangle macro="rect">
|
||||||
<label text="visibility test" weight="bold" />
|
<label text="visibility test" weight="bold" />
|
||||||
<CheckBox id="cb_visible" height="24" text="visible" />
|
<CheckBox id="cb_visible" height="24" text="visible" />
|
||||||
|
|||||||
@@ -19,14 +19,19 @@ use wgui::{
|
|||||||
i18n::Translation,
|
i18n::Translation,
|
||||||
layout::{Layout, LayoutParams, LayoutUpdateParams, Widget},
|
layout::{Layout, LayoutParams, LayoutUpdateParams, Widget},
|
||||||
parser::{Fetchable, ParseDocumentExtra, ParseDocumentParams, ParserState},
|
parser::{Fetchable, ParseDocumentExtra, ParseDocumentParams, ParserState},
|
||||||
taffy,
|
taffy::{self, prelude::length},
|
||||||
task::Tasks,
|
task::Tasks,
|
||||||
widget::{label::WidgetLabel, rectangle::WidgetRectangle},
|
widget::{div::WidgetDiv, label::WidgetLabel, rectangle::WidgetRectangle},
|
||||||
windowing::{WguiWindow, WguiWindowParams},
|
windowing::{
|
||||||
|
context_menu,
|
||||||
|
window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum TestbedTask {
|
pub enum TestbedTask {
|
||||||
ShowPopup,
|
ShowPopup,
|
||||||
|
ShowContextMenu(Vec2),
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Data {
|
struct Data {
|
||||||
@@ -34,6 +39,7 @@ struct Data {
|
|||||||
state: ParserState,
|
state: ParserState,
|
||||||
|
|
||||||
popup_window: WguiWindow,
|
popup_window: WguiWindow,
|
||||||
|
context_menu: context_menu::ContextMenu,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TestbedGeneric {
|
pub struct TestbedGeneric {
|
||||||
@@ -138,6 +144,7 @@ impl TestbedGeneric {
|
|||||||
|
|
||||||
let label_cur_option = state.fetch_widget(&layout.state, "label_current_option")?;
|
let label_cur_option = state.fetch_widget(&layout.state, "label_current_option")?;
|
||||||
|
|
||||||
|
let button_context_menu = state.fetch_component_as::<ComponentButton>("button_context_menu")?;
|
||||||
let button_click_me = state.fetch_component_as::<ComponentButton>("button_click_me")?;
|
let button_click_me = state.fetch_component_as::<ComponentButton>("button_click_me")?;
|
||||||
let button = button_click_me.clone();
|
let button = button_click_me.clone();
|
||||||
button_click_me.on_click(Box::new(move |common, _e| {
|
button_click_me.on_click(Box::new(move |common, _e| {
|
||||||
@@ -174,6 +181,7 @@ impl TestbedGeneric {
|
|||||||
data: Rc::new(RefCell::new(Data {
|
data: Rc::new(RefCell::new(Data {
|
||||||
state,
|
state,
|
||||||
popup_window: WguiWindow::default(),
|
popup_window: WguiWindow::default(),
|
||||||
|
context_menu: context_menu::ContextMenu::default(),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -185,6 +193,14 @@ impl TestbedGeneric {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
button_context_menu.on_click({
|
||||||
|
let tasks = testbed.tasks.clone();
|
||||||
|
Box::new(move |_common, m| {
|
||||||
|
tasks.push(TestbedTask::ShowContextMenu(m.mouse_pos_absolute.unwrap()));
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
Ok(testbed)
|
Ok(testbed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +212,7 @@ impl TestbedGeneric {
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
match task {
|
match task {
|
||||||
TestbedTask::ShowPopup => self.show_popup(params, data)?,
|
TestbedTask::ShowPopup => self.show_popup(params, data)?,
|
||||||
|
TestbedTask::ShowContextMenu(position) => self.show_context_menu(params, data, *position)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -207,11 +224,57 @@ impl TestbedGeneric {
|
|||||||
data: &mut Data,
|
data: &mut Data,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
data.popup_window.open(&mut WguiWindowParams {
|
data.popup_window.open(&mut WguiWindowParams {
|
||||||
globals: self.globals.clone(),
|
globals: &self.globals,
|
||||||
position: Vec2::new(128.0, 128.0),
|
position: Vec2::new(128.0, 128.0),
|
||||||
layout: &mut self.layout,
|
layout: &mut self.layout,
|
||||||
title: Translation::from_raw_text("foo"),
|
extra: WguiWindowParamsExtra {
|
||||||
extra: Default::default(),
|
title: Some(Translation::from_raw_text("foo")),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.layout.add_child(
|
||||||
|
data.popup_window.get_content().id,
|
||||||
|
WidgetDiv::create(),
|
||||||
|
taffy::Style {
|
||||||
|
size: taffy::Size {
|
||||||
|
width: length(128.0),
|
||||||
|
height: length(64.0),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_context_menu(
|
||||||
|
&mut self,
|
||||||
|
_params: &mut TestbedUpdateParams,
|
||||||
|
data: &mut Data,
|
||||||
|
position: Vec2,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
data.context_menu.open(&mut context_menu::OpenParams {
|
||||||
|
globals: &self.globals,
|
||||||
|
layout: &mut self.layout,
|
||||||
|
position,
|
||||||
|
on_action: Rc::new(move |action| {
|
||||||
|
log::info!("got action: {}", action.name);
|
||||||
|
}),
|
||||||
|
cells: vec![
|
||||||
|
context_menu::Cell {
|
||||||
|
title: Translation::from_raw_text("Options"),
|
||||||
|
action_name: "options".into(),
|
||||||
|
},
|
||||||
|
context_menu::Cell {
|
||||||
|
title: Translation::from_raw_text("Exit software"),
|
||||||
|
action_name: "exit".into(),
|
||||||
|
},
|
||||||
|
context_menu::Cell {
|
||||||
|
title: Translation::from_raw_text("Restart software"),
|
||||||
|
action_name: "restart".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
16
wgui/assets/wgui/context_menu.xml
Normal file
16
wgui/assets/wgui/context_menu.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<layout>
|
||||||
|
<!-- text: str -->
|
||||||
|
<template name="Cell">
|
||||||
|
<Button id="button" text="${text}" weight="bold" border="0" color="#FFFFFF00" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template name="Separator">
|
||||||
|
<rectangle width="100%" height="2" color="~color_accent" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<elements>
|
||||||
|
<rectangle consume_mouse_events="1" color="#000000fa" round="8" border="2" border_color="~color_accent" gap="4" padding="8" id="buttons" flex_direction="column">
|
||||||
|
|
||||||
|
</rectangle>
|
||||||
|
</elements>
|
||||||
|
</layout>
|
||||||
@@ -19,7 +19,7 @@ use crate::{
|
|||||||
util::WLength,
|
util::WLength,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use glam::{Mat4, Vec3};
|
use glam::{Mat4, Vec2, Vec3};
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
@@ -66,7 +66,9 @@ impl Default for Params<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ButtonClickEvent {}
|
pub struct ButtonClickEvent {
|
||||||
|
pub mouse_pos_absolute: Option<Vec2>,
|
||||||
|
}
|
||||||
pub type ButtonClickCallback = Box<dyn Fn(&mut CallbackDataCommon, ButtonClickEvent) -> anyhow::Result<()>>;
|
pub type ButtonClickCallback = Box<dyn Fn(&mut CallbackDataCommon, ButtonClickEvent) -> anyhow::Result<()>>;
|
||||||
|
|
||||||
pub struct Colors {
|
pub struct Colors {
|
||||||
@@ -370,9 +372,7 @@ fn register_event_mouse_release(
|
|||||||
|
|
||||||
if state.down {
|
if state.down {
|
||||||
state.down = false;
|
state.down = false;
|
||||||
if state.hovered
|
if state.hovered {
|
||||||
&& let Some(on_click) = &state.on_click
|
|
||||||
{
|
|
||||||
anim_hover(
|
anim_hover(
|
||||||
rect,
|
rect,
|
||||||
event_data.widget_data,
|
event_data.widget_data,
|
||||||
@@ -383,8 +383,16 @@ fn register_event_mouse_release(
|
|||||||
state.sticky_down,
|
state.sticky_down,
|
||||||
);
|
);
|
||||||
|
|
||||||
on_click(common, ButtonClickEvent {})?;
|
if let Some(on_click) = &state.on_click {
|
||||||
|
on_click(
|
||||||
|
common,
|
||||||
|
ButtonClickEvent {
|
||||||
|
mouse_pos_absolute: event_data.metadata.get_mouse_pos_absolute(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(EventResult::Consumed)
|
Ok(EventResult::Consumed)
|
||||||
} else {
|
} else {
|
||||||
Ok(EventResult::Pass)
|
Ok(EventResult::Pass)
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
use taffy::Style;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{Component, ComponentBase, ComponentTrait, RefreshData, checkbox::ComponentCheckbox},
|
components::{Component, ComponentBase, ComponentTrait, RefreshData, checkbox::ComponentCheckbox},
|
||||||
event::CallbackDataCommon,
|
event::CallbackDataCommon,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ pub fn parse_component_button<'a>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (widget, component) = button::construct(
|
let (widget, button) = button::construct(
|
||||||
&mut ctx.get_construct_essentials(parent_id),
|
&mut ctx.get_construct_essentials(parent_id),
|
||||||
button::Params {
|
button::Params {
|
||||||
color,
|
color,
|
||||||
@@ -124,7 +124,7 @@ pub fn parse_component_button<'a>(
|
|||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
process_component(ctx, Component(component), widget.id, attribs);
|
process_component(ctx, Component(button), widget.id, attribs);
|
||||||
parse_children(file, ctx, node, widget.id)?;
|
parse_children(file, ctx, node, widget.id)?;
|
||||||
|
|
||||||
Ok(widget.id)
|
Ok(widget.id)
|
||||||
|
|||||||
@@ -809,7 +809,7 @@ fn parse_tag_macro(file: &ParserFile, ctx: &mut ParserContext, node: roxmltree::
|
|||||||
}
|
}
|
||||||
|
|
||||||
let Some(name) = macro_name else {
|
let Some(name) = macro_name else {
|
||||||
log::error!("Template name not specified, ignoring");
|
log::error!("Macro name not specified, ignoring");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1160,7 +1160,6 @@ fn parse_document_root(
|
|||||||
.ok_or_else(|| anyhow::anyhow!("layout node not found"))?;
|
.ok_or_else(|| anyhow::anyhow!("layout node not found"))?;
|
||||||
|
|
||||||
for child_node in node_layout.children() {
|
for child_node in node_layout.children() {
|
||||||
#[allow(clippy::single_match)]
|
|
||||||
match child_node.tag_name().name() {
|
match child_node.tag_name().name() {
|
||||||
/* topmost include directly in <layout> */
|
/* topmost include directly in <layout> */
|
||||||
"include" => parse_tag_include(file, ctx, parent_id, &raw_attribs(&child_node))?,
|
"include" => parse_tag_include(file, ctx, parent_id, &raw_attribs(&child_node))?,
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
use std::{cell::RefCell, rc::Rc};
|
|
||||||
|
|
||||||
use glam::Vec2;
|
|
||||||
use taffy::prelude::{length, percent};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
assets::AssetPath,
|
|
||||||
components::button::ComponentButton,
|
|
||||||
event::StyleSetRequest,
|
|
||||||
globals::WguiGlobals,
|
|
||||||
i18n::Translation,
|
|
||||||
layout::{Layout, LayoutTask, LayoutTasks, WidgetPair},
|
|
||||||
parser::{self, Fetchable, ParserState},
|
|
||||||
widget::{div::WidgetDiv, label::WidgetLabel},
|
|
||||||
};
|
|
||||||
|
|
||||||
struct OpenedWindow {
|
|
||||||
layout_tasks: LayoutTasks,
|
|
||||||
widget: WidgetPair,
|
|
||||||
content: WidgetPair,
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
state: ParserState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for OpenedWindow {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.layout_tasks.push(LayoutTask::RemoveWidget(self.widget.id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct State {
|
|
||||||
opened_window: Option<OpenedWindow>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct WguiWindow(Rc<RefCell<State>>);
|
|
||||||
|
|
||||||
pub struct OnContentData {
|
|
||||||
pub widget: WidgetPair,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub enum WguiWindowPlacement {
|
|
||||||
#[default]
|
|
||||||
TopLeft,
|
|
||||||
BottomLeft,
|
|
||||||
TopRight,
|
|
||||||
BottomRight,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct WguiWindowParamsExtra {
|
|
||||||
pub fixed_width: Option<f32>,
|
|
||||||
pub fixed_height: Option<f32>,
|
|
||||||
pub placement: WguiWindowPlacement,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct WguiWindowParams<'a> {
|
|
||||||
pub position: Vec2,
|
|
||||||
pub globals: WguiGlobals,
|
|
||||||
pub layout: &'a mut Layout,
|
|
||||||
pub title: Translation,
|
|
||||||
pub extra: WguiWindowParamsExtra,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for WguiWindow {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self(Rc::new(RefCell::new(State { opened_window: None })))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const WINDOW_DECORATION_HEADER_HEIGHT: f32 = 32.0;
|
|
||||||
const WINDOW_DECORATION_PADDING: f32 = 2.0;
|
|
||||||
|
|
||||||
impl WguiWindow {
|
|
||||||
pub fn close(&self) {
|
|
||||||
self.0.borrow_mut().opened_window = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn open(&mut self, params: &mut WguiWindowParams) -> anyhow::Result<()> {
|
|
||||||
// close previous one if it's already open
|
|
||||||
self.close();
|
|
||||||
|
|
||||||
const XML_PATH: AssetPath = AssetPath::WguiInternal("wgui/window_frame.xml");
|
|
||||||
|
|
||||||
let (padding, justify_content, align_items) = match params.extra.placement {
|
|
||||||
WguiWindowPlacement::TopLeft => (
|
|
||||||
taffy::Rect {
|
|
||||||
left: length(params.position.x - WINDOW_DECORATION_PADDING),
|
|
||||||
top: length(params.position.y - WINDOW_DECORATION_HEADER_HEIGHT - WINDOW_DECORATION_PADDING),
|
|
||||||
bottom: length(0.0),
|
|
||||||
right: length(0.0),
|
|
||||||
},
|
|
||||||
taffy::JustifyContent::Start, // x start
|
|
||||||
taffy::AlignItems::Start, // y start
|
|
||||||
),
|
|
||||||
WguiWindowPlacement::BottomLeft => (
|
|
||||||
taffy::Rect {
|
|
||||||
left: length(params.position.x - WINDOW_DECORATION_PADDING),
|
|
||||||
top: length(0.0),
|
|
||||||
bottom: length(params.position.y - WINDOW_DECORATION_PADDING),
|
|
||||||
right: length(0.0),
|
|
||||||
},
|
|
||||||
taffy::JustifyContent::Start, // x start
|
|
||||||
taffy::AlignItems::End, // y end
|
|
||||||
),
|
|
||||||
WguiWindowPlacement::TopRight => (
|
|
||||||
taffy::Rect {
|
|
||||||
left: length(0.0),
|
|
||||||
top: length(params.position.y - WINDOW_DECORATION_HEADER_HEIGHT - WINDOW_DECORATION_PADDING),
|
|
||||||
bottom: length(0.0),
|
|
||||||
right: length(params.position.x - WINDOW_DECORATION_PADDING),
|
|
||||||
},
|
|
||||||
taffy::JustifyContent::End, // x end
|
|
||||||
taffy::AlignItems::Start, // y start
|
|
||||||
),
|
|
||||||
WguiWindowPlacement::BottomRight => (
|
|
||||||
taffy::Rect {
|
|
||||||
left: length(0.0),
|
|
||||||
top: length(0.0),
|
|
||||||
bottom: length(params.position.y - WINDOW_DECORATION_PADDING),
|
|
||||||
right: length(params.position.x - WINDOW_DECORATION_PADDING),
|
|
||||||
},
|
|
||||||
taffy::JustifyContent::End, // x end
|
|
||||||
taffy::AlignItems::End, // y end
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let (widget, _) = params.layout.add_topmost_child(
|
|
||||||
WidgetDiv::create(),
|
|
||||||
taffy::Style {
|
|
||||||
position: taffy::Position::Absolute,
|
|
||||||
align_items: Some(align_items),
|
|
||||||
justify_content: Some(justify_content),
|
|
||||||
padding,
|
|
||||||
size: taffy::Size {
|
|
||||||
width: percent(1.0),
|
|
||||||
height: percent(1.0),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let state = parser::parse_from_assets(
|
|
||||||
&parser::ParseDocumentParams {
|
|
||||||
globals: params.globals.clone(),
|
|
||||||
path: XML_PATH,
|
|
||||||
extra: Default::default(),
|
|
||||||
},
|
|
||||||
params.layout,
|
|
||||||
widget.id,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let but_close = state.fetch_component_as::<ComponentButton>("but_close").unwrap();
|
|
||||||
but_close.on_click({
|
|
||||||
let this = self.clone();
|
|
||||||
Box::new(move |_common, _e| {
|
|
||||||
this.close();
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut text_title = state.fetch_widget_as::<WidgetLabel>(¶ms.layout.state, "text_window_title")?;
|
|
||||||
text_title.set_text_simple(&mut params.globals.get(), params.title.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = state.fetch_widget(¶ms.layout.state, "content")?;
|
|
||||||
|
|
||||||
self.0.borrow_mut().opened_window = Some(OpenedWindow {
|
|
||||||
widget,
|
|
||||||
state,
|
|
||||||
layout_tasks: params.layout.tasks.clone(),
|
|
||||||
content: content.clone(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut c = params.layout.start_common();
|
|
||||||
if let Some(width) = params.extra.fixed_width {
|
|
||||||
c.common()
|
|
||||||
.alterables
|
|
||||||
.set_style(content.id, StyleSetRequest::Width(length(width)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(height) = params.extra.fixed_height {
|
|
||||||
c.common()
|
|
||||||
.alterables
|
|
||||||
.set_style(content.id, StyleSetRequest::Height(length(height)));
|
|
||||||
}
|
|
||||||
|
|
||||||
c.finish()?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_content(&self) -> WidgetPair {
|
|
||||||
let state = self.0.borrow_mut();
|
|
||||||
state.opened_window.as_ref().unwrap().content.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
114
wgui/src/windowing/context_menu.rs
Normal file
114
wgui/src/windowing/context_menu.rs
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
use std::{collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
|
use glam::Vec2;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
assets::AssetPath,
|
||||||
|
components::button::ComponentButton,
|
||||||
|
event::CallbackDataCommon,
|
||||||
|
globals::WguiGlobals,
|
||||||
|
i18n::Translation,
|
||||||
|
layout::Layout,
|
||||||
|
parser::{self, Fetchable},
|
||||||
|
windowing::window::{WguiWindow, WguiWindowParams, WguiWindowParamsExtra},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct Cell {
|
||||||
|
pub title: Translation,
|
||||||
|
pub action_name: Rc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContextMenuAction<'a> {
|
||||||
|
pub common: &'a mut CallbackDataCommon<'a>,
|
||||||
|
pub name: Rc<str>, // action name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OpenParams<'a> {
|
||||||
|
pub position: Vec2,
|
||||||
|
pub globals: &'a WguiGlobals,
|
||||||
|
pub layout: &'a mut Layout,
|
||||||
|
pub on_action: Rc<dyn Fn(ContextMenuAction)>,
|
||||||
|
pub cells: Vec<Cell>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ContextMenu {
|
||||||
|
window: WguiWindow,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn doc_params<'a>(globals: WguiGlobals) -> parser::ParseDocumentParams<'a> {
|
||||||
|
parser::ParseDocumentParams {
|
||||||
|
globals,
|
||||||
|
path: AssetPath::WguiInternal("wgui/context_menu.xml"),
|
||||||
|
extra: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextMenu {
|
||||||
|
pub fn open(&mut self, params: &mut OpenParams) -> anyhow::Result<()> {
|
||||||
|
self.window.open(&mut WguiWindowParams {
|
||||||
|
globals: params.globals,
|
||||||
|
layout: params.layout,
|
||||||
|
position: params.position,
|
||||||
|
extra: WguiWindowParamsExtra {
|
||||||
|
with_decorations: false,
|
||||||
|
close_if_clicked_outside: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let content = self.window.get_content();
|
||||||
|
|
||||||
|
let mut state = parser::parse_from_assets(&doc_params(params.globals.clone()), params.layout, content.id)?;
|
||||||
|
|
||||||
|
let id_buttons = state.get_widget_id("buttons")?;
|
||||||
|
|
||||||
|
for (idx, cell) in params.cells.iter().enumerate() {
|
||||||
|
let mut par = HashMap::new();
|
||||||
|
par.insert(Rc::from("text"), cell.title.generate(&mut params.globals.i18n()));
|
||||||
|
let data_cell = state.parse_template(
|
||||||
|
&doc_params(params.globals.clone()),
|
||||||
|
"Cell",
|
||||||
|
params.layout,
|
||||||
|
id_buttons,
|
||||||
|
par,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let button = data_cell.fetch_component_as::<ComponentButton>("button")?;
|
||||||
|
button.on_click({
|
||||||
|
let on_action = params.on_action.clone();
|
||||||
|
let name = cell.action_name.clone();
|
||||||
|
let window = self.window.clone();
|
||||||
|
Box::new(move |common, _| {
|
||||||
|
(*on_action)(ContextMenuAction {
|
||||||
|
name: name.clone(),
|
||||||
|
// FIXME: why i can't just provide this as-is!?
|
||||||
|
/* common: common, */
|
||||||
|
common: &mut CallbackDataCommon {
|
||||||
|
alterables: common.alterables,
|
||||||
|
state: common.state,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.close();
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if idx < params.cells.len() - 1 {
|
||||||
|
state.parse_template(
|
||||||
|
&doc_params(params.globals.clone()),
|
||||||
|
"Separator",
|
||||||
|
params.layout,
|
||||||
|
id_buttons,
|
||||||
|
Default::default(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(&self) {
|
||||||
|
self.window.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
2
wgui/src/windowing/mod.rs
Normal file
2
wgui/src/windowing/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod context_menu;
|
||||||
|
pub mod window;
|
||||||
301
wgui/src/windowing/window.rs
Normal file
301
wgui/src/windowing/window.rs
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use glam::Vec2;
|
||||||
|
use taffy::prelude::{length, percent};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
animation::{Animation, AnimationEasing},
|
||||||
|
assets::AssetPath,
|
||||||
|
components::button::ComponentButton,
|
||||||
|
drawing,
|
||||||
|
event::{EventListenerKind, StyleSetRequest},
|
||||||
|
globals::WguiGlobals,
|
||||||
|
i18n::Translation,
|
||||||
|
layout::{Layout, LayoutTask, LayoutTasks, WidgetPair},
|
||||||
|
parser::{self, Fetchable, ParserState},
|
||||||
|
widget::{
|
||||||
|
EventResult,
|
||||||
|
div::WidgetDiv,
|
||||||
|
label::WidgetLabel,
|
||||||
|
rectangle::{WidgetRectangle, WidgetRectangleParams},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OpenedWindow {
|
||||||
|
layout_tasks: LayoutTasks,
|
||||||
|
widget: WidgetPair,
|
||||||
|
input_grabber: Option<WidgetPair>,
|
||||||
|
content: WidgetPair,
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
state: Option<ParserState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for OpenedWindow {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.layout_tasks.push(LayoutTask::RemoveWidget(self.widget.id));
|
||||||
|
if let Some(grabber) = &self.input_grabber {
|
||||||
|
self.layout_tasks.push(LayoutTask::RemoveWidget(grabber.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
opened_window: Option<OpenedWindow>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WguiWindow(Rc<RefCell<State>>);
|
||||||
|
|
||||||
|
pub struct OnContentData {
|
||||||
|
pub widget: WidgetPair,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub enum WguiWindowPlacement {
|
||||||
|
#[default]
|
||||||
|
TopLeft,
|
||||||
|
BottomLeft,
|
||||||
|
TopRight,
|
||||||
|
BottomRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WguiWindowParamsExtra {
|
||||||
|
pub fixed_width: Option<f32>,
|
||||||
|
pub fixed_height: Option<f32>,
|
||||||
|
pub placement: WguiWindowPlacement,
|
||||||
|
pub with_decorations: bool,
|
||||||
|
pub no_decoration_padding: f32,
|
||||||
|
pub close_if_clicked_outside: bool,
|
||||||
|
pub title: Option<Translation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WguiWindowParamsExtra {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
fixed_width: None,
|
||||||
|
fixed_height: None,
|
||||||
|
title: None,
|
||||||
|
placement: WguiWindowPlacement::TopLeft,
|
||||||
|
no_decoration_padding: 0.0,
|
||||||
|
close_if_clicked_outside: false,
|
||||||
|
with_decorations: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WguiWindowParams<'a> {
|
||||||
|
pub position: Vec2,
|
||||||
|
pub globals: &'a WguiGlobals,
|
||||||
|
pub layout: &'a mut Layout,
|
||||||
|
pub extra: WguiWindowParamsExtra,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WguiWindow {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Rc::new(RefCell::new(State { opened_window: None })))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const WINDOW_DECORATION_PADDING: f32 = 2.0;
|
||||||
|
const WINDOW_DECORATION_HEADER_HEIGHT: f32 = 32.0;
|
||||||
|
|
||||||
|
impl WguiWindow {
|
||||||
|
pub fn close(&self) {
|
||||||
|
self.0.borrow_mut().opened_window = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
pub fn open(&mut self, params: &mut WguiWindowParams) -> anyhow::Result<()> {
|
||||||
|
// close previous one if it's already open
|
||||||
|
self.close();
|
||||||
|
|
||||||
|
let header_height = if params.extra.with_decorations {
|
||||||
|
WINDOW_DECORATION_HEADER_HEIGHT
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let window_padding = if params.extra.with_decorations {
|
||||||
|
WINDOW_DECORATION_PADDING
|
||||||
|
} else {
|
||||||
|
params.extra.no_decoration_padding
|
||||||
|
};
|
||||||
|
|
||||||
|
let (padding, justify_content, align_items) = match params.extra.placement {
|
||||||
|
WguiWindowPlacement::TopLeft => (
|
||||||
|
taffy::Rect {
|
||||||
|
left: length(params.position.x - window_padding),
|
||||||
|
top: length(params.position.y - header_height - window_padding),
|
||||||
|
bottom: length(0.0),
|
||||||
|
right: length(0.0),
|
||||||
|
},
|
||||||
|
taffy::JustifyContent::Start, // x start
|
||||||
|
taffy::AlignItems::Start, // y start
|
||||||
|
),
|
||||||
|
WguiWindowPlacement::BottomLeft => (
|
||||||
|
taffy::Rect {
|
||||||
|
left: length(params.position.x - window_padding),
|
||||||
|
top: length(0.0),
|
||||||
|
bottom: length(params.position.y - window_padding),
|
||||||
|
right: length(0.0),
|
||||||
|
},
|
||||||
|
taffy::JustifyContent::Start, // x start
|
||||||
|
taffy::AlignItems::End, // y end
|
||||||
|
),
|
||||||
|
WguiWindowPlacement::TopRight => (
|
||||||
|
taffy::Rect {
|
||||||
|
left: length(0.0),
|
||||||
|
top: length(params.position.y - header_height - window_padding),
|
||||||
|
bottom: length(0.0),
|
||||||
|
right: length(params.position.x - window_padding),
|
||||||
|
},
|
||||||
|
taffy::JustifyContent::End, // x end
|
||||||
|
taffy::AlignItems::Start, // y start
|
||||||
|
),
|
||||||
|
WguiWindowPlacement::BottomRight => (
|
||||||
|
taffy::Rect {
|
||||||
|
left: length(0.0),
|
||||||
|
top: length(0.0),
|
||||||
|
bottom: length(params.position.y - window_padding),
|
||||||
|
right: length(params.position.x - window_padding),
|
||||||
|
},
|
||||||
|
taffy::JustifyContent::End, // x end
|
||||||
|
taffy::AlignItems::End, // y end
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let input_grabber = if params.extra.close_if_clicked_outside {
|
||||||
|
let mut rect = WidgetRectangle::create(Default::default());
|
||||||
|
rect.flags.consume_mouse_events = true;
|
||||||
|
|
||||||
|
let this = self.clone();
|
||||||
|
rect.event_listeners.register(
|
||||||
|
EventListenerKind::MousePress,
|
||||||
|
Box::new(move |_, _, (), ()| {
|
||||||
|
this.close();
|
||||||
|
Ok(EventResult::Consumed)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let (widget, _) = params.layout.add_topmost_child(
|
||||||
|
rect,
|
||||||
|
taffy::Style {
|
||||||
|
position: taffy::Position::Absolute,
|
||||||
|
size: taffy::Size {
|
||||||
|
width: percent(1.0),
|
||||||
|
height: percent(1.0),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Fade animation
|
||||||
|
params.layout.animations.add(Animation::new(
|
||||||
|
widget.id,
|
||||||
|
20,
|
||||||
|
AnimationEasing::OutQuad,
|
||||||
|
Box::new(|common, data| {
|
||||||
|
let rect = data.obj.get_as_mut::<WidgetRectangle>().unwrap() /* should always succeed */;
|
||||||
|
rect.params.color = drawing::Color::new(0.0, 0.0, 0.0, data.pos * 0.3);
|
||||||
|
common.alterables.mark_redraw();
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
Some(widget)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let (widget, _) = params.layout.add_topmost_child(
|
||||||
|
WidgetDiv::create(),
|
||||||
|
taffy::Style {
|
||||||
|
position: taffy::Position::Absolute,
|
||||||
|
align_items: Some(align_items),
|
||||||
|
justify_content: Some(justify_content),
|
||||||
|
padding,
|
||||||
|
size: taffy::Size {
|
||||||
|
width: percent(1.0),
|
||||||
|
height: percent(1.0),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let content_id = if params.extra.with_decorations {
|
||||||
|
let xml_path: AssetPath = AssetPath::WguiInternal("wgui/window_frame.xml");
|
||||||
|
|
||||||
|
let state = parser::parse_from_assets(
|
||||||
|
&parser::ParseDocumentParams {
|
||||||
|
globals: params.globals.clone(),
|
||||||
|
path: xml_path,
|
||||||
|
extra: Default::default(),
|
||||||
|
},
|
||||||
|
params.layout,
|
||||||
|
widget.id,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let but_close = state.fetch_component_as::<ComponentButton>("but_close").unwrap();
|
||||||
|
but_close.on_click({
|
||||||
|
let this = self.clone();
|
||||||
|
Box::new(move |_common, _e| {
|
||||||
|
this.close();
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(title) = ¶ms.extra.title {
|
||||||
|
let mut text_title = state.fetch_widget_as::<WidgetLabel>(¶ms.layout.state, "text_window_title")?;
|
||||||
|
text_title.set_text_simple(&mut params.globals.get(), title.clone());
|
||||||
|
}
|
||||||
|
let content = state.fetch_widget(¶ms.layout.state, "content")?;
|
||||||
|
|
||||||
|
self.0.borrow_mut().opened_window = Some(OpenedWindow {
|
||||||
|
widget,
|
||||||
|
state: Some(state),
|
||||||
|
layout_tasks: params.layout.tasks.clone(),
|
||||||
|
content: content.clone(),
|
||||||
|
input_grabber,
|
||||||
|
});
|
||||||
|
|
||||||
|
content.id
|
||||||
|
} else {
|
||||||
|
// without decorations
|
||||||
|
let (content, _) = params
|
||||||
|
.layout
|
||||||
|
.add_child(widget.id, WidgetDiv::create(), Default::default())?;
|
||||||
|
|
||||||
|
self.0.borrow_mut().opened_window = Some(OpenedWindow {
|
||||||
|
widget,
|
||||||
|
state: None,
|
||||||
|
layout_tasks: params.layout.tasks.clone(),
|
||||||
|
content: content.clone(),
|
||||||
|
input_grabber,
|
||||||
|
});
|
||||||
|
|
||||||
|
content.id
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut c = params.layout.start_common();
|
||||||
|
if let Some(width) = params.extra.fixed_width {
|
||||||
|
c.common()
|
||||||
|
.alterables
|
||||||
|
.set_style(content_id, StyleSetRequest::Width(length(width)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(height) = params.extra.fixed_height {
|
||||||
|
c.common()
|
||||||
|
.alterables
|
||||||
|
.set_style(content_id, StyleSetRequest::Height(length(height)));
|
||||||
|
}
|
||||||
|
|
||||||
|
c.finish()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_content(&self) -> WidgetPair {
|
||||||
|
let state = self.0.borrow_mut();
|
||||||
|
state.opened_window.as_ref().unwrap().content.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user