core: Render yellow highlight on keyboard focus

This patch implements rendering of the yellow rectangle around
a focused element after pressing Tab. Focus tracker which is responsible
for keeping track of the current focus is now also responsible
for keeping track of the highlight and rendering thereof.
This commit is contained in:
Kamil Jarosz 2024-03-22 01:08:10 +01:00 committed by TÖRÖK Attila
parent 068363a87c
commit 95983bf4f3
8 changed files with 115 additions and 10 deletions

View File

@ -1926,6 +1926,11 @@ pub trait TDisplayObject<'gc>:
None
}
/// Whether this object may be highlighted by tab ordering.
fn is_highlight_enabled(&self) -> bool {
false
}
/// Whether this display object has been created by ActionScript 3.
/// When this flag is set, changes from SWF `RemoveObject` tags are
/// ignored.

View File

@ -430,6 +430,11 @@ impl<'gc> TDisplayObject<'gc> for Avm1Button<'gc> {
self.0.tab_index.get().map(|i| i as i64)
}
fn is_highlight_enabled(&self) -> bool {
// TODO focusrect support
true
}
fn avm1_unload(&self, context: &mut UpdateContext<'_, 'gc>) {
let had_focus = self.0.has_focus.get();
if had_focus {

View File

@ -3041,6 +3041,11 @@ impl<'gc> TDisplayObject<'gc> for MovieClip<'gc> {
fn tab_index(&self) -> Option<i64> {
self.0.read().tab_index.map(|i| i as i64)
}
fn is_highlight_enabled(&self) -> bool {
// TODO focusrect support
true
}
}
impl<'gc> TDisplayObjectContainer<'gc> for MovieClip<'gc> {

View File

@ -847,6 +847,8 @@ impl<'gc> TDisplayObject<'gc> for Stage<'gc> {
render_base((*self).into(), context);
self.focus_tracker().render_highlight(context);
if self.should_letterbox() {
self.draw_letterbox(context);
}

View File

@ -1,25 +1,58 @@
use crate::avm1::Avm1;
use crate::avm1::Value;
use crate::context::UpdateContext;
use crate::context::{RenderContext, UpdateContext};
pub use crate::display_object::{
DisplayObject, TDisplayObject, TDisplayObjectContainer, TextSelection,
};
use crate::drawing::Drawing;
use either::Either;
use gc_arena::lock::GcLock;
use gc_arena::{Collect, Mutation};
use swf::Twips;
use gc_arena::barrier::unlock;
use gc_arena::lock::Lock;
use gc_arena::{Collect, Gc, Mutation};
use ruffle_render::shape_utils::DrawCommand;
use std::cell::RefCell;
use swf::{Color, LineJoinStyle, Point, Twips};
#[derive(Collect)]
#[collect(no_drop)]
pub struct FocusTrackerData<'gc> {
focus: Lock<Option<DisplayObject<'gc>>>,
highlight: RefCell<Highlight>,
}
enum Highlight {
Inactive,
Active(Drawing),
}
#[derive(Clone, Copy, Collect)]
#[collect(no_drop)]
pub struct FocusTracker<'gc>(GcLock<'gc, Option<DisplayObject<'gc>>>);
pub struct FocusTracker<'gc>(Gc<'gc, FocusTrackerData<'gc>>);
impl<'gc> FocusTracker<'gc> {
const HIGHLIGHT_WIDTH: Twips = Twips::from_pixels_i32(3);
const HIGHLIGHT_COLOR: Color = Color::YELLOW;
// Although at 3px width Round and Miter are similar
// to each other, it seems that FP uses Round.
const HIGHLIGHT_LINE_JOIN_STYLE: LineJoinStyle = LineJoinStyle::Round;
pub fn new(mc: &Mutation<'gc>) -> Self {
Self(GcLock::new(mc, None.into()))
Self(Gc::new(
mc,
FocusTrackerData {
focus: Lock::new(None),
highlight: RefCell::new(Highlight::Inactive),
},
))
}
pub fn reset_highlight(&self) {
self.0.highlight.replace(Highlight::Inactive);
}
pub fn get(&self) -> Option<DisplayObject<'gc>> {
self.0.get()
self.0.focus.get()
}
pub fn set(
@ -27,11 +60,15 @@ impl<'gc> FocusTracker<'gc> {
focused_element: Option<DisplayObject<'gc>>,
context: &mut UpdateContext<'_, 'gc>,
) {
let old = self.0.get();
let old = self.0.focus.get();
// Check if the focused element changed.
if old.map(|o| o.as_ptr()) != focused_element.map(|o| o.as_ptr()) {
self.0.set(context.gc(), focused_element);
let focus = unlock!(Gc::write(context.gc(), self.0), FocusTrackerData, focus);
focus.set(focused_element);
// The highlight always follows the focus.
self.update_highlight();
if let Some(old) = old {
old.on_focus_changed(context, false, focused_element);
@ -89,7 +126,7 @@ impl<'gc> FocusTracker<'gc> {
.peekable();
let first = tab_order.peek().copied();
let next = if let Some(current_focus) = self.0.get() {
let next = if let Some(current_focus) = self.get() {
// Find the next object which should take the focus.
tab_order
.skip_while(|o| o.as_ptr() != current_focus.as_ptr())
@ -102,9 +139,46 @@ impl<'gc> FocusTracker<'gc> {
if next.is_some() {
self.set(next.copied(), context);
self.update_highlight();
}
}
fn update_highlight(&self) {
self.0.highlight.replace(self.redraw_highlight());
}
fn redraw_highlight(&self) -> Highlight {
let Some(focus) = self.get() else {
return Highlight::Inactive;
};
if !focus.is_highlight_enabled() {
return Highlight::Inactive;
}
let bounds = focus.world_bounds().grow(-Self::HIGHLIGHT_WIDTH / 2);
let mut drawing = Drawing::new();
drawing.set_line_style(Some(
swf::LineStyle::new()
.with_width(Self::HIGHLIGHT_WIDTH)
.with_color(Self::HIGHLIGHT_COLOR)
.with_join_style(Self::HIGHLIGHT_LINE_JOIN_STYLE),
));
drawing.draw_command(DrawCommand::MoveTo(Point::new(bounds.x_min, bounds.y_min)));
drawing.draw_command(DrawCommand::LineTo(Point::new(bounds.x_min, bounds.y_max)));
drawing.draw_command(DrawCommand::LineTo(Point::new(bounds.x_max, bounds.y_max)));
drawing.draw_command(DrawCommand::LineTo(Point::new(bounds.x_max, bounds.y_min)));
drawing.draw_command(DrawCommand::LineTo(Point::new(bounds.x_min, bounds.y_min)));
Highlight::Active(drawing)
}
pub fn render_highlight(&self, context: &mut RenderContext<'_, 'gc>) {
if let Highlight::Active(ref highlight) = *self.0.highlight.borrow() {
highlight.render(context);
};
}
fn order_custom(tab_order: &mut Vec<DisplayObject>) {
// Custom ordering disables automatic ordering and
// ignores all objects without tabIndex.

View File

@ -1187,6 +1187,17 @@ impl Player {
tracker.cycle(context, reversed);
});
}
if matches!(
event,
PlayerEvent::MouseDown { .. }
| PlayerEvent::MouseUp { .. }
| PlayerEvent::MouseMove { .. }
) {
self.mutate_with_update_context(|context| {
context.focus_tracker.reset_highlight();
});
}
}
/// Update dragged object, if any.

View File

@ -23,6 +23,9 @@ impl Color {
pub const RED: Self = Self::from_rgb(0xFF0000, 255);
pub const GREEN: Self = Self::from_rgb(0x00FF00, 255);
pub const BLUE: Self = Self::from_rgb(0x0000FF, 255);
pub const YELLOW: Self = Self::from_rgb(0xFFFF00, 255);
pub const CYAN: Self = Self::from_rgb(0x00FFFF, 255);
pub const MAGENTA: Self = Self::from_rgb(0xFF00FF, 255);
/// Creates a `Color` from a 32-bit `rgb` value and an `alpha` value.
///

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB