| // Copyright 2015 The Chromium 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 <math.h> |
| |
| #include "services/keyboard_native/view_observer_delegate.h" |
| |
| #include "base/bind.h" |
| #include "base/sys_info.h" |
| #include "mojo/gpu/gl_texture.h" |
| #include "mojo/public/cpp/application/application_impl.h" |
| #include "mojo/public/cpp/application/connect.h" |
| #include "mojo/services/prediction/interfaces/prediction.mojom.h" |
| #include "mojo/skia/ganesh_surface.h" |
| #include "services/keyboard_native/clip_animation.h" |
| #include "services/keyboard_native/keyboard_service_impl.h" |
| #include "services/keyboard_native/predictor.h" |
| #include "skia/ext/refptr.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkTypeface.h" |
| #include "ui/gfx/geometry/rect_f.h" |
| #include "ui/gfx/shadow_value.h" |
| #include "ui/gfx/skia_util.h" |
| #include "ui/gfx/vector2d.h" |
| |
| namespace keyboard { |
| |
| static const base::TimeDelta kAnimationDurationMs = |
| base::TimeDelta::FromMilliseconds(1000); |
| |
| mojo::Size ToSize(const mojo::Rect& rect) { |
| mojo::Size size; |
| size.width = rect.width; |
| size.height = rect.height; |
| return size; |
| } |
| |
| ViewObserverDelegate::ViewObserverDelegate() |
| : keyboard_service_impl_(nullptr), |
| predictor_(nullptr), |
| view_(nullptr), |
| id_namespace_(0u), |
| surface_id_(1u), |
| key_layout_(), |
| needs_draw_(false), |
| frame_pending_(false), |
| floating_key_height_(0.0f), |
| floating_key_width_(0.0f), |
| weak_factory_(this) { |
| key_layout_.SetTextCallback( |
| base::Bind(&ViewObserverDelegate::OnText, weak_factory_.GetWeakPtr())); |
| key_layout_.SetDeleteCallback( |
| base::Bind(&ViewObserverDelegate::OnDelete, weak_factory_.GetWeakPtr())); |
| key_layout_.SetSuggestTextCallback(base::Bind( |
| &ViewObserverDelegate::OnSuggestText, weak_factory_.GetWeakPtr())); |
| submit_frame_callback_ = base::Bind(&ViewObserverDelegate::OnFrameComplete, |
| weak_factory_.GetWeakPtr()); |
| } |
| |
| ViewObserverDelegate::~ViewObserverDelegate() { |
| } |
| |
| void ViewObserverDelegate::SetKeyboardServiceImpl( |
| KeyboardServiceImpl* keyboard_service_impl) { |
| keyboard_service_impl_ = keyboard_service_impl; |
| } |
| |
| void ViewObserverDelegate::SetIdNamespace(uint32_t id_namespace) { |
| id_namespace_ = id_namespace; |
| UpdateSurfaceIds(); |
| } |
| |
| void ViewObserverDelegate::UpdateSurfaceIds() { |
| auto surface_id = mojo::SurfaceId::New(); |
| surface_id->id_namespace = id_namespace_; |
| surface_id->local = surface_id_; |
| view_->SetSurfaceId(surface_id.Pass()); |
| } |
| |
| void ViewObserverDelegate::OnViewCreated(mojo::View* view, mojo::Shell* shell) { |
| if (view_ != nullptr) { |
| view_->RemoveObserver(this); |
| } |
| view_ = view; |
| view_->AddObserver(this); |
| gl_context_ = mojo::GLContext::Create(shell); |
| mojo::ResourceReturnerPtr resource_returner; |
| texture_cache_.reset(new mojo::TextureCache(gl_context_, &resource_returner)); |
| gr_context_.reset(new mojo::GaneshContext(gl_context_)); |
| mojo::ServiceProviderPtr surfaces_service_provider; |
| shell->ConnectToApplication("mojo:surfaces_service", |
| mojo::GetProxy(&surfaces_service_provider), |
| nullptr); |
| mojo::ConnectToService(surfaces_service_provider.get(), &surface_); |
| surface_->SetResourceReturner(resource_returner.Pass()); |
| surface_->CreateSurface(surface_id_); |
| surface_->GetIdNamespace(base::Bind(&ViewObserverDelegate::SetIdNamespace, |
| base::Unretained(this))); |
| predictor_ = new Predictor(shell); |
| predictor_->SetUpdateCallback(base::Bind( |
| &ViewObserverDelegate::OnUpdateSuggestion, weak_factory_.GetWeakPtr())); |
| key_layout_.SetPredictor(predictor_); |
| IssueDraw(); |
| } |
| |
| void ViewObserverDelegate::OnText(const std::string& text) { |
| keyboard_service_impl_->OnKey(text.c_str()); |
| } |
| |
| void ViewObserverDelegate::OnDelete() { |
| keyboard_service_impl_->OnDelete(); |
| } |
| |
| void ViewObserverDelegate::OnSuggestText(const std::string& text) { |
| std::string text_with_space = text + " "; |
| keyboard_service_impl_->OnKey(text_with_space.c_str()); |
| } |
| |
| void ViewObserverDelegate::OnUpdateSuggestion() { |
| IssueDraw(); |
| } |
| |
| void ViewObserverDelegate::UpdateState(int32 pointer_id, |
| mojo::EventType action, |
| const gfx::PointF& touch_point) { |
| // Ignore touches outside of key area. |
| if (!key_area_.Contains(touch_point)) { |
| return; |
| } |
| |
| base::TimeTicks current_ticks = base::TimeTicks::Now(); |
| |
| auto it2 = active_pointer_state_.find(pointer_id); |
| if (it2 != active_pointer_state_.end()) { |
| if (it2->second->last_point_valid) { |
| animations_.push_back(make_scoped_ptr( |
| new MotionDecayAnimation(current_ticks, kAnimationDurationMs, |
| it2->second->last_point, touch_point))); |
| it2->second->last_point = touch_point; |
| } else { |
| it2->second->last_point = touch_point; |
| } |
| it2->second->last_point_valid = action != mojo::EventType::POINTER_UP; |
| } |
| |
| if (action == mojo::EventType::POINTER_UP || |
| action == mojo::EventType::POINTER_DOWN) { |
| animations_.push_back(make_scoped_ptr(new MaterialSplashAnimation( |
| current_ticks, kAnimationDurationMs, touch_point))); |
| } |
| } |
| |
| void ViewObserverDelegate::DrawKeysToCanvas(const gfx::RectF& key_area, |
| SkCanvas* canvas) { |
| key_layout_.SetKeyArea(key_area); |
| key_layout_.Draw(canvas); |
| } |
| |
| void ViewObserverDelegate::DrawState() { |
| base::TimeTicks current_ticks = base::TimeTicks::Now(); |
| mojo::Size size = ToSize(view_->bounds()); |
| mojo::GaneshContext::Scope scope(gr_context_.get()); |
| scoped_ptr<mojo::TextureCache::TextureInfo> texture_info( |
| texture_cache_->GetTexture(size).Pass()); |
| mojo::GaneshSurface surface(gr_context_.get(), |
| texture_info->Texture().Pass()); |
| |
| gr_context_.get()->gr()->resetContext(kTextureBinding_GrGLBackendState); |
| |
| SkCanvas* canvas = surface.canvas(); |
| |
| canvas->clear(SK_ColorTRANSPARENT); |
| |
| // If we have a clip animation it must go first as only things drawn after the |
| // clip is set will be clipped. |
| if (clip_animation_) { |
| if (!clip_animation_->IsDone(current_ticks)) { |
| clip_animation_->Draw(canvas, current_ticks); |
| } else { |
| clip_animation_.reset(); |
| } |
| } |
| |
| DrawKeysToCanvas(key_area_, canvas); |
| // clip animations to the key area. |
| canvas->clipRect(SkRect::MakeXYWH(key_area_.x(), key_area_.y(), |
| key_area_.width(), key_area_.height())); |
| DrawAnimations(canvas, current_ticks); |
| |
| // reset clip to whatever the clip animation sets. |
| canvas->clipRect(SkRect::MakeXYWH(0, 0, size.width, size.height), |
| SkRegion::kReplace_Op); |
| if (clip_animation_) { |
| if (!clip_animation_->IsDone(current_ticks)) { |
| clip_animation_->Draw(canvas, current_ticks); |
| } else { |
| clip_animation_.reset(); |
| } |
| } |
| |
| DrawFloatingKey(canvas, floating_key_height_, floating_key_width_); |
| |
| canvas->flush(); |
| scoped_ptr<mojo::GLTexture> surface_texture(surface.TakeTexture().Pass()); |
| mojo::FramePtr frame = mojo::TextureUploader::GetUploadFrame( |
| gl_context_, texture_info->ResourceId(), surface_texture); |
| texture_cache_->NotifyPendingResourceReturn(texture_info->ResourceId(), |
| surface_texture.Pass()); |
| surface_->SubmitFrame(surface_id_, frame.Pass(), submit_frame_callback_); |
| } |
| |
| void ViewObserverDelegate::DrawAnimations( |
| SkCanvas* canvas, |
| const base::TimeTicks& current_ticks) { |
| auto first_valid_animation = animations_.end(); |
| for (auto it = animations_.begin(); it != animations_.end(); ++it) { |
| bool done = (*it)->IsDone(current_ticks); |
| if (!done) { |
| (*it)->Draw(canvas, current_ticks); |
| if (first_valid_animation == animations_.end()) { |
| first_valid_animation = it; |
| } |
| } |
| } |
| |
| // Clean up 'done' animations. |
| animations_.erase(animations_.begin(), first_valid_animation); |
| } |
| |
| void ViewObserverDelegate::DrawFloatingKey(SkCanvas* canvas, |
| float floating_key_height, |
| float floating_key_width) { |
| if (active_pointer_ids_.empty()) { |
| return; |
| } |
| |
| for (auto& pointer_state : active_pointer_state_) { |
| if (pointer_state.second->last_key != nullptr && |
| pointer_state.second->last_point_valid) { |
| SkPaint paint; |
| paint.setColor(SK_ColorYELLOW); |
| float left = |
| pointer_state.second->last_point.x() - (floating_key_width / 2); |
| float top = |
| pointer_state.second->last_point.y() - (0.45f * key_area_.height()); |
| SkRect rect = SkRect::MakeLTRB(left, top, left + floating_key_width, |
| top + floating_key_height); |
| |
| // Add shadow beneath the floating key. |
| paint.setStrokeJoin(SkPaint::kRound_Join); |
| int blur = 20; |
| float ratio_x_from_center = |
| (left + (floating_key_width / 2) - (key_area_.width() / 2)) / |
| (key_area_.width() / 2); |
| float ratio_y_from_center = |
| (top + (floating_key_height / 2) - (key_area_.height() / 2)) / |
| (key_area_.height() / 2); |
| SkColor color = SkColorSetARGB(0x80, 0, 0, 0); |
| std::vector<gfx::ShadowValue> shadows; |
| shadows.push_back(gfx::ShadowValue( |
| gfx::Vector2d(ratio_x_from_center * 10, ratio_y_from_center * 10), |
| blur, color)); |
| skia::RefPtr<SkDrawLooper> looper = gfx::CreateShadowDrawLooper(shadows); |
| paint.setLooper(looper.get()); |
| |
| canvas->drawRect(rect, paint); |
| |
| skia::RefPtr<SkTypeface> typeface = skia::AdoptRef( |
| SkTypeface::CreateFromName("Arial", SkTypeface::kNormal)); |
| SkPaint text_paint; |
| text_paint.setTypeface(typeface.get()); |
| text_paint.setColor(SK_ColorBLACK); |
| text_paint.setTextSize(floating_key_height / 1.7f); |
| text_paint.setAntiAlias(true); |
| text_paint.setTextAlign(SkPaint::kCenter_Align); |
| pointer_state.second->last_key->Draw( |
| canvas, text_paint, |
| gfx::RectF(left, top, floating_key_width, floating_key_height)); |
| } |
| } |
| } |
| |
| void ViewObserverDelegate::IssueDraw() { |
| if (!frame_pending_) { |
| DrawState(); |
| frame_pending_ = true; |
| needs_draw_ = !animations_.empty() || clip_animation_; |
| } else { |
| needs_draw_ = true; |
| } |
| } |
| |
| void ViewObserverDelegate::OnFrameComplete() { |
| frame_pending_ = false; |
| if (needs_draw_) { |
| IssueDraw(); |
| } |
| } |
| |
| // mojo::ViewObserver implementation. |
| void ViewObserverDelegate::OnViewBoundsChanged(mojo::View* view, |
| const mojo::Rect& old_bounds, |
| const mojo::Rect& new_bounds) { |
| surface_->DestroySurface(surface_id_); |
| surface_id_++; |
| surface_->CreateSurface(surface_id_); |
| UpdateSurfaceIds(); |
| |
| floating_key_height_ = static_cast<float>(new_bounds.height) / 5.0f; |
| floating_key_width_ = static_cast<float>(new_bounds.width) / 8.0f; |
| |
| // One third of our height will be used for overlapping views behind us. Only |
| // the floating key will be drawn into that area. The remaining two thirds |
| // will be the key area. |
| float overlap = new_bounds.height / 3.0f; |
| key_area_ = |
| gfx::RectF(0, overlap, new_bounds.width, new_bounds.height - overlap); |
| |
| // Upon resize, kick off a clip animation centered on our key area with a |
| // radius equal to the length from a corner of the new bounds to the center of |
| // the key area. |
| base::TimeTicks current_ticks = base::TimeTicks::Now(); |
| gfx::PointF center(key_area_.width() / 2.0f, |
| overlap + (key_area_.height() / 2.0f)); |
| float radius = sqrt(new_bounds.width / 2.0f * new_bounds.width / 2.0f + |
| new_bounds.height / 2.0f * new_bounds.height / 2.0f); |
| clip_animation_.reset( |
| new ClipAnimation(current_ticks, kAnimationDurationMs, center, radius)); |
| |
| IssueDraw(); |
| } |
| |
| void ViewObserverDelegate::OnViewInputEvent(mojo::View* view, |
| const mojo::EventPtr& event) { |
| if (event->pointer_data) { |
| gfx::PointF point(event->pointer_data->x, event->pointer_data->y); |
| |
| if (event->action == mojo::EventType::POINTER_UP) { |
| key_layout_.OnTouchUp(point); |
| } |
| |
| if (event->action == mojo::EventType::POINTER_DOWN || |
| event->action == mojo::EventType::POINTER_UP) { |
| auto it = |
| std::find(active_pointer_ids_.begin(), active_pointer_ids_.end(), |
| event->pointer_data->pointer_id); |
| if (it != active_pointer_ids_.end()) { |
| active_pointer_ids_.erase(it); |
| } |
| |
| auto it2 = active_pointer_state_.find(event->pointer_data->pointer_id); |
| if (it2 != active_pointer_state_.end()) { |
| active_pointer_state_.erase(it2); |
| } |
| if (event->action == mojo::EventType::POINTER_DOWN) { |
| active_pointer_ids_.push_back(event->pointer_data->pointer_id); |
| PointerState* pointer_state = new PointerState(); |
| pointer_state->last_key = nullptr; |
| pointer_state->last_point = gfx::PointF(); |
| pointer_state->last_point_valid = false; |
| active_pointer_state_[event->pointer_data->pointer_id].reset( |
| pointer_state); |
| } |
| } |
| |
| auto it2 = active_pointer_state_.find(event->pointer_data->pointer_id); |
| if (it2 != active_pointer_state_.end()) { |
| active_pointer_state_[event->pointer_data->pointer_id]->last_key = |
| key_layout_.GetKeyAtPoint(point); |
| } |
| |
| UpdateState(event->pointer_data->pointer_id, event->action, point); |
| IssueDraw(); |
| } |
| } |
| |
| void ViewObserverDelegate::OnViewDestroyed(mojo::View* view) { |
| if (view_ == view) { |
| view_->RemoveObserver(this); |
| view_ = nullptr; |
| gl_context_->Destroy(); |
| } |
| } |
| |
| } // namespace keyboard |