1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146

pdf / pdf_ink_undo_redo_model.h [blame]

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

#ifndef PDF_PDF_INK_UNDO_REDO_MODEL_H_
#define PDF_PDF_INK_UNDO_REDO_MODEL_H_

#include <stddef.h>

#include <optional>
#include <set>
#include <vector>

#include "base/types/strong_alias.h"
#include "pdf/buildflags.h"
#include "pdf/pdf_ink_ids.h"
#include "third_party/abseil-cpp/absl/types/variant.h"

static_assert(BUILDFLAG(ENABLE_PDF_INK2), "ENABLE_PDF_INK2 not set to true");

namespace chrome_pdf {

// Models draw and erase commands. Based on the recorded commands,
// processes undo / redo requests and calculates what commands need to be
// applied.
class PdfInkUndoRedoModel {
 public:
  enum class CommandsType {
    kNone,
    kDraw,
    kErase,
  };

  // Set of IDs to draw/erase. There are multiple types of IDs:
  // - `InkStrokeId` is for strokes that are first drawn, and maybe erased
  //   later.
  // - `InkModeledShapeId` is for modeled shapes that are pre-existing and can
  //   be erased.
  using IdType = absl::variant<InkStrokeId, InkModeledShapeId>;
  using DrawCommands =
      base::StrongAlias<class DrawCommandsTag, std::set<IdType>>;
  using EraseCommands =
      base::StrongAlias<class EraseCommandsTag, std::set<IdType>>;

  using Commands = absl::variant<absl::monostate, DrawCommands, EraseCommands>;

  // Set of IDs used for drawing to discard. This does not use `IdType`, because
  // model shapes are pre-existing and cannot be discarded.
  using DiscardedDrawCommands = std::set<InkStrokeId>;

  PdfInkUndoRedoModel();
  PdfInkUndoRedoModel(const PdfInkUndoRedoModel&) = delete;
  PdfInkUndoRedoModel& operator=(const PdfInkUndoRedoModel&) = delete;
  ~PdfInkUndoRedoModel();

  // For all Draw / Erase methods:
  // - The expected usage is: 1 StartOp call, any number of Op(Variant) calls,
  //   1 FinishOp call.
  // - StartOp returns a non-null, but possible empty value on success. Returns
  //   nullopt if any requirements are not met.
  // - Op(Variant) and FinishOp return true on success. Return false if any
  //   requirements are not met.
  // - Must not return false in production code. Returning false is only allowed
  //   in tests to check failure modes without resorting to death tests.

  // Starts recording draw commands. If the current commands stack position is
  // not at the top of the stack, then this discards all entries from the
  // current position to the top of the stack. The caller can discard its
  // entries with IDs that match the returned values.
  // Must be called before Draw().
  // Must not be called while another draw/erase has been started.
  [[nodiscard]] std::optional<DiscardedDrawCommands> StartDraw();
  // Records drawing a stroke identified by `id`.
  // Must be called between StartDraw() and FinishDraw().
  // `id` must not be on the commands stack.
  [[nodiscard]] bool Draw(InkStrokeId id);
  // Finishes recording draw commands and pushes a new element onto the stack.
  // Must be called after StartDraw().
  [[nodiscard]] bool FinishDraw();

  // Starts recording erase commands. If the current commands stack position is
  // not at the top of the stack, then this discards all entries from the
  // current position to the top of the stack. The caller can discard its
  // entries with IDs that match the returned values.
  // Must be called before EraseStroke() and EraseShape().
  // Must not be called while another draw/erase has been started.
  [[nodiscard]] std::optional<DiscardedDrawCommands> StartErase();
  // Records erasing a stroke identified by `id`.
  // Must be called between StartErase() and FinishErase().
  // `id` must be in a `DrawCommands` on the commands stack.
  // `id` must not be in any `EraseCommands` on the commands stack.
  [[nodiscard]] bool EraseStroke(InkStrokeId id);
  // Records erasing a shape identified by `id`.
  // Must be called between StartErase() and FinishErase().
  // `id` must not be in any `EraseCommands` on the commands stack.
  // Unlike EraseStroke(), EraseShape() has no corresponding draw method, so it
  // relies on the caller to pass in valid `id` values. If the caller passes in
  // invalid values, `PdfInkUndoRedoModel` will faithfully give them back during
  // undo/redo operations.
  [[nodiscard]] bool EraseShape(InkModeledShapeId id);
  // Finishes recording erase commands and pushes a new element onto the stack.
  // Must be called after StartErase().
  [[nodiscard]] bool FinishErase();

  // Returns the commands that needs to be applied to satisfy the undo / redo
  // request and moves the position in the commands stack without modifying the
  // commands themselves.
  Commands Undo();
  Commands Redo();

  static CommandsType GetCommandsType(const Commands& commands);
  static const DrawCommands& GetDrawCommands(const Commands& commands);
  static const EraseCommands& GetEraseCommands(const Commands& commands);

 private:
  template <typename T>
  std::optional<DiscardedDrawCommands> StartImpl();

  bool IsAtTopOfStackWithGivenCommandType(CommandsType type) const;
  bool HasIdInDrawCommands(IdType id) const;
  bool HasIdInEraseCommands(IdType id) const;

  // Invariants:
  // (1) Never empty.
  // (2) The last element and only the last element can be `absl::monostate`.
  // (3) IDs used in `DrawCommands` elements are unique among all `DrawCommands`
  //     elements.
  // (4) IDs added to a `DrawCommands` must not exist in any `EraseCommands`.
  // (5) IDs used in `EraseCommands` elements are unique among all
  //     `EraseCommands` elements.
  // (6) IDs added to a `EraseCommands` must exist in some `DrawCommands`
  //     element.
  // (7) `DrawCommands` only contains `InkStrokeId` elements here. The reason
  //     `DrawCommands` can hold `InkModeledShapeId` is to undo an
  //     `InkModeledShapeId` erasure, where the caller needs to know they need
  //     to draw the shape.
  std::vector<Commands> commands_stack_ = {absl::monostate()};

  // Invariants:
  // (8) Always less than the size of `commands_stack_`.
  size_t stack_position_ = 0;
};

}  // namespace chrome_pdf

#endif  // PDF_PDF_INK_UNDO_REDO_MODEL_H_