use bevy::prelude::*;
use bevy::ui::Outline;
pub struct FocusPlugin;
impl Plugin for FocusPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(update_focus_outline_system, update_focus_ring_system),
);
}
}
#[derive(Component, Default)]
pub struct Focusable {
pub focused: bool,
pub focus_visible: bool,
pub ring_color: Option<Color>,
pub ring_offset: f32,
pub ring_width: f32,
pub use_native_outline: bool,
}
impl Focusable {
pub fn new() -> Self {
Self {
focused: false,
focus_visible: false,
ring_color: None,
ring_offset: 2.0,
ring_width: 3.0,
use_native_outline: true,
}
}
pub fn legacy() -> Self {
Self {
use_native_outline: false,
..Self::new()
}
}
pub fn with_color(mut self, color: Color) -> Self {
self.ring_color = Some(color);
self
}
pub fn with_offset(mut self, offset: f32) -> Self {
self.ring_offset = offset;
self
}
pub fn with_width(mut self, width: f32) -> Self {
self.ring_width = width;
self
}
pub fn to_outline(&self, default_color: Color) -> Outline {
let color = if self.focus_visible {
self.ring_color.unwrap_or(default_color)
} else {
Color::NONE
};
Outline::new(Val::Px(self.ring_width), Val::Px(self.ring_offset), color)
}
}
#[derive(Component)]
pub struct FocusRing {
pub target: Entity,
}
#[derive(Event, bevy::prelude::Message)]
pub struct FocusGained {
pub entity: Entity,
pub from_keyboard: bool,
}
#[derive(Event, bevy::prelude::Message)]
pub struct FocusLost {
pub entity: Entity,
}
fn update_focus_outline_system(
mut focusables: Query<(&Focusable, &mut Outline), Changed<Focusable>>,
) {
for (focusable, mut outline) in focusables.iter_mut() {
if focusable.use_native_outline {
let default_color = Color::srgb(0.0, 0.47, 0.84); if focusable.focus_visible {
outline.width = Val::Px(focusable.ring_width);
outline.offset = Val::Px(focusable.ring_offset);
outline.color = focusable.ring_color.unwrap_or(default_color);
} else {
outline.color = Color::NONE;
}
}
}
}
fn update_focus_ring_system(
focusables: Query<(&Focusable, &Children), Changed<Focusable>>,
mut focus_rings: Query<&mut Node, With<FocusRing>>,
) {
for (focusable, children) in focusables.iter() {
if focusable.use_native_outline {
continue;
}
for child in children.iter() {
if let Ok(mut node) = focus_rings.get_mut(child) {
node.display = if focusable.focus_visible {
Display::Flex
} else {
Display::None
};
}
}
}
}
pub fn create_focus_ring(target: Entity, color: Color, offset: f32, width: f32) -> impl Bundle {
(
FocusRing { target },
Node {
position_type: PositionType::Absolute,
left: Val::Px(-offset - width),
top: Val::Px(-offset - width),
right: Val::Px(-offset - width),
bottom: Val::Px(-offset - width),
border: UiRect::all(Val::Px(width)),
border_radius: BorderRadius::all(Val::Px(4.0 + offset)),
display: Display::None,
..default()
},
BorderColor::all(color),
BackgroundColor(Color::NONE),
)
}
pub fn create_native_focus_outline(color: Color, offset: f32, width: f32) -> Outline {
Outline::new(Val::Px(width), Val::Px(offset), color)
}