Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: Render yellow rectangular highlight on keyboard focus #15717

Merged
merged 5 commits into from Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions core/src/display_object.rs
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions core/src/display_object/avm1_button.rs
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions core/src/display_object/movie_clip.rs
Expand Up @@ -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> {
Expand Down
11 changes: 11 additions & 0 deletions core/src/display_object/stage.rs
Expand Up @@ -15,6 +15,7 @@ use crate::display_object::interactive::{
};
use crate::display_object::{render_base, DisplayObjectBase, DisplayObjectPtr};
use crate::events::{ClipEvent, ClipEventResult};
use crate::focus_tracker::FocusTracker;
use crate::prelude::*;
use crate::string::{FromWStr, WStr};
use crate::tag_utils::SwfMovie;
Expand Down Expand Up @@ -147,6 +148,9 @@ pub struct StageData<'gc> {
/// identity matrix unless explicitly set from ActionScript)
#[collect(require_static)]
viewport_matrix: Matrix,

/// A tracker for the current keyboard focused element
focus_tracker: FocusTracker<'gc>,
}

impl<'gc> Stage<'gc> {
Expand Down Expand Up @@ -184,6 +188,7 @@ impl<'gc> Stage<'gc> {
stage3ds: vec![],
movie,
viewport_matrix: Matrix::IDENTITY,
focus_tracker: FocusTracker::new(gc_context),
},
));
stage.set_is_root(gc_context, true);
Expand Down Expand Up @@ -732,6 +737,10 @@ impl<'gc> Stage<'gc> {
Avm2::dispatch_event(context, full_screen_event, stage);
}
}

pub fn focus_tracker(&self) -> FocusTracker<'gc> {
self.0.read().focus_tracker
}
}

impl<'gc> TDisplayObject<'gc> for Stage<'gc> {
Expand Down Expand Up @@ -838,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);
}
Expand Down
94 changes: 84 additions & 10 deletions core/src/focus_tracker.rs
@@ -1,37 +1,74 @@
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(
&self,
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);
Expand Down Expand Up @@ -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())
Expand All @@ -102,7 +139,44 @@ 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();
Copy link
Contributor

@Dinnerbone Dinnerbone Apr 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Drawing for this is going to be a little expensive on the rendering side, but realistically this isn't going to be hit enough to be noticed (I hope?)
Ideally we have a draw command that's "draw box outline" later, and let the render backend optimise that. It can make it screen-pixel perfect too, which would be nice.

Not something you need to change for this PR - just a comment!

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>) {
Expand Down
19 changes: 12 additions & 7 deletions core/src/player.rs
Expand Up @@ -33,7 +33,6 @@ use crate::events::GamepadButton;
use crate::events::{ButtonKeyCode, ClipEvent, ClipEventResult, KeyCode, MouseButton, PlayerEvent};
use crate::external::{ExternalInterface, ExternalInterfaceProvider, NullFsCommandProvider};
use crate::external::{FsCommandProvider, Value as ExternalValue};
use crate::focus_tracker::FocusTracker;
use crate::frame_lifecycle::{run_all_phases_avm2, FramePhase};
use crate::library::Library;
use crate::limits::ExecutionLimit;
Expand Down Expand Up @@ -159,9 +158,6 @@ struct GcRootData<'gc> {
/// External interface for (for example) JavaScript <-> ActionScript interaction
external_interface: ExternalInterface<'gc>,

/// A tracker for the current keyboard focused element
focus_tracker: FocusTracker<'gc>,

/// Manager of active sound instances.
audio_manager: AudioManager<'gc>,

Expand Down Expand Up @@ -1191,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.
Expand Down Expand Up @@ -1841,7 +1848,6 @@ impl Player {
let mut root_data = gc_root.data.write(gc_context);
let mouse_hovered_object = root_data.mouse_hovered_object;
let mouse_pressed_object = root_data.mouse_pressed_object;
let focus_tracker = root_data.focus_tracker;

#[allow(unused_variables)]
let (
Expand Down Expand Up @@ -1906,7 +1912,7 @@ impl Player {
start_time: self.start_time,
update_start: Instant::now(),
max_execution_duration: self.max_execution_duration,
focus_tracker,
focus_tracker: stage.focus_tracker(),
times_get_time_called: 0,
time_offset: &mut self.time_offset,
audio_manager,
Expand Down Expand Up @@ -2450,7 +2456,6 @@ impl PlayerBuilder {
external_interface_providers,
fs_command_provider,
),
focus_tracker: FocusTracker::new(gc_context),
library: Library::empty(),
load_manager: LoadManager::new(),
mouse_hovered_object: None,
Expand Down
3 changes: 3 additions & 0 deletions swf/src/types/color.rs
Expand Up @@ -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.
///
Expand Down
11 changes: 11 additions & 0 deletions swf/src/types/rectangle.rs
Expand Up @@ -130,6 +130,17 @@ impl<T: Coordinate> Rectangle<T> {
&& self.y_min <= other.y_max
&& self.y_max >= other.y_min
}

#[must_use]
pub fn grow(mut self, amount: T) -> Self {
if self.is_valid() {
self.x_min -= amount;
self.x_max += amount;
self.y_min -= amount;
self.y_max += amount;
}
self
}
}

impl<T: Coordinate> Default for Rectangle<T> {
Expand Down
@@ -0,0 +1,29 @@
[
{ "type": "KeyDown", "key_code": 9 },
{ "type": "Wait" },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "Wait" },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "MouseMove", "pos": [42, 42] },
{ "type": "Wait" },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "MouseDown", "pos": [42, 42], "btn": "Left" },
{ "type": "Wait" },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "MouseUp", "pos": [42, 42], "btn": "Left" },
{ "type": "Wait" },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 97 },
{ "type": "Wait" },
{ "type": "KeyDown", "key_code": 27 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 27 },
{ "type": "Wait" },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "KeyDown", "key_code": 27 },
{ "type": "KeyDown", "key_code": 9 },
{ "type": "Wait" },
{ "type": "MouseMove", "pos": [42, 42] },
{ "type": "KeyDown", "key_code": 27 },
{ "type": "Wait" }
]
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
@@ -0,0 +1,15 @@
clip.tabEnabled = true;
clip.focusEnabled = true;
clip.tabIndex = 1;
clip2.tabEnabled = true;
clip2.focusEnabled = true;
clip2.tabIndex = 2;

var listener = new Object();
listener.onKeyDown = function() {
if (Key.getCode() == 27) {
Selection.setFocus(clip2);
Selection.setFocus(clip);
}
};
Key.addListener(listener);
Binary file not shown.
@@ -0,0 +1,40 @@
num_ticks = 9

[image_comparisons."output.01_highlight_under"]
tolerance = 0
trigger = 1

[image_comparisons."output.02_highlight_over"]
tolerance = 0
trigger = 2

[image_comparisons."output.03_after_mouse_move"]
tolerance = 0
trigger = 3

[image_comparisons."output.04_after_mouse_up"]
tolerance = 0
trigger = 4

[image_comparisons."output.05_after_mouse_down"]
tolerance = 0
trigger = 5

[image_comparisons."output.06_after_key_down"]
tolerance = 0
trigger = 6

[image_comparisons."output.07_after_focus_change"]
tolerance = 0
trigger = 7

[image_comparisons."output.08_after_focus_change_and_tab"]
tolerance = 0
trigger = 8

[image_comparisons."output.09_after_focus_change_without_highlight"]
tolerance = 0
trigger = 9

[player_options]
with_renderer = { optional = false, sample_count = 1 }