// Copyright (c) 2012 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 <algorithm> #include <string> #include <utility> #include <vector> #include "ppapi/c/dev/ppb_cursor_control_dev.h" #include "ppapi/c/ppb_console.h" #include "ppapi/cpp/completion_callback.h" #include "ppapi/cpp/dev/font_dev.h" #include "ppapi/cpp/graphics_2d.h" #include "ppapi/cpp/image_data.h" #include "ppapi/cpp/input_event.h" #include "ppapi/cpp/instance.h" #include "ppapi/cpp/module.h" #include "ppapi/cpp/rect.h" #include "ppapi/cpp/size.h" #include "ppapi/cpp/text_input_controller.h" namespace { // Extracted from: ui/events/keycodes/keyboard_codes.h enum { VKEY_BACK = 0x08, VKEY_SHIFT = 0x10, VKEY_DELETE = 0x2E, VKEY_LEFT = 0x25, VKEY_UP = 0x26, VKEY_RIGHT = 0x27, VKEY_DOWN = 0x28, }; const uint32_t kTextfieldBgColor = 0xffffffff; const uint32_t kTextfieldTextColor = 0xff000000; const uint32_t kTextfieldCaretColor = 0xff000000; const uint32_t kTextfieldPreeditTextColor = 0xffff0000; const uint32_t kTextfieldSelectionBackgroundColor = 0xffeecccc; const uint32_t kTextfieldUnderlineColorMain = 0xffff0000; const uint32_t kTextfieldUnderlineColorSub = 0xffddaaaa; void FillRect(pp::ImageData* image, int left, int top, int width, int height, uint32_t color) { for (int y = std::max(0, top); y < std::min(image->size().height() - 1, top + height); ++y) { for (int x = std::max(0, left); x < std::min(image->size().width() - 1, left + width); ++x) *image->GetAddr32(pp::Point(x, y)) = color; } } void FillRect(pp::ImageData* image, const pp::Rect& rect, uint32_t color) { FillRect(image, rect.x(), rect.y(), rect.width(), rect.height(), color); } size_t GetPrevCharOffsetUtf8(const std::string& str, size_t current_pos) { size_t i = current_pos; if (i > 0) { do --i; while (i > 0 && (str[i] & 0xc0) == 0x80); } return i; } size_t GetNextCharOffsetUtf8(const std::string& str, size_t current_pos) { size_t i = current_pos; if (i < str.size()) { do ++i; while (i < str.size() && (str[i] & 0xc0) == 0x80); } return i; } size_t GetNthCharOffsetUtf8(const std::string& str, size_t n) { size_t i = 0; for (size_t step = 0; step < n; ++step) i = GetNextCharOffsetUtf8(str, i); return i; } } // namespace class TextFieldStatusHandler { public: virtual ~TextFieldStatusHandler() {} virtual void FocusIn(const pp::Rect& caret) {} virtual void FocusOut() {} virtual void UpdateSelection(const std::string& text) {} }; class TextFieldStatusNotifyingHandler : public TextFieldStatusHandler { public: explicit TextFieldStatusNotifyingHandler(pp::Instance* instance) : textinput_control_(instance) { } protected: // Implement TextFieldStatusHandler. virtual void FocusIn(const pp::Rect& caret) { textinput_control_.SetTextInputType(PP_TEXTINPUT_TYPE_TEXT); textinput_control_.UpdateCaretPosition(caret); } virtual void FocusOut() { textinput_control_.CancelCompositionText(); textinput_control_.SetTextInputType(PP_TEXTINPUT_TYPE_NONE); } virtual void UpdateSelection(const std::string& text) { textinput_control_.UpdateSurroundingText(text, 0, text.size()); } private: pp::TextInputController textinput_control_; }; // Hand-made text field for demonstrating text input API. class MyTextField { public: MyTextField(pp::Instance* instance, TextFieldStatusHandler* handler, int x, int y, int width, int height) : instance_(instance), status_handler_(handler), area_(x, y, width, height), font_size_(height - 2), caret_pos_(std::string::npos), anchor_pos_(std::string::npos), target_segment_(0) { pp::FontDescription_Dev desc; desc.set_family(PP_FONTFAMILY_SANSSERIF); desc.set_size(font_size_); font_ = pp::Font_Dev(instance_, desc); } // Paint on the specified ImageData. void PaintOn(pp::ImageData* image, pp::Rect clip) { clip = clip.Intersect(area_); FillRect(image, clip, kTextfieldBgColor); if (caret_pos_ != std::string::npos) { int offset = area_.x(); // selection (for the case without composition text) if (composition_.empty() && HasSelection()) { int left_x = font_.MeasureSimpleText( utf8_text_.substr(0, SelectionLeft())); int right_x = font_.MeasureSimpleText( utf8_text_.substr(0, SelectionRight())); FillRect(image, offset + left_x, area_.y(), right_x - left_x, area_.height(), kTextfieldSelectionBackgroundColor); } // before caret { std::string str = utf8_text_.substr(0, caret_pos_); font_.DrawTextAt( image, pp::TextRun_Dev(str.c_str(), false, false), pp::Point(offset, area_.y() + font_size_), kTextfieldTextColor, clip, false); offset += font_.MeasureSimpleText(str); } // composition { const std::string& str = composition_; // selection if (composition_selection_.first != composition_selection_.second) { int left_x = font_.MeasureSimpleText( str.substr(0, composition_selection_.first)); int right_x = font_.MeasureSimpleText( str.substr(0, composition_selection_.second)); FillRect(image, offset + left_x, area_.y(), right_x - left_x, area_.height(), kTextfieldSelectionBackgroundColor); } // composition text font_.DrawTextAt( image, pp::TextRun_Dev(str.c_str(), false, false), pp::Point(offset, area_.y() + font_size_), kTextfieldPreeditTextColor, clip, false); for (size_t i = 0; i < segments_.size(); ++i) { size_t l = segments_[i].first; size_t r = segments_[i].second; if (l != r) { int lx = font_.MeasureSimpleText(str.substr(0, l)); int rx = font_.MeasureSimpleText(str.substr(0, r)); FillRect(image, offset + lx + 2, area_.y() + font_size_ + 1, rx - lx - 4, 2, i == static_cast<size_t>(target_segment_) ? kTextfieldUnderlineColorMain : kTextfieldUnderlineColorSub); } } // caret int caretx = font_.MeasureSimpleText( str.substr(0, composition_selection_.first)); FillRect(image, pp::Rect(offset + caretx, area_.y(), 2, area_.height()), kTextfieldCaretColor); offset += font_.MeasureSimpleText(str); } // after caret { std::string str = utf8_text_.substr(caret_pos_); font_.DrawTextAt( image, pp::TextRun_Dev(str.c_str(), false, false), pp::Point(offset, area_.y() + font_size_), kTextfieldTextColor, clip, false); } } else { font_.DrawTextAt( image, pp::TextRun_Dev(utf8_text_.c_str(), false, false), pp::Point(area_.x(), area_.y() + font_size_), kTextfieldTextColor, clip, false); } } // Update current composition text. void SetComposition( const std::string& text, const std::vector< std::pair<uint32_t, uint32_t> >& segments, int32_t target_segment, const std::pair<uint32_t, uint32_t>& selection) { if (HasSelection() && !text.empty()) InsertText(std::string()); composition_ = text; segments_ = segments; target_segment_ = target_segment; composition_selection_ = selection; CaretPosChanged(); } // Is the text field focused? bool Focused() const { return caret_pos_ != std::string::npos; } // Does the coordinate (x,y) is contained inside the edit box? bool Contains(int x, int y) const { return area_.Contains(x, y); } // Resets the content text. void SetText(const std::string& text) { utf8_text_ = text; if (Focused()) { caret_pos_ = anchor_pos_ = text.size(); CaretPosChanged(); } } // Inserts a text at the current caret position. void InsertText(const std::string& text) { if (!Focused()) return; utf8_text_.replace(SelectionLeft(), SelectionRight() - SelectionLeft(), text); caret_pos_ = anchor_pos_ = SelectionLeft() + text.size(); CaretPosChanged(); } // Handles mouse click event and changes the focus state. bool RefocusByMouseClick(int x, int y) { if (!Contains(x, y)) { // The text field is unfocused. caret_pos_ = anchor_pos_ = std::string::npos; return false; } // The text field is focused. size_t n = font_.CharacterOffsetForPixel( pp::TextRun_Dev(utf8_text_.c_str()), x - area_.x()); caret_pos_ = anchor_pos_ = GetNthCharOffsetUtf8(utf8_text_, n); CaretPosChanged(); return true; } void MouseDrag(int x, int y) { if (!Focused()) return; size_t n = font_.CharacterOffsetForPixel( pp::TextRun_Dev(utf8_text_.c_str()), x - area_.x()); caret_pos_ = GetNthCharOffsetUtf8(utf8_text_, n); } void MouseUp(int x, int y) { if (!Focused()) return; CaretPosChanged(); } void KeyLeft(bool shift) { if (!Focused()) return; // Move caret to the head of the selection or to the previous character. if (!shift && HasSelection()) caret_pos_ = SelectionLeft(); else caret_pos_ = GetPrevCharOffsetUtf8(utf8_text_, caret_pos_); // Move the anchor if the shift key is not pressed. if (!shift) anchor_pos_ = caret_pos_; CaretPosChanged(); } void KeyRight(bool shift) { if (!Focused()) return; // Move caret to the end of the selection or to the next character. if (!shift && HasSelection()) caret_pos_ = SelectionRight(); else caret_pos_ = GetNextCharOffsetUtf8(utf8_text_, caret_pos_); // Move the anchor if the shift key is not pressed. if (!shift) anchor_pos_ = caret_pos_; CaretPosChanged(); } void KeyDelete() { if (!Focused()) return; if (HasSelection()) { InsertText(std::string()); } else { size_t i = GetNextCharOffsetUtf8(utf8_text_, caret_pos_); utf8_text_.erase(caret_pos_, i - caret_pos_); CaretPosChanged(); } } void KeyBackspace() { if (!Focused()) return; if (HasSelection()) { InsertText(std::string()); } else if (caret_pos_ != 0) { size_t i = GetPrevCharOffsetUtf8(utf8_text_, caret_pos_); utf8_text_.erase(i, caret_pos_ - i); caret_pos_ = anchor_pos_ = i; CaretPosChanged(); } } private: // Notify the plugin instance that the caret position has changed. void CaretPosChanged() { if (Focused()) { std::string str = utf8_text_.substr(0, caret_pos_); if (!composition_.empty()) str += composition_.substr(0, composition_selection_.first); int px = font_.MeasureSimpleText(str); pp::Rect caret(area_.x() + px, area_.y(), 0, area_.height() + 2); status_handler_->FocusIn(caret); status_handler_->UpdateSelection( utf8_text_.substr(SelectionLeft(), SelectionRight() - SelectionLeft())); } } size_t SelectionLeft() const { return std::min(caret_pos_, anchor_pos_); } size_t SelectionRight() const { return std::max(caret_pos_, anchor_pos_); } bool HasSelection() const { return caret_pos_ != anchor_pos_; } pp::Instance* instance_; TextFieldStatusHandler* status_handler_; pp::Rect area_; int font_size_; pp::Font_Dev font_; std::string utf8_text_; size_t caret_pos_; size_t anchor_pos_; std::string composition_; std::vector< std::pair<uint32_t, uint32_t> > segments_; std::pair<uint32_t, uint32_t> composition_selection_; int target_segment_; }; class MyInstance : public pp::Instance { public: explicit MyInstance(PP_Instance instance) : pp::Instance(instance), status_handler_(new TextFieldStatusHandler), dragging_(false) { } ~MyInstance() { delete status_handler_; } virtual bool Init(uint32_t argc, const char* argn[], const char* argv[]) { RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE); RequestFilteringInputEvents(PP_INPUTEVENT_CLASS_KEYBOARD); for (uint32_t i = 0; i < argc; ++i) { if (argn[i] == std::string("ime")) { if (argv[i] == std::string("no")) { // Example of NO-IME plugins (e.g., games). // // When a plugin never wants to accept text input, at initialization // explicitly turn off the text input feature by calling: pp::TextInputController(this).SetTextInputType( PP_TEXTINPUT_TYPE_NONE); } else if (argv[i] == std::string("unaware")) { // Demonstrating the behavior of IME-unaware plugins. // Never call any text input related APIs. // // In such a case, the plugin is assumed to always accept text input. // For example, when the plugin is focused in touch devices a virtual // keyboard may pop up, or in environment IME is used, users can type // text via IME on the plugin. The characters are delivered to the // plugin via PP_INPUTEVENT_TYPE_CHAR events. } else if (argv[i] == std::string("caretmove")) { // Demonstrating the behavior of plugins with limited IME support. // // It uses SetTextInputType() and UpdateCaretPosition() API to notify // text input status to the browser, but unable to handle inline // compositions. By using the notified information. the browser can, // say, show virtual keyboards or IMEs only at appropriate timing // that the plugin does need to accept text input. delete status_handler_; status_handler_ = new TextFieldStatusNotifyingHandler(this); } else if (argv[i] == std::string("full")) { // Demonstrating the behavior of plugins fully supporting IME. // // It notifies updates of caret positions to the browser, // and handles all text input events by itself. delete status_handler_; status_handler_ = new TextFieldStatusNotifyingHandler(this); RequestInputEvents(PP_INPUTEVENT_CLASS_IME); } break; } } textfield_.push_back(MyTextField(this, status_handler_, 10, 10, 300, 20)); textfield_.back().SetText("Hello"); textfield_.push_back(MyTextField(this, status_handler_, 30, 100, 300, 20)); textfield_.back().SetText("World"); return true; } protected: virtual bool HandleInputEvent(const pp::InputEvent& event) { bool ret = false; switch (event.GetType()) { case PP_INPUTEVENT_TYPE_MOUSEDOWN: { const pp::MouseInputEvent mouseEvent(event); ret = OnMouseDown(mouseEvent); break; } case PP_INPUTEVENT_TYPE_MOUSEMOVE: { const pp::MouseInputEvent mouseEvent(event); ret = OnMouseMove(mouseEvent); break; } case PP_INPUTEVENT_TYPE_MOUSEUP: { const pp::MouseInputEvent mouseEvent(event); ret = OnMouseUp(mouseEvent); break; } case PP_INPUTEVENT_TYPE_KEYDOWN: { Log("Keydown"); const pp::KeyboardInputEvent keyEvent(event); ret = OnKeyDown(keyEvent); break; } case PP_INPUTEVENT_TYPE_CHAR: { const pp::KeyboardInputEvent keyEvent(event); Log("Char [" + keyEvent.GetCharacterText().AsString() + "]"); ret = OnChar(keyEvent); break; } case PP_INPUTEVENT_TYPE_IME_COMPOSITION_START: { const pp::IMEInputEvent imeEvent(event); Log("CompositionStart [" + imeEvent.GetText().AsString() + "]"); ret = true; break; } case PP_INPUTEVENT_TYPE_IME_COMPOSITION_UPDATE: { const pp::IMEInputEvent imeEvent(event); Log("CompositionUpdate [" + imeEvent.GetText().AsString() + "]"); ret = OnCompositionUpdate(imeEvent); break; } case PP_INPUTEVENT_TYPE_IME_COMPOSITION_END: { const pp::IMEInputEvent imeEvent(event); Log("CompositionEnd [" + imeEvent.GetText().AsString() + "]"); ret = OnCompositionEnd(imeEvent); break; } case PP_INPUTEVENT_TYPE_IME_TEXT: { const pp::IMEInputEvent imeEvent(event); Log("ImeText [" + imeEvent.GetText().AsString() + "]"); ret = OnImeText(imeEvent); break; } default: break; } if (ret && (dragging_ || event.GetType() != PP_INPUTEVENT_TYPE_MOUSEMOVE)) Paint(); return ret; } virtual void DidChangeView(const pp::Rect& position, const pp::Rect& clip) { if (position.size() == last_size_) return; last_size_ = position.size(); Paint(); } private: bool OnCompositionUpdate(const pp::IMEInputEvent& ev) { for (std::vector<MyTextField>::iterator it = textfield_.begin(); it != textfield_.end(); ++it) { if (it->Focused()) { std::vector< std::pair<uint32_t, uint32_t> > segs; for (uint32_t i = 0; i < ev.GetSegmentNumber(); ++i) segs.push_back(std::make_pair(ev.GetSegmentOffset(i), ev.GetSegmentOffset(i + 1))); uint32_t selection_start; uint32_t selection_end; ev.GetSelection(&selection_start, &selection_end); it->SetComposition(ev.GetText().AsString(), segs, ev.GetTargetSegment(), std::make_pair(selection_start, selection_end)); return true; } } return false; } bool OnCompositionEnd(const pp::IMEInputEvent& ev) { for (std::vector<MyTextField>::iterator it = textfield_.begin(); it != textfield_.end(); ++it) { if (it->Focused()) { it->SetComposition(std::string(), std::vector<std::pair<uint32_t, uint32_t> >(), 0, std::make_pair(0, 0)); return true; } } return false; } bool OnMouseDown(const pp::MouseInputEvent& ev) { dragging_ = true; bool anyone_focused = false; for (std::vector<MyTextField>::iterator it = textfield_.begin(); it != textfield_.end(); ++it) { if (it->RefocusByMouseClick(ev.GetPosition().x(), ev.GetPosition().y())) { anyone_focused = true; } } if (!anyone_focused) status_handler_->FocusOut(); return true; } bool OnMouseMove(const pp::MouseInputEvent& ev) { const PPB_CursorControl_Dev* cursor_control = reinterpret_cast<const PPB_CursorControl_Dev*>( pp::Module::Get()->GetBrowserInterface( PPB_CURSOR_CONTROL_DEV_INTERFACE)); if (!cursor_control) return false; for (std::vector<MyTextField>::iterator it = textfield_.begin(); it != textfield_.end(); ++it) { if (it->Contains(ev.GetPosition().x(), ev.GetPosition().y())) { cursor_control->SetCursor(pp_instance(), PP_CURSORTYPE_IBEAM, 0, NULL); if (it->Focused() && dragging_) it->MouseDrag(ev.GetPosition().x(), ev.GetPosition().y()); return true; } } cursor_control->SetCursor(pp_instance(), PP_CURSORTYPE_POINTER, 0, NULL); return true; } bool OnMouseUp(const pp::MouseInputEvent& ev) { dragging_ = false; for (std::vector<MyTextField>::iterator it = textfield_.begin(); it != textfield_.end(); ++it) if (it->Focused()) it->MouseUp(ev.GetPosition().x(), ev.GetPosition().y()); return false; } bool OnKeyDown(const pp::KeyboardInputEvent& ev) { for (std::vector<MyTextField>::iterator it = textfield_.begin(); it != textfield_.end(); ++it) { if (it->Focused()) { bool shift = ev.GetModifiers() & PP_INPUTEVENT_MODIFIER_SHIFTKEY; switch (ev.GetKeyCode()) { case VKEY_LEFT: it->KeyLeft(shift); break; case VKEY_RIGHT: it->KeyRight(shift); break; case VKEY_DELETE: it->KeyDelete(); break; case VKEY_BACK: it->KeyBackspace(); break; } return true; } } return false; } bool OnChar(const pp::KeyboardInputEvent& ev) { for (std::vector<MyTextField>::iterator it = textfield_.begin(); it != textfield_.end(); ++it) { if (it->Focused()) { std::string str = ev.GetCharacterText().AsString(); if (str != "\r" && str != "\n") it->InsertText(str); return true; } } return false; } bool OnImeText(const pp::IMEInputEvent ev) { for (std::vector<MyTextField>::iterator it = textfield_.begin(); it != textfield_.end(); ++it) { if (it->Focused()) { it->InsertText(ev.GetText().AsString()); return true; } } return false; } void Paint() { pp::Rect clip(0, 0, last_size_.width(), last_size_.height()); PaintClip(clip); } void PaintClip(const pp::Rect& clip) { pp::ImageData image(this, PP_IMAGEDATAFORMAT_BGRA_PREMUL, last_size_, true); pp::Graphics2D device(this, last_size_, false); BindGraphics(device); for (std::vector<MyTextField>::iterator it = textfield_.begin(); it != textfield_.end(); ++it) { it->PaintOn(&image, clip); } device.PaintImageData(image, pp::Point(0, 0)); device.Flush(pp::CompletionCallback(&OnFlush, this)); } static void OnFlush(void* user_data, int32_t result) {} // Prints a debug message. void Log(const pp::Var& value) { const PPB_Console* console = reinterpret_cast<const PPB_Console*>( pp::Module::Get()->GetBrowserInterface(PPB_CONSOLE_INTERFACE)); if (!console) return; console->Log(pp_instance(), PP_LOGLEVEL_LOG, value.pp_var()); } // IME Control interface. TextFieldStatusHandler* status_handler_; // Remembers the size of this instance. pp::Size last_size_; // Holds instances of text fields. std::vector<MyTextField> textfield_; // Whether or not during a drag operation. bool dragging_; }; class MyModule : public pp::Module { virtual pp::Instance* CreateInstance(PP_Instance instance) { return new MyInstance(instance); } }; namespace pp { Module* CreateModule() { return new MyModule(); } } // namespace pp