use bevy_app::{Plugin, PreStartup, Update};
use bevy_ecs::{
component::Component,
entity::Entity,
event::EntityEvent,
hierarchy::ChildOf,
lifecycle::{Add, HookContext},
observer::On,
query::{Changed, Or, QueryData, With, Without},
resource::Resource,
schedule::IntoScheduleConfigs,
system::{Commands, Query, Res},
world::World,
};
use bevy_log::error;
use bevy_picking::{
events::{Pointer, Press},
pointer::PointerButton,
};
use bevy_text::TextLayoutInfo;
use bevy_ui::{ComputedNode, RelativeCursorPosition, widget::Text};
use bevy_window::Window;
use tiny_bail::prelude::*;
use crate::{TooltipHighlightLink, TooltipTermLink, TooltipTermLinkRecursive, TooltipsNested};
pub(crate) struct TextObservePlugin;
impl Plugin for TextObservePlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_systems(PreStartup, setup_component_hooks)
.add_systems(Update, tooltip_links.run_if(mouse_moved))
.add_observer(term_link_textspan_parent)
.add_observer(recursive_term_link_textspan_parent)
.add_observer(highlight_link_textspan_parent);
}
}
#[derive(Resource, Clone, Copy)]
pub(crate) struct WasHoveringText {
pub(crate) entity: Entity,
pub(crate) relative_cursor_entity: Entity,
}
#[derive(Debug, EntityEvent)]
pub struct TextHoveredOver {
pub(crate) entity: Entity,
}
#[derive(Debug, EntityEvent)]
pub struct TextHoveredOut {
pub(crate) entity: Entity,
}
#[derive(Debug, EntityEvent)]
pub(crate) struct TextMiddlePress {
pub(crate) entity: Entity,
}
#[derive(Component, Debug)]
#[require(RelativeCursorPosition)]
pub struct ToolTipListenTextSpan;
fn setup_component_hooks(world: &mut World) {
world
.register_component_hooks::<ToolTipListenTextSpan>()
.on_insert(|mut world, HookContext { entity, .. }| {
world.commands().entity(entity).observe(middle_mouse_links);
});
}
pub(crate) fn highlight_link_textspan_parent(
add: On<Add, TooltipHighlightLink>,
text_query: Query<Entity, With<Text>>,
ancestor_query: Query<&ChildOf>,
mut commands: Commands,
) {
let add_entity = add.entity;
if text_query.contains(add_entity) {
r!(commands.get_entity(add_entity)).insert(RelativeCursorPosition::default());
return;
}
for entity in ancestor_query.iter_ancestors(add_entity) {
if text_query.contains(entity) {
r!(commands.get_entity(entity)).insert(ToolTipListenTextSpan);
return;
}
}
error!(
"{add_entity} has no text component nor any ancestors with a text componet.\nText needs to be inserted concurrently or before any links.\nThis limitation will be removed when textspans support observers"
);
}
pub(crate) fn term_link_textspan_parent(
add: On<Add, TooltipTermLink>,
text_query: Query<Entity, With<Text>>,
ancestor_query: Query<&ChildOf>,
mut commands: Commands,
) {
let add_entity = add.entity;
if text_query.contains(add_entity) {
r!(commands.get_entity(add_entity)).insert(RelativeCursorPosition::default());
return;
}
for entity in ancestor_query.iter_ancestors(add_entity) {
if text_query.contains(entity) {
r!(commands.get_entity(entity)).insert(ToolTipListenTextSpan);
return;
}
}
error!(
"{add_entity} has no text component nor any ancestors with a text componet.\nText needs to be inserted concurrently or before any links.\nThis limitation will be removed when textspans support observers"
);
}
pub(crate) fn recursive_term_link_textspan_parent(
add: On<Add, TooltipTermLinkRecursive>,
text_query: Query<Entity, With<Text>>,
ancestor_query: Query<&ChildOf>,
mut commands: Commands,
) {
let add_entity = add.entity;
if text_query.contains(add_entity) {
r!(commands.get_entity(add_entity)).insert(RelativeCursorPosition::default());
return;
}
for entity in ancestor_query.iter_ancestors(add_entity) {
if text_query.contains(entity) {
r!(commands.get_entity(entity)).insert(ToolTipListenTextSpan);
return;
}
}
error!(
"{add_entity} has no text component nor any ancestors with a text componet.\nText needs to be inserted concurrently or before any links.\nThis limitation will be removed when textspans support observers"
);
}
fn mouse_moved(window_query: Query<&Window, Changed<Window>>) -> bool {
window_query.count() == 1
}
#[derive(QueryData)]
struct TooltipLinksQuery {
entity: Entity,
text_layout_info: &'static TextLayoutInfo,
compute_node: &'static ComputedNode,
relative_cursor: &'static RelativeCursorPosition,
}
#[allow(clippy::type_complexity)]
fn tooltip_links(
tooltip_links_query: Query<
TooltipLinksQuery,
(
Without<TooltipsNested>,
Or<(
With<TooltipTermLink>,
With<TooltipHighlightLink>,
With<ToolTipListenTextSpan>,
)>,
),
>,
was_hovering: Option<Res<WasHoveringText>>,
mut commands: Commands,
) {
if let Some(hovered) = was_hovering {
let links_item = match tooltip_links_query.get(hovered.relative_cursor_entity) {
Ok(item) => item,
Err(_) => {
commands.remove_resource::<WasHoveringText>();
return;
}
};
let relative = links_item.relative_cursor;
let ui_node = links_item.compute_node;
let text_layout = links_item.text_layout_info;
match relative.normalized {
Some(norm) => {
let adjusted_cursor_position = ui_node.size() / 2. + norm * ui_node.size();
if let Some(rect) = text_layout
.section_rects
.iter()
.find(|rect| rect.1.contains(adjusted_cursor_position))
{
if rect.0 != hovered.entity {
commands.remove_resource::<WasHoveringText>();
commands.trigger(TextHoveredOut {
entity: hovered.entity,
});
}
return;
} else {
commands.remove_resource::<WasHoveringText>();
commands.trigger(TextHoveredOut {
entity: hovered.entity,
});
}
}
None => {
commands.remove_resource::<WasHoveringText>();
commands.trigger(TextHoveredOut {
entity: hovered.entity,
});
return;
}
}
}
for links_item in tooltip_links_query {
let entity = links_item.entity;
let relative = links_item.relative_cursor;
let ui_node = links_item.compute_node;
let text_layout = links_item.text_layout_info;
if relative.cursor_over
&& let Some(norm) = relative.normalized
{
let adjusted_cursor_position = ui_node.size() / 2. + norm * ui_node.size();
if let Some((hovered_entity, _)) = text_layout
.section_rects
.iter()
.find(|rect| rect.1.contains(adjusted_cursor_position))
.copied()
{
commands.trigger(TextHoveredOver {
entity: hovered_entity,
});
commands.insert_resource(WasHoveringText {
entity: hovered_entity,
relative_cursor_entity: entity,
});
return;
}
}
}
}
#[allow(clippy::type_complexity)]
fn middle_mouse_links(
mut press: On<Pointer<Press>>,
tooltip_links_query: Query<
TooltipLinksQuery,
(
Without<TooltipsNested>,
Or<(
With<TooltipTermLink>,
With<TooltipHighlightLink>,
With<ToolTipListenTextSpan>,
)>,
),
>,
mut commands: Commands,
) {
press.propagate(false);
if press.button != PointerButton::Middle {
return;
}
if let Ok(links_item) = tooltip_links_query.get(press.entity) {
let relative = links_item.relative_cursor;
let ui_node = links_item.compute_node;
let text_layout = links_item.text_layout_info;
if relative.cursor_over
&& let Some(norm) = relative.normalized
{
let adjusted_cursor_position = ui_node.size() / 2. + norm * ui_node.size();
if let Some((hovered_entity, _)) = text_layout
.section_rects
.iter()
.find(|rect| rect.1.contains(adjusted_cursor_position))
.copied()
{
commands.trigger(TextMiddlePress {
entity: hovered_entity,
});
}
}
}
}