#![warn(missing_docs)]
use crate::style::resource::StyleResourceExt;
use crate::style::Style;
use crate::{
border::BorderBuilder,
core::{
algebra::Vector2, math::Rect, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
uuid_provider, variable::InheritableVariable, visitor::prelude::*,
},
define_constructor,
message::{ButtonState, KeyCode, MessageDirection, OsEvent, UiMessage},
widget::{Widget, WidgetBuilder, WidgetMessage},
BuildContext, Control, RestrictionEntry, Thickness, UiNode, UserInterface,
};
use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
use fyrox_graph::BaseSceneGraph;
use std::ops::{Deref, DerefMut};
#[derive(Debug, Clone, PartialEq)]
pub enum PopupMessage {
Open,
Close,
Content(Handle<UiNode>),
Placement(Placement),
AdjustPosition,
Owner(Handle<UiNode>),
RelayedMessage(UiMessage),
}
impl PopupMessage {
define_constructor!(
PopupMessage:Open => fn open(), layout: false
);
define_constructor!(
PopupMessage:Close => fn close(), layout: false
);
define_constructor!(
PopupMessage:Content => fn content(Handle<UiNode>), layout: false
);
define_constructor!(
PopupMessage:Placement => fn placement(Placement), layout: false
);
define_constructor!(
PopupMessage:AdjustPosition => fn adjust_position(), layout: true
);
define_constructor!(
PopupMessage:Owner => fn owner(Handle<UiNode>), layout: false
);
define_constructor!(
PopupMessage:RelayedMessage => fn relayed_message(UiMessage), layout: false
);
}
#[derive(Copy, Clone, PartialEq, Debug, Visit, Reflect)]
pub enum Placement {
LeftTop(Handle<UiNode>),
RightTop(Handle<UiNode>),
Center(Handle<UiNode>),
LeftBottom(Handle<UiNode>),
RightBottom(Handle<UiNode>),
Cursor(Handle<UiNode>),
Position {
position: Vector2<f32>,
target: Handle<UiNode>,
},
}
impl Default for Placement {
fn default() -> Self {
Self::LeftTop(Default::default())
}
}
impl Placement {
pub fn target(&self) -> Handle<UiNode> {
match self {
Placement::LeftTop(target)
| Placement::RightTop(target)
| Placement::Center(target)
| Placement::LeftBottom(target)
| Placement::RightBottom(target)
| Placement::Cursor(target)
| Placement::Position { target, .. } => *target,
}
}
}
#[derive(Default, Clone, Visit, Debug, Reflect, ComponentProvider)]
pub struct Popup {
pub widget: Widget,
pub placement: InheritableVariable<Placement>,
pub stays_open: InheritableVariable<bool>,
pub is_open: InheritableVariable<bool>,
pub content: InheritableVariable<Handle<UiNode>>,
pub body: InheritableVariable<Handle<UiNode>>,
pub smart_placement: InheritableVariable<bool>,
pub owner: Handle<UiNode>,
}
impl ConstructorProvider<UiNode, UserInterface> for Popup {
fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
GraphNodeConstructor::new::<Self>()
.with_variant("Popup", |ui| {
PopupBuilder::new(WidgetBuilder::new().with_name("Popup"))
.build(&mut ui.build_ctx())
.into()
})
.with_group("Layout")
}
}
crate::define_widget_deref!(Popup);
fn adjust_placement_position(
node_screen_bounds: Rect<f32>,
screen_size: Vector2<f32>,
) -> Vector2<f32> {
let mut new_position = node_screen_bounds.position;
let right_bottom = node_screen_bounds.right_bottom_corner();
if right_bottom.x > screen_size.x {
new_position.x -= right_bottom.x - screen_size.x;
}
if right_bottom.y > screen_size.y {
new_position.y -= right_bottom.y - screen_size.y;
}
new_position
}
impl Popup {
fn left_top_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
ui.try_get(target)
.map(|n| n.screen_position())
.unwrap_or_default()
}
fn right_top_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
ui.try_get(target)
.map(|n| n.screen_position() + Vector2::new(n.actual_global_size().x, 0.0))
.unwrap_or_else(|| {
Vector2::new(ui.screen_size().x - self.widget.actual_global_size().x, 0.0)
})
}
fn center_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
ui.try_get(target)
.map(|n| n.screen_position() + n.actual_global_size().scale(0.5))
.unwrap_or_else(|| (ui.screen_size - self.widget.actual_global_size()).scale(0.5))
}
fn left_bottom_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
ui.try_get(target)
.map(|n| n.screen_position() + Vector2::new(0.0, n.actual_global_size().y))
.unwrap_or_else(|| {
Vector2::new(0.0, ui.screen_size().y - self.widget.actual_global_size().y)
})
}
fn right_bottom_placement(&self, ui: &UserInterface, target: Handle<UiNode>) -> Vector2<f32> {
ui.try_get(target)
.map(|n| n.screen_position() + n.actual_global_size())
.unwrap_or_else(|| ui.screen_size - self.widget.actual_global_size())
}
}
uuid_provider!(Popup = "1c641540-59eb-4ccd-a090-2173dab02245");
impl Control for Popup {
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.widget.handle_routed_message(ui, message);
if let Some(msg) = message.data::<PopupMessage>() {
if message.destination() == self.handle() {
match msg {
PopupMessage::Open => {
if !*self.is_open {
self.is_open.set_value_and_mark_modified(true);
ui.send_message(WidgetMessage::visibility(
self.handle(),
MessageDirection::ToWidget,
true,
));
ui.push_picking_restriction(RestrictionEntry {
handle: self.handle(),
stop: false,
});
ui.send_message(WidgetMessage::topmost(
self.handle(),
MessageDirection::ToWidget,
));
let position = match *self.placement {
Placement::LeftTop(target) => self.left_top_placement(ui, target),
Placement::RightTop(target) => self.right_top_placement(ui, target),
Placement::Center(target) => self.center_placement(ui, target),
Placement::LeftBottom(target) => {
self.left_bottom_placement(ui, target)
}
Placement::RightBottom(target) => {
self.right_bottom_placement(ui, target)
}
Placement::Cursor(_) => ui.cursor_position(),
Placement::Position { position, .. } => position,
};
ui.send_message(WidgetMessage::desired_position(
self.handle(),
MessageDirection::ToWidget,
ui.screen_to_root_canvas_space(position),
));
ui.send_message(WidgetMessage::focus(
if self.content.is_some() {
*self.content
} else {
self.handle
},
MessageDirection::ToWidget,
));
if *self.smart_placement {
ui.send_message(PopupMessage::adjust_position(
self.handle,
MessageDirection::ToWidget,
));
}
}
}
PopupMessage::Close => {
if *self.is_open {
self.is_open.set_value_and_mark_modified(false);
ui.send_message(WidgetMessage::visibility(
self.handle(),
MessageDirection::ToWidget,
false,
));
ui.remove_picking_restriction(self.handle());
if let Some(top) = ui.top_picking_restriction() {
ui.send_message(WidgetMessage::focus(
top.handle,
MessageDirection::ToWidget,
));
}
if ui.captured_node() == self.handle() {
ui.release_mouse_capture();
}
}
}
PopupMessage::Content(content) => {
if self.content.is_some() {
ui.send_message(WidgetMessage::remove(
*self.content,
MessageDirection::ToWidget,
));
}
self.content.set_value_and_mark_modified(*content);
ui.send_message(WidgetMessage::link(
*self.content,
MessageDirection::ToWidget,
*self.body,
));
}
PopupMessage::Placement(placement) => {
self.placement.set_value_and_mark_modified(*placement);
self.invalidate_layout();
}
PopupMessage::AdjustPosition => {
let new_position =
adjust_placement_position(self.screen_bounds(), ui.screen_size());
if new_position != self.screen_position() {
ui.send_message(WidgetMessage::desired_position(
self.handle,
MessageDirection::ToWidget,
ui.screen_to_root_canvas_space(new_position),
));
}
}
PopupMessage::Owner(owner) => {
self.owner = *owner;
}
PopupMessage::RelayedMessage(_) => (),
}
}
} else if let Some(WidgetMessage::KeyDown(key)) = message.data() {
if !message.handled() && *key == KeyCode::Escape {
ui.send_message(PopupMessage::close(self.handle, MessageDirection::ToWidget));
message.set_handled(true);
}
}
if ui.is_valid_handle(self.owner) && !message.handled() {
ui.send_message(PopupMessage::relayed_message(
self.owner,
MessageDirection::ToWidget,
message.clone(),
));
}
}
fn handle_os_event(
&mut self,
self_handle: Handle<UiNode>,
ui: &mut UserInterface,
event: &OsEvent,
) {
if let OsEvent::MouseInput { state, .. } = event {
if let Some(top_restriction) = ui.top_picking_restriction() {
if *state == ButtonState::Pressed
&& top_restriction.handle == self_handle
&& *self.is_open
{
let pos = ui.cursor_position();
if !self.widget.screen_bounds().contains(pos) && !*self.stays_open {
ui.send_message(PopupMessage::close(
self.handle(),
MessageDirection::ToWidget,
));
}
}
}
}
}
}
pub struct PopupBuilder {
widget_builder: WidgetBuilder,
placement: Placement,
stays_open: bool,
content: Handle<UiNode>,
smart_placement: bool,
owner: Handle<UiNode>,
}
impl PopupBuilder {
pub fn new(widget_builder: WidgetBuilder) -> Self {
Self {
widget_builder,
placement: Placement::Cursor(Default::default()),
stays_open: false,
content: Default::default(),
smart_placement: true,
owner: Default::default(),
}
}
pub fn with_placement(mut self, placement: Placement) -> Self {
self.placement = placement;
self
}
pub fn with_smart_placement(mut self, smart_placement: bool) -> Self {
self.smart_placement = smart_placement;
self
}
pub fn stays_open(mut self, value: bool) -> Self {
self.stays_open = value;
self
}
pub fn with_content(mut self, content: Handle<UiNode>) -> Self {
self.content = content;
self
}
pub fn with_owner(mut self, owner: Handle<UiNode>) -> Self {
self.owner = owner;
self
}
pub fn build_popup(self, ctx: &mut BuildContext) -> Popup {
let style = &ctx.style;
let body = BorderBuilder::new(
WidgetBuilder::new()
.with_background(style.property(Style::BRUSH_PRIMARY))
.with_foreground(style.property(Style::BRUSH_DARKEST))
.with_child(self.content),
)
.with_stroke_thickness(Thickness::uniform(1.0).into())
.build(ctx);
Popup {
widget: self
.widget_builder
.with_child(body)
.with_visibility(false)
.with_handle_os_events(true)
.build(ctx),
placement: self.placement.into(),
stays_open: self.stays_open.into(),
is_open: false.into(),
content: self.content.into(),
smart_placement: self.smart_placement.into(),
body: body.into(),
owner: self.owner,
}
}
pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
let popup = self.build_popup(ctx);
ctx.add_node(UiNode::new(popup))
}
}
#[cfg(test)]
mod test {
use crate::popup::PopupBuilder;
use crate::{test::test_widget_deletion, widget::WidgetBuilder};
#[test]
fn test_deletion() {
test_widget_deletion(|ctx| PopupBuilder::new(WidgetBuilder::new()).build(ctx));
}
}