chromium/ppapi/examples/ime/ime.cc

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include <stddef.h>
#include <stdint.h>

#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/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"
#include "ppapi/cpp/trusted/browser_font_trusted.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, static_cast<uint32_t>(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::BrowserFontDescription desc;
    desc.set_family(PP_BROWSERFONT_TRUSTED_FAMILY_SANSSERIF);
    desc.set_size(font_size_);
    font_ = pp::BrowserFont_Trusted(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::BrowserFontTextRun(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::BrowserFontTextRun(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::BrowserFontTextRun(str.c_str(), false, false),
            pp::Point(offset, area_.y() + font_size_),
            kTextfieldTextColor,
            clip,
            false);
      }
    } else {
      font_.DrawTextAt(
          image,
          pp::BrowserFontTextRun(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::BrowserFontTextRun(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::BrowserFontTextRun(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::BrowserFont_Trusted 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