| // Copyright 2021 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/ui/a11y/lib/magnifier/magnifier_2.h" |
| |
| #include <lib/syslog/cpp/macros.h> |
| |
| namespace a11y { |
| |
| Magnifier2::Magnifier2(std::shared_ptr<Delegate> delegate) : delegate_(delegate) {} |
| |
| void Magnifier2::ZoomOutIfMagnified() { |
| if (state_.mode != Mode::UNMAGNIFIED) { |
| state_.mode = Mode::UNMAGNIFIED; |
| TransitionOutOfZoom(); |
| } |
| } |
| bool Magnifier2::State::operator==(const State& o) const { |
| return transition_rate == o.transition_rate && scale == o.scale && translation == o.translation; |
| } |
| |
| bool Magnifier2::State::operator!=(const State& o) const { return !(*this == o); } |
| |
| void Magnifier2::State::FocusOn(const ::fuchsia::math::PointF& focus) { |
| glm::vec2 focus_vec; |
| focus_vec.x = focus.x; |
| focus_vec.y = focus.y; |
| |
| translation = -focus_vec * (scale - 1); |
| } |
| |
| void Magnifier2::TransitionIntoZoom() { |
| state_.FocusOn(state_.gesture_context.CurrentCentroid(false /* use_local_coordinates */)); |
| state_.scale = kDefaultScale; |
| state_.transition_rate = kTransitionRate; |
| UpdateTransform(); |
| } |
| |
| void Magnifier2::TransitionOutOfZoom() { |
| state_.transition_rate = -kTransitionRate; |
| UpdateTransform(); |
| } |
| |
| void Magnifier2::ClampTranslation() { |
| auto& translation = state_.translation; |
| const auto freedom = state_.scale - 1; |
| translation.x = std::clamp(translation.x, -freedom, freedom); |
| translation.y = std::clamp(translation.y, -freedom, freedom); |
| } |
| |
| void Magnifier2::HandleTemporaryDrag(const Delta& delta) { |
| FX_DCHECK(state_.mode == Mode::TEMPORARY); |
| // If the zoom is temporary, treat the coordinate as a focal point, i.e. |
| // focus on the area that would be at that position unzoomed. |
| // |
| // Instead of using the raw centroid coordinate, which jumps around as |
| // fingers are added or removed, move the original tap coordinate by the |
| // delta. |
| state_.FocusOn(state_.gesture_context.CurrentCentroid(false /* use_local_coordinates */)); |
| |
| // Ensure that translation does not fall outside of sensical values. |
| ClampTranslation(); |
| |
| UpdateTransform(); |
| } |
| |
| void Magnifier2::HandlePersistentDrag(const Delta& delta) { |
| FX_DCHECK(state_.mode == Mode::PERSISTENT); |
| |
| float& scale = state_.scale; |
| const float old_scale = scale; |
| scale *= delta.scale; |
| scale = std::clamp(scale, kMinScale, kMaxScale); |
| // Account for clamping for accurate anchor calculation |
| const float actual_delta_scale = scale / old_scale; |
| |
| auto& translation = state_.translation; |
| // For persistent magnification, we want the UX to be a little bit different from temporary |
| // magnification. In persistent magnification, the user can pan and zoom using a two-finger drag |
| // gesture. As the distance between the user’s fingers change, the zoom should change |
| // proportionally to the change in distance (so moving the fingers twice as far apart will cause |
| // the scale to increase by a factor of 2). So, given the two fingers’ former and current |
| // locations, we can compute new_scale = old_scale * (new_distance_between_fingers / |
| // old_distance_between_fingers). To achieve panning, our goal is to keep the same point in |
| // unscaled space under the centroid of the drag at all times. To do so, we need to consider both |
| // the new and previous locations of the centroid (midpoint) between the two fingers. To determine |
| // the point in unscaled space that is under the previous centroid of the two fingers, we can |
| // simply apply the inverse of the current magnification transform to the previous centroid |
| // coordinates. Then, we can compute the new magnification transform by determining what |
| // translation will place the unscaled point at the new centroid location (after we’ve applied the |
| // new scale factor). |
| auto current_centroid = |
| ToVec2(state_.gesture_context.CurrentCentroid(false /* use_local_coordinates */)); |
| translation = |
| current_centroid + delta.translation + actual_delta_scale * (translation - current_centroid); |
| |
| // Ensure that translation does not fall outside of sensical values. |
| ClampTranslation(); |
| |
| UpdateTransform(); |
| } |
| |
| void Magnifier2::UpdateTransform() { |
| float& transition_rate = state_.transition_rate; |
| float& transition_progress = state_.transition_progress; |
| bool& update_pending = state_.update_pending; |
| bool& update_in_progress = state_.update_in_progress; |
| |
| if (!delegate_) { |
| FX_LOGS(WARNING) << "No magnification delegate registered."; |
| |
| // If there's no handler, don't bother animating. |
| if (transition_rate > 0) { |
| transition_progress = 1; |
| transition_rate = 0; |
| } else if (transition_rate < 0) { |
| transition_progress = 0; |
| transition_rate = 0; |
| } |
| return; |
| } |
| |
| if (update_in_progress) { |
| update_pending = true; // We'll |UpdateTransform| on the next callback instead. |
| } else { |
| update_in_progress = true; |
| |
| if (transition_rate != 0) { |
| transition_progress = std::clamp(transition_progress + transition_rate, 0.f, 1.f); |
| |
| // If the transition rate is positive, then transition progress of 0 |
| // indicates that the transition is just beginning, and transition |
| // progress of 1 indicatest that the transition is complete. |
| // For negative transition rate, transition progress decreases from 1 -> |
| // 0. |
| // |
| // Therefore, this if statement just checks if the transition is not yet |
| // complete. |
| if ((transition_rate > 0 && transition_progress < 1) || |
| (transition_rate < 0 && transition_progress > 0)) { |
| update_pending = true; |
| } else { |
| transition_rate = 0; |
| } |
| } |
| |
| auto translation_x = transition_progress * state_.translation.x; |
| auto translation_y = transition_progress * state_.translation.y; |
| auto scale = 1 + transition_progress * (state_.scale - 1); |
| |
| delegate_->SetMagnificationTransform(scale, translation_x, translation_y, [this] { |
| state_.update_in_progress = false; |
| if (state_.update_pending) { |
| state_.update_pending = false; |
| UpdateTransform(); |
| } |
| }); |
| } |
| } |
| |
| void Magnifier2::TogglePersistentMagnification() { |
| if (state_.mode == Mode::UNMAGNIFIED) { |
| state_.mode = Mode::PERSISTENT; |
| TransitionIntoZoom(); |
| } else if (state_.mode == Mode::PERSISTENT) { |
| state_.mode = Mode::UNMAGNIFIED; |
| TransitionOutOfZoom(); |
| } |
| } |
| |
| void Magnifier2::BindGestures(a11y::GestureHandlerV2* gesture_handler) { |
| FX_DCHECK(gesture_handler); |
| |
| // Add gestures with higher priority earlier than gestures with lower priority. |
| bool gesture_bind_status = |
| gesture_handler->BindMFingerNTapAction(1 /* number of fingers */, 3 /* number of taps */, |
| [this](a11y::gesture_util_v2::GestureContext context) { |
| state_.gesture_context = context; |
| // One-finger-triple-tap should be a NOOP in |
| // temporary magnification mode. |
| if (state_.mode == Mode::TEMPORARY) { |
| return; |
| } |
| TogglePersistentMagnification(); |
| }); |
| FX_DCHECK(gesture_bind_status); |
| |
| gesture_bind_status = |
| gesture_handler->BindMFingerNTapAction(3 /* number of fingers */, 2 /* number of taps */, |
| [this](a11y::gesture_util_v2::GestureContext context) { |
| state_.gesture_context = context; |
| // Three-finger-double-tap should be a NOOP in |
| // temporary magnification mode. |
| if (state_.mode == Mode::TEMPORARY) { |
| return; |
| } |
| TogglePersistentMagnification(); |
| }); |
| FX_DCHECK(gesture_bind_status); |
| |
| gesture_bind_status = gesture_handler->BindMFingerNTapDragAction( |
| [this](a11y::gesture_util_v2::GestureContext context) { |
| state_.gesture_context = context; |
| // Tap-drag gestures should only work to enable temporary magnification |
| // from an unmagnified state. |
| if (state_.mode != Mode::UNMAGNIFIED) { |
| return; |
| } |
| state_.mode = Mode::TEMPORARY; |
| TransitionIntoZoom(); |
| }, /* on recognize */ |
| [this](a11y::gesture_util_v2::GestureContext context) { |
| // We should be in TEMPORARY magnification mode if we hit this callback. |
| if (state_.mode != Mode::TEMPORARY) { |
| return; |
| } |
| auto delta = GetDelta(context /* current */, state_.gesture_context /* previous */, |
| 0 /* scale_min_finger_radius */); |
| state_.gesture_context = context; |
| HandleTemporaryDrag(delta); |
| }, /* on update */ |
| [this](a11y::gesture_util_v2::GestureContext context) { |
| state_.mode = Mode::UNMAGNIFIED; |
| TransitionOutOfZoom(); |
| }, /* on complete */ |
| 1 /* number of fingers */, 3 /* number of taps */); |
| FX_DCHECK(gesture_bind_status); |
| |
| gesture_bind_status = gesture_handler->BindMFingerNTapDragAction( |
| [this](a11y::gesture_util_v2::GestureContext context) { |
| state_.gesture_context = context; |
| // Tap-drag gestures should only work to enable temporary magnification |
| // from an unmagnified state. |
| if (state_.mode != Mode::UNMAGNIFIED) { |
| return; |
| } |
| state_.mode = Mode::TEMPORARY; |
| TransitionIntoZoom(); |
| }, /* on recognize */ |
| [this](a11y::gesture_util_v2::GestureContext context) { |
| // We should be in TEMPORARY magnification mode if we hit this callback. |
| if (state_.mode != Mode::TEMPORARY) { |
| return; |
| } |
| auto delta = GetDelta(context /* current */, state_.gesture_context /* previous */, |
| 0 /* scale_min_finger_radius */); |
| state_.gesture_context = context; |
| HandleTemporaryDrag(delta); |
| }, /* on update */ |
| [this](a11y::gesture_util_v2::GestureContext context) { |
| state_.mode = Mode::UNMAGNIFIED; |
| TransitionOutOfZoom(); |
| }, /* on complete */ |
| 3 /* number of fingers */, 2 /* number of taps */); |
| FX_DCHECK(gesture_bind_status); |
| |
| gesture_bind_status = gesture_handler->BindTwoFingerDragAction( |
| [this](a11y::gesture_util_v2::GestureContext context) { |
| // The magnifier should only respond to two-finger drags when in |
| // PERSISTENT magnification mode. |
| if (state_.mode != Mode::PERSISTENT) { |
| return; |
| } |
| // Since this is the first event received for the gesture, we don't have |
| // any previous context to compare the current one against, so we can't |
| // adjust the magnification transform meaningfully yet. |
| state_.gesture_context = context; |
| }, /* on recognize */ |
| [this](a11y::gesture_util_v2::GestureContext context) { |
| // The magnifier should only respond to two-finger drags when in |
| // PERSISTENT magnification mode. |
| if (state_.mode != Mode::PERSISTENT) { |
| return; |
| } |
| auto delta = GetDelta(context /* current */, state_.gesture_context /* previous */, |
| kZoomMinFingerDistance); |
| |
| HandlePersistentDrag(delta); |
| |
| // We need to wait until after we've called HandlePersistentDrag() to |
| // update state_.gesture_context, becuase HandlePersistentDrag() needs |
| // to use the old gesture_context. |
| state_.gesture_context = context; |
| }, /* on update */ |
| [](a11y::gesture_util_v2::GestureContext context) { |
| /* NOOP */ |
| } /* on complete */); |
| FX_DCHECK(gesture_bind_status); |
| } |
| |
| } // namespace a11y |