| // Copyright 2017 The PDFium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com |
| |
| #include "xfa/fde/cfde_texteditengine.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "core/fxcrt/fx_extension.h" |
| #include "core/fxcrt/span_util.h" |
| #include "core/fxge/text_char_pos.h" |
| #include "third_party/base/check.h" |
| #include "third_party/base/notreached.h" |
| #include "third_party/base/numerics/safe_conversions.h" |
| #include "xfa/fde/cfde_textout.h" |
| #include "xfa/fde/cfde_wordbreak_data.h" |
| #include "xfa/fgas/font/cfgas_gefont.h" |
| |
| namespace { |
| |
| class InsertOperation final : public CFDE_TextEditEngine::Operation { |
| public: |
| InsertOperation(CFDE_TextEditEngine* engine, |
| size_t start_idx, |
| const WideString& added_text) |
| : engine_(engine), start_idx_(start_idx), added_text_(added_text) {} |
| |
| ~InsertOperation() override = default; |
| |
| void Redo() const override { |
| engine_->Insert(start_idx_, added_text_, |
| CFDE_TextEditEngine::RecordOperation::kSkipRecord); |
| } |
| |
| void Undo() const override { |
| engine_->Delete(start_idx_, added_text_.GetLength(), |
| CFDE_TextEditEngine::RecordOperation::kSkipRecord); |
| } |
| |
| private: |
| UnownedPtr<CFDE_TextEditEngine> engine_; |
| size_t start_idx_; |
| WideString added_text_; |
| }; |
| |
| class DeleteOperation final : public CFDE_TextEditEngine::Operation { |
| public: |
| DeleteOperation(CFDE_TextEditEngine* engine, |
| size_t start_idx, |
| const WideString& removed_text) |
| : engine_(engine), start_idx_(start_idx), removed_text_(removed_text) {} |
| |
| ~DeleteOperation() override = default; |
| |
| void Redo() const override { |
| engine_->Delete(start_idx_, removed_text_.GetLength(), |
| CFDE_TextEditEngine::RecordOperation::kSkipRecord); |
| } |
| |
| void Undo() const override { |
| engine_->Insert(start_idx_, removed_text_, |
| CFDE_TextEditEngine::RecordOperation::kSkipRecord); |
| } |
| |
| private: |
| UnownedPtr<CFDE_TextEditEngine> engine_; |
| size_t start_idx_; |
| WideString removed_text_; |
| }; |
| |
| class ReplaceOperation final : public CFDE_TextEditEngine::Operation { |
| public: |
| ReplaceOperation(CFDE_TextEditEngine* engine, |
| size_t start_idx, |
| const WideString& removed_text, |
| const WideString& added_text) |
| : insert_op_(engine, start_idx, added_text), |
| delete_op_(engine, start_idx, removed_text) {} |
| |
| ~ReplaceOperation() override = default; |
| |
| void Redo() const override { |
| delete_op_.Redo(); |
| insert_op_.Redo(); |
| } |
| |
| void Undo() const override { |
| insert_op_.Undo(); |
| delete_op_.Undo(); |
| } |
| |
| private: |
| InsertOperation insert_op_; |
| DeleteOperation delete_op_; |
| }; |
| |
| int GetBreakFlagsFor(WordBreakProperty current, WordBreakProperty next) { |
| if (current == WordBreakProperty::kMidLetter) { |
| if (next == WordBreakProperty::kALetter) |
| return 1; |
| } else if (current == WordBreakProperty::kMidNum) { |
| if (next == WordBreakProperty::kNumeric) |
| return 2; |
| } else if (current == WordBreakProperty::kMidNumLet) { |
| if (next == WordBreakProperty::kALetter) |
| return 1; |
| if (next == WordBreakProperty::kNumeric) |
| return 2; |
| } |
| return 0; |
| } |
| |
| bool BreakFlagsChanged(int flags, WordBreakProperty previous) { |
| return (flags != 1 || previous != WordBreakProperty::kALetter) && |
| (flags != 2 || previous != WordBreakProperty::kNumeric); |
| } |
| |
| } // namespace |
| |
| CFDE_TextEditEngine::CFDE_TextEditEngine() { |
| content_.resize(gap_size_); |
| operation_buffer_.resize(max_edit_operations_); |
| |
| text_break_.SetFontSize(font_size_); |
| text_break_.SetLineBreakTolerance(2.0f); |
| text_break_.SetTabWidth(36); |
| } |
| |
| CFDE_TextEditEngine::~CFDE_TextEditEngine() = default; |
| |
| void CFDE_TextEditEngine::Clear() { |
| text_length_ = 0; |
| gap_position_ = 0; |
| gap_size_ = kGapSize; |
| |
| content_.clear(); |
| content_.resize(gap_size_); |
| |
| ClearSelection(); |
| ClearOperationRecords(); |
| } |
| |
| void CFDE_TextEditEngine::SetMaxEditOperationsForTesting(size_t max) { |
| max_edit_operations_ = max; |
| operation_buffer_.resize(max); |
| |
| ClearOperationRecords(); |
| } |
| |
| void CFDE_TextEditEngine::AdjustGap(size_t idx, size_t length) { |
| static const size_t char_size = sizeof(WideString::CharType); |
| |
| // Move the gap, if necessary. |
| if (idx < gap_position_) { |
| memmove(content_.data() + idx + gap_size_, content_.data() + idx, |
| (gap_position_ - idx) * char_size); |
| gap_position_ = idx; |
| } else if (idx > gap_position_) { |
| memmove(content_.data() + gap_position_, |
| content_.data() + gap_position_ + gap_size_, |
| (idx - gap_position_) * char_size); |
| gap_position_ = idx; |
| } |
| |
| // If the gap is too small, make it bigger. |
| if (length >= gap_size_) { |
| size_t new_gap_size = length + kGapSize; |
| content_.resize(text_length_ + new_gap_size); |
| |
| memmove(content_.data() + gap_position_ + new_gap_size, |
| content_.data() + gap_position_ + gap_size_, |
| (text_length_ - gap_position_) * char_size); |
| |
| gap_size_ = new_gap_size; |
| } |
| } |
| |
| size_t CFDE_TextEditEngine::CountCharsExceedingSize(const WideString& text, |
| size_t num_to_check) { |
| if (!limit_horizontal_area_ && !limit_vertical_area_) |
| return 0; |
| |
| auto text_out = std::make_unique<CFDE_TextOut>(); |
| text_out->SetLineSpace(line_spacing_); |
| text_out->SetFont(font_); |
| text_out->SetFontSize(font_size_); |
| |
| FDE_TextStyle style; |
| style.single_line_ = !is_multiline_; |
| |
| CFX_RectF text_rect; |
| if (is_linewrap_enabled_) { |
| style.line_wrap_ = true; |
| text_rect.width = available_width_; |
| } else { |
| text_rect.width = kPageWidthMax; |
| } |
| text_out->SetStyles(style); |
| |
| size_t length = text.GetLength(); |
| WideStringView temp = text.AsStringView(); |
| |
| float vertical_height = line_spacing_ * visible_line_count_; |
| size_t chars_exceeding_size = 0; |
| // TODO(dsinclair): Can this get changed to a binary search? |
| for (size_t i = 0; i < num_to_check; i++) { |
| text_out->CalcLogicSize(temp, &text_rect); |
| if (limit_horizontal_area_ && text_rect.width <= available_width_) |
| break; |
| if (limit_vertical_area_ && text_rect.height <= vertical_height) |
| break; |
| |
| ++chars_exceeding_size; |
| |
| --length; |
| temp = temp.First(length); |
| } |
| |
| return chars_exceeding_size; |
| } |
| |
| void CFDE_TextEditEngine::Insert(size_t idx, |
| const WideString& request_text, |
| RecordOperation add_operation) { |
| WideString text = request_text; |
| if (text.IsEmpty()) |
| return; |
| |
| idx = std::min(idx, text_length_); |
| |
| TextChange change; |
| change.selection_start = idx; |
| change.selection_end = idx; |
| change.text = text; |
| change.previous_text = GetText(); |
| change.cancelled = false; |
| |
| if (delegate_ && (add_operation != RecordOperation::kSkipRecord && |
| add_operation != RecordOperation::kSkipNotify)) { |
| delegate_->OnTextWillChange(&change); |
| if (change.cancelled) |
| return; |
| |
| text = change.text; |
| idx = change.selection_start; |
| |
| // Delegate extended the selection, so delete it before we insert. |
| if (change.selection_end != change.selection_start) |
| DeleteSelectedText(RecordOperation::kSkipRecord); |
| |
| // Delegate may have changed text entirely, recheck. |
| idx = std::min(idx, text_length_); |
| } |
| |
| size_t length = text.GetLength(); |
| if (length == 0) |
| return; |
| |
| // If we're going to be too big we insert what we can and notify the |
| // delegate we've filled the text after the insert is done. |
| bool exceeded_limit = false; |
| |
| // Currently we allow inserting a number of characters over the text limit if |
| // we're skipping notify. This means we're setting the formatted text into the |
| // engine. Otherwise, if you enter 123456789 for an SSN into a field |
| // with a 9 character limit and we reformat to 123-45-6789 we'll truncate |
| // the 89 when inserting into the text edit. See https://crbug.com/pdfium/1089 |
| if (has_character_limit_ && text_length_ + length > character_limit_) { |
| if (add_operation == RecordOperation::kSkipNotify) { |
| // Raise the limit to allow subsequent changes to expanded text. |
| character_limit_ = text_length_ + length; |
| } else { |
| // Truncate the text to comply with the limit. |
| CHECK(text_length_ <= character_limit_); |
| length = character_limit_ - text_length_; |
| exceeded_limit = true; |
| } |
| } |
| AdjustGap(idx, length); |
| |
| if (validation_enabled_ || limit_horizontal_area_ || limit_vertical_area_) { |
| WideString str; |
| if (gap_position_ > 0) |
| str += WideStringView(content_.data(), gap_position_); |
| |
| str += text; |
| |
| if (text_length_ - gap_position_ > 0) { |
| str += WideStringView(content_.data() + gap_position_ + gap_size_, |
| text_length_ - gap_position_); |
| } |
| |
| if (validation_enabled_ && delegate_ && !delegate_->OnValidate(str)) { |
| // TODO(dsinclair): Notify delegate of validation failure? |
| return; |
| } |
| |
| // Check if we've limited the horizontal/vertical area, and if so determine |
| // how many of our characters would be outside the area. |
| size_t chars_exceeding = CountCharsExceedingSize(str, length); |
| if (chars_exceeding > 0) { |
| // If none of the characters will fit, notify and exit. |
| if (chars_exceeding == length) { |
| if (delegate_) |
| delegate_->NotifyTextFull(); |
| return; |
| } |
| |
| // Some, but not all, chars will fit, insert them and then notify |
| // we're full. |
| exceeded_limit = true; |
| length -= chars_exceeding; |
| } |
| } |
| |
| if (add_operation == RecordOperation::kInsertRecord) { |
| AddOperationRecord( |
| std::make_unique<InsertOperation>(this, gap_position_, text)); |
| } |
| |
| WideString previous_text; |
| if (delegate_) |
| previous_text = GetText(); |
| |
| // Copy the new text into the gap. |
| fxcrt::spancpy(pdfium::make_span(content_).subspan(gap_position_), |
| text.span().first(length)); |
| gap_position_ += length; |
| gap_size_ -= length; |
| text_length_ += length; |
| |
| is_dirty_ = true; |
| |
| // Inserting text resets the selection. |
| ClearSelection(); |
| |
| if (delegate_) { |
| if (exceeded_limit) |
| delegate_->NotifyTextFull(); |
| |
| delegate_->OnTextChanged(); |
| } |
| } |
| |
| void CFDE_TextEditEngine::AddOperationRecord(std::unique_ptr<Operation> op) { |
| size_t last_insert_position = next_operation_index_to_insert_ == 0 |
| ? max_edit_operations_ - 1 |
| : next_operation_index_to_insert_ - 1; |
| |
| // If our undo record is not the last thing we inserted then we need to |
| // remove all the undo records between our insert position and the undo marker |
| // and make that our new insert position. |
| if (next_operation_index_to_undo_ != last_insert_position) { |
| if (next_operation_index_to_undo_ > last_insert_position) { |
| // Our Undo position is ahead of us, which means we need to clear out the |
| // head of the queue. |
| while (last_insert_position != 0) { |
| operation_buffer_[last_insert_position].reset(); |
| --last_insert_position; |
| } |
| operation_buffer_[0].reset(); |
| |
| // Moving this will let us then clear out the end, setting the undo |
| // position to before the insert position. |
| last_insert_position = max_edit_operations_ - 1; |
| } |
| |
| // Clear out the vector from undo position to our set insert position. |
| while (next_operation_index_to_undo_ != last_insert_position) { |
| operation_buffer_[last_insert_position].reset(); |
| --last_insert_position; |
| } |
| } |
| |
| // We're now pointing at the next thing we want to Undo, so insert at the |
| // next position in the queue. |
| ++last_insert_position; |
| if (last_insert_position >= max_edit_operations_) |
| last_insert_position = 0; |
| |
| operation_buffer_[last_insert_position] = std::move(op); |
| next_operation_index_to_insert_ = |
| (last_insert_position + 1) % max_edit_operations_; |
| next_operation_index_to_undo_ = last_insert_position; |
| } |
| |
| void CFDE_TextEditEngine::ClearOperationRecords() { |
| for (auto& record : operation_buffer_) |
| record.reset(); |
| |
| next_operation_index_to_undo_ = max_edit_operations_ - 1; |
| next_operation_index_to_insert_ = 0; |
| } |
| |
| size_t CFDE_TextEditEngine::GetIndexLeft(size_t pos) const { |
| if (pos == 0) |
| return 0; |
| --pos; |
| |
| while (pos != 0) { |
| // We want to be on the location just before the \r or \n |
| wchar_t ch = GetChar(pos - 1); |
| if (ch != '\r' && ch != '\n') |
| break; |
| |
| --pos; |
| } |
| return pos; |
| } |
| |
| size_t CFDE_TextEditEngine::GetIndexRight(size_t pos) const { |
| if (pos >= text_length_) |
| return text_length_; |
| ++pos; |
| |
| wchar_t ch = GetChar(pos); |
| // We want to be on the location after the \r\n. |
| while (pos < text_length_ && (ch == '\r' || ch == '\n')) { |
| ++pos; |
| ch = GetChar(pos); |
| } |
| |
| return pos; |
| } |
| |
| size_t CFDE_TextEditEngine::GetIndexUp(size_t pos) const { |
| size_t line_start = GetIndexAtStartOfLine(pos); |
| if (line_start == 0) |
| return pos; |
| |
| // Determine how far along the line we were. |
| size_t dist = pos - line_start; |
| |
| // Move to the end of the preceding line. |
| wchar_t ch; |
| do { |
| --line_start; |
| ch = GetChar(line_start); |
| } while (line_start != 0 && (ch == '\r' || ch == '\n')); |
| |
| if (line_start == 0) |
| return dist; |
| |
| // Get the start of the line prior to the current line. |
| size_t prior_start = GetIndexAtStartOfLine(line_start); |
| |
| // Prior line is shorter then next line, and we're past the end of that line |
| // return the end of line. |
| if (prior_start + dist > line_start) |
| return GetIndexAtEndOfLine(line_start); |
| |
| return prior_start + dist; |
| } |
| |
| size_t CFDE_TextEditEngine::GetIndexDown(size_t pos) const { |
| size_t line_end = GetIndexAtEndOfLine(pos); |
| if (line_end == text_length_) |
| return pos; |
| |
| wchar_t ch; |
| do { |
| ++line_end; |
| ch = GetChar(line_end); |
| } while (line_end < text_length_ && (ch == '\r' || ch == '\n')); |
| |
| if (line_end == text_length_) |
| return line_end; |
| |
| // Determine how far along the line we are. |
| size_t dist = pos - GetIndexAtStartOfLine(pos); |
| |
| // Check if next line is shorter then current line. If so, return end |
| // of next line. |
| size_t next_line_end = GetIndexAtEndOfLine(line_end); |
| if (line_end + dist > next_line_end) |
| return next_line_end; |
| |
| return line_end + dist; |
| } |
| |
| size_t CFDE_TextEditEngine::GetIndexAtStartOfLine(size_t pos) const { |
| if (pos == 0) |
| return 0; |
| |
| wchar_t ch = GetChar(pos); |
| // What to do. |
| if (ch == '\r' || ch == '\n') |
| return pos; |
| |
| do { |
| // We want to be on the location just after the \r\n |
| ch = GetChar(pos - 1); |
| if (ch == '\r' || ch == '\n') |
| break; |
| |
| --pos; |
| } while (pos > 0); |
| |
| return pos; |
| } |
| |
| size_t CFDE_TextEditEngine::GetIndexAtEndOfLine(size_t pos) const { |
| if (pos >= text_length_) |
| return text_length_; |
| |
| wchar_t ch = GetChar(pos); |
| // Not quite sure which way to go here? |
| if (ch == '\r' || ch == '\n') |
| return pos; |
| |
| // We want to be on the location of the first \r or \n. |
| do { |
| ++pos; |
| ch = GetChar(pos); |
| } while (pos < text_length_ && (ch != '\r' && ch != '\n')); |
| |
| return pos; |
| } |
| |
| void CFDE_TextEditEngine::LimitHorizontalScroll(bool val) { |
| ClearOperationRecords(); |
| limit_horizontal_area_ = val; |
| } |
| |
| void CFDE_TextEditEngine::LimitVerticalScroll(bool val) { |
| ClearOperationRecords(); |
| limit_vertical_area_ = val; |
| } |
| |
| bool CFDE_TextEditEngine::CanUndo() const { |
| return operation_buffer_[next_operation_index_to_undo_] != nullptr && |
| next_operation_index_to_undo_ != next_operation_index_to_insert_; |
| } |
| |
| bool CFDE_TextEditEngine::CanRedo() const { |
| size_t idx = (next_operation_index_to_undo_ + 1) % max_edit_operations_; |
| return idx != next_operation_index_to_insert_ && |
| operation_buffer_[idx] != nullptr; |
| } |
| |
| bool CFDE_TextEditEngine::Redo() { |
| if (!CanRedo()) |
| return false; |
| |
| next_operation_index_to_undo_ = |
| (next_operation_index_to_undo_ + 1) % max_edit_operations_; |
| operation_buffer_[next_operation_index_to_undo_]->Redo(); |
| return true; |
| } |
| |
| bool CFDE_TextEditEngine::Undo() { |
| if (!CanUndo()) |
| return false; |
| |
| operation_buffer_[next_operation_index_to_undo_]->Undo(); |
| next_operation_index_to_undo_ = next_operation_index_to_undo_ == 0 |
| ? max_edit_operations_ - 1 |
| : next_operation_index_to_undo_ - 1; |
| return true; |
| } |
| |
| void CFDE_TextEditEngine::Layout() { |
| if (!is_dirty_) |
| return; |
| |
| is_dirty_ = false; |
| RebuildPieces(); |
| } |
| |
| CFX_RectF CFDE_TextEditEngine::GetContentsBoundingBox() { |
| // Layout if necessary. |
| Layout(); |
| return contents_bounding_box_; |
| } |
| |
| void CFDE_TextEditEngine::SetAvailableWidth(size_t width) { |
| if (width == available_width_) |
| return; |
| |
| ClearOperationRecords(); |
| |
| available_width_ = width; |
| if (is_linewrap_enabled_) |
| text_break_.SetLineWidth(width); |
| if (is_comb_text_) |
| SetCombTextWidth(); |
| |
| is_dirty_ = true; |
| } |
| |
| void CFDE_TextEditEngine::SetHasCharacterLimit(bool limit) { |
| if (has_character_limit_ == limit) |
| return; |
| |
| has_character_limit_ = limit; |
| character_limit_ = std::max(character_limit_, text_length_); |
| if (is_comb_text_) |
| SetCombTextWidth(); |
| |
| is_dirty_ = true; |
| } |
| |
| void CFDE_TextEditEngine::SetCharacterLimit(size_t limit) { |
| if (character_limit_ == limit) |
| return; |
| |
| ClearOperationRecords(); |
| |
| character_limit_ = std::max(limit, text_length_); |
| if (is_comb_text_) |
| SetCombTextWidth(); |
| |
| is_dirty_ = true; |
| } |
| |
| void CFDE_TextEditEngine::SetFont(RetainPtr<CFGAS_GEFont> font) { |
| if (font_ == font) |
| return; |
| |
| font_ = std::move(font); |
| text_break_.SetFont(font_); |
| is_dirty_ = true; |
| } |
| |
| RetainPtr<CFGAS_GEFont> CFDE_TextEditEngine::GetFont() const { |
| return font_; |
| } |
| |
| void CFDE_TextEditEngine::SetFontSize(float size) { |
| if (font_size_ == size) |
| return; |
| |
| font_size_ = size; |
| text_break_.SetFontSize(font_size_); |
| is_dirty_ = true; |
| } |
| |
| void CFDE_TextEditEngine::SetTabWidth(float width) { |
| int32_t old_tab_width = text_break_.GetTabWidth(); |
| text_break_.SetTabWidth(width); |
| if (old_tab_width == text_break_.GetTabWidth()) |
| return; |
| |
| is_dirty_ = true; |
| } |
| |
| void CFDE_TextEditEngine::SetAlignment(uint32_t alignment) { |
| if (alignment == character_alignment_) |
| return; |
| |
| character_alignment_ = alignment; |
| text_break_.SetAlignment(alignment); |
| is_dirty_ = true; |
| } |
| |
| void CFDE_TextEditEngine::SetVisibleLineCount(size_t count) { |
| if (visible_line_count_ == count) |
| return; |
| |
| visible_line_count_ = std::max(static_cast<size_t>(1), count); |
| is_dirty_ = true; |
| } |
| |
| void CFDE_TextEditEngine::EnableMultiLine(bool val) { |
| if (is_multiline_ == val) |
| return; |
| |
| is_multiline_ = val; |
| Mask<CFGAS_Break::LayoutStyle> style = text_break_.GetLayoutStyles(); |
| if (is_multiline_) |
| style.Clear(CFGAS_Break::LayoutStyle::kSingleLine); |
| else |
| style |= CFGAS_Break::LayoutStyle::kSingleLine; |
| |
| text_break_.SetLayoutStyles(style); |
| is_dirty_ = true; |
| } |
| |
| void CFDE_TextEditEngine::EnableLineWrap(bool val) { |
| if (is_linewrap_enabled_ == val) |
| return; |
| |
| is_linewrap_enabled_ = val; |
| text_break_.SetLineWidth(is_linewrap_enabled_ ? available_width_ |
| : kPageWidthMax); |
| is_dirty_ = true; |
| } |
| |
| void CFDE_TextEditEngine::SetCombText(bool enable) { |
| if (is_comb_text_ == enable) |
| return; |
| |
| is_comb_text_ = enable; |
| |
| Mask<CFGAS_Break::LayoutStyle> style = text_break_.GetLayoutStyles(); |
| if (enable) { |
| style |= CFGAS_Break::LayoutStyle::kCombText; |
| SetCombTextWidth(); |
| } else { |
| style.Clear(CFGAS_Break::LayoutStyle::kCombText); |
| } |
| text_break_.SetLayoutStyles(style); |
| is_dirty_ = true; |
| } |
| |
| void CFDE_TextEditEngine::SetCombTextWidth() { |
| size_t width = available_width_; |
| if (has_character_limit_) |
| width /= character_limit_; |
| |
| text_break_.SetCombWidth(width); |
| } |
| |
| void CFDE_TextEditEngine::SelectAll() { |
| if (text_length_ == 0) |
| return; |
| |
| has_selection_ = true; |
| selection_.start_idx = 0; |
| selection_.count = text_length_; |
| } |
| |
| void CFDE_TextEditEngine::ClearSelection() { |
| has_selection_ = false; |
| selection_.start_idx = 0; |
| selection_.count = 0; |
| } |
| |
| void CFDE_TextEditEngine::SetSelection(size_t start_idx, size_t count) { |
| if (count == 0) { |
| ClearSelection(); |
| return; |
| } |
| |
| if (start_idx > text_length_) |
| return; |
| if (start_idx + count > text_length_) |
| count = text_length_ - start_idx; |
| |
| has_selection_ = true; |
| selection_.start_idx = start_idx; |
| selection_.count = count; |
| } |
| |
| WideString CFDE_TextEditEngine::GetSelectedText() const { |
| if (!has_selection_) |
| return WideString(); |
| |
| WideString text; |
| if (selection_.start_idx < gap_position_) { |
| // Fully on left of gap. |
| if (selection_.start_idx + selection_.count < gap_position_) { |
| text += WideStringView(content_.data() + selection_.start_idx, |
| selection_.count); |
| return text; |
| } |
| |
| // Pre-gap text |
| text += WideStringView(content_.data() + selection_.start_idx, |
| gap_position_ - selection_.start_idx); |
| |
| if (selection_.count - (gap_position_ - selection_.start_idx) > 0) { |
| // Post-gap text |
| text += WideStringView( |
| content_.data() + gap_position_ + gap_size_, |
| selection_.count - (gap_position_ - selection_.start_idx)); |
| } |
| |
| return text; |
| } |
| |
| // Fully right of gap |
| text += WideStringView(content_.data() + gap_size_ + selection_.start_idx, |
| selection_.count); |
| return text; |
| } |
| |
| WideString CFDE_TextEditEngine::DeleteSelectedText( |
| RecordOperation add_operation) { |
| if (!has_selection_) |
| return WideString(); |
| |
| return Delete(selection_.start_idx, selection_.count, add_operation); |
| } |
| |
| WideString CFDE_TextEditEngine::Delete(size_t start_idx, |
| size_t length, |
| RecordOperation add_operation) { |
| if (start_idx >= text_length_) |
| return WideString(); |
| |
| TextChange change; |
| change.text.clear(); |
| change.cancelled = false; |
| if (delegate_ && (add_operation != RecordOperation::kSkipRecord && |
| add_operation != RecordOperation::kSkipNotify)) { |
| change.previous_text = GetText(); |
| change.selection_start = start_idx; |
| change.selection_end = start_idx + length; |
| |
| delegate_->OnTextWillChange(&change); |
| if (change.cancelled) |
| return WideString(); |
| |
| // Delegate may have changed the selection range. |
| start_idx = change.selection_start; |
| length = change.selection_end - change.selection_start; |
| |
| // Delegate may have changed text entirely, recheck. |
| if (start_idx >= text_length_) |
| return WideString(); |
| } |
| |
| length = std::min(length, text_length_ - start_idx); |
| AdjustGap(start_idx + length, 0); |
| |
| WideString ret; |
| ret += WideStringView(content_.data() + start_idx, length); |
| |
| if (add_operation == RecordOperation::kInsertRecord) { |
| AddOperationRecord(std::make_unique<DeleteOperation>(this, start_idx, ret)); |
| } |
| |
| WideString previous_text = GetText(); |
| |
| gap_position_ = start_idx; |
| gap_size_ += length; |
| |
| text_length_ -= length; |
| is_dirty_ = true; |
| ClearSelection(); |
| |
| // The JS requested the insertion of text instead of just a deletion. |
| if (!change.text.IsEmpty()) |
| Insert(start_idx, change.text, RecordOperation::kSkipRecord); |
| |
| if (delegate_) |
| delegate_->OnTextChanged(); |
| |
| return ret; |
| } |
| |
| void CFDE_TextEditEngine::ReplaceSelectedText(const WideString& requested_rep) { |
| WideString rep = requested_rep; |
| |
| if (delegate_) { |
| TextChange change; |
| change.selection_start = selection_.start_idx; |
| change.selection_end = selection_.start_idx + selection_.count; |
| change.text = rep; |
| change.previous_text = GetText(); |
| change.cancelled = false; |
| |
| delegate_->OnTextWillChange(&change); |
| if (change.cancelled) |
| return; |
| |
| rep = change.text; |
| selection_.start_idx = change.selection_start; |
| selection_.count = change.selection_end - change.selection_start; |
| } |
| |
| size_t start_idx = selection_.start_idx; |
| WideString txt = DeleteSelectedText(RecordOperation::kSkipRecord); |
| Insert(gap_position_, rep, RecordOperation::kSkipRecord); |
| |
| AddOperationRecord( |
| std::make_unique<ReplaceOperation>(this, start_idx, txt, rep)); |
| } |
| |
| WideString CFDE_TextEditEngine::GetText() const { |
| WideString str; |
| if (gap_position_ > 0) |
| str += WideStringView(content_.data(), gap_position_); |
| if (text_length_ - gap_position_ > 0) { |
| str += WideStringView(content_.data() + gap_position_ + gap_size_, |
| text_length_ - gap_position_); |
| } |
| return str; |
| } |
| |
| size_t CFDE_TextEditEngine::GetLength() const { |
| return text_length_; |
| } |
| |
| wchar_t CFDE_TextEditEngine::GetChar(size_t idx) const { |
| if (idx >= text_length_) |
| return L'\0'; |
| if (password_mode_) |
| return password_alias_; |
| |
| return idx < gap_position_ |
| ? content_[idx] |
| : content_[gap_position_ + gap_size_ + (idx - gap_position_)]; |
| } |
| |
| int32_t CFDE_TextEditEngine::GetWidthOfChar(size_t idx) { |
| // Recalculate the widths if necessary. |
| Layout(); |
| return idx < char_widths_.size() ? char_widths_[idx] : 0; |
| } |
| |
| size_t CFDE_TextEditEngine::GetIndexForPoint(const CFX_PointF& point) { |
| // Recalculate the widths if necessary. |
| Layout(); |
| |
| auto start_it = text_piece_info_.begin(); |
| for (; start_it < text_piece_info_.end(); ++start_it) { |
| if (start_it->rtPiece.top <= point.y && |
| point.y < start_it->rtPiece.bottom()) |
| break; |
| } |
| // We didn't find the point before getting to the end of the text, return |
| // end of text. |
| if (start_it == text_piece_info_.end()) |
| return text_length_; |
| |
| auto end_it = start_it; |
| for (; end_it < text_piece_info_.end(); ++end_it) { |
| // We've moved past where the point should be and didn't find anything. |
| // Return the start of the current piece as the location. |
| if (end_it->rtPiece.bottom() <= point.y || point.y < end_it->rtPiece.top) |
| break; |
| } |
| // Make sure the end iterator is pointing to our text pieces. |
| if (end_it == text_piece_info_.end()) |
| --end_it; |
| |
| size_t start_it_idx = start_it->nStart; |
| for (; start_it <= end_it; ++start_it) { |
| bool piece_contains_point_vertically = |
| (point.y >= start_it->rtPiece.top && |
| point.y < start_it->rtPiece.bottom()); |
| if (!piece_contains_point_vertically) |
| continue; |
| |
| std::vector<CFX_RectF> rects = GetCharRects(*start_it); |
| for (size_t i = 0; i < rects.size(); ++i) { |
| bool character_contains_point_horizontally = |
| (point.x >= rects[i].left && point.x < rects[i].right()); |
| if (!character_contains_point_horizontally) |
| continue; |
| |
| // When clicking on the left half of a character, the cursor should be |
| // moved before it. If the click was on the right half of that character, |
| // move the cursor after it. |
| bool closer_to_left = |
| (point.x - rects[i].left < rects[i].right() - point.x); |
| size_t caret_pos = closer_to_left ? i : i + 1; |
| size_t pos = start_it->nStart + caret_pos; |
| if (pos >= text_length_) |
| return text_length_; |
| |
| wchar_t wch = GetChar(pos); |
| if (wch == L'\n' || wch == L'\r') { |
| if (wch == L'\n' && pos > 0 && GetChar(pos - 1) == L'\r') |
| --pos; |
| return pos; |
| } |
| |
| // TODO(dsinclair): Old code had a before flag set based on bidi? |
| return pos; |
| } |
| |
| // Point is not within the horizontal range of any characters, it's |
| // afterwards. Return the position after the last character. |
| // The last line has nCount equal to the number of characters + 1 (sentinel |
| // character maybe?). Restrict to the text_length_ to account for that. |
| size_t pos = std::min( |
| static_cast<size_t>(start_it->nStart + start_it->nCount), text_length_); |
| |
| // If the line is not the last one and it was broken right after a breaking |
| // whitespace (space or line break), the cursor should not be placed after |
| // the whitespace, but before it. If the cursor is moved after the |
| // whitespace, it goes to the beginning of the next line. |
| bool is_last_line = (std::next(start_it) == text_piece_info_.end()); |
| if (!is_last_line && pos > 0) { |
| wchar_t previous_char = GetChar(pos - 1); |
| if (previous_char == L' ' || previous_char == L'\n' || |
| previous_char == L'\r') { |
| --pos; |
| } |
| } |
| |
| return pos; |
| } |
| |
| if (start_it == text_piece_info_.end()) |
| return start_it_idx; |
| if (start_it == end_it) |
| return start_it->nStart; |
| |
| // We didn't find the point before going over all of the pieces, we want to |
| // return the start of the piece after the point. |
| return end_it->nStart; |
| } |
| |
| std::vector<CFX_RectF> CFDE_TextEditEngine::GetCharRects( |
| const FDE_TEXTEDITPIECE& piece) { |
| if (piece.nCount < 1) |
| return std::vector<CFX_RectF>(); |
| |
| CFGAS_TxtBreak::Run tr; |
| tr.pEdtEngine = this; |
| tr.iStart = piece.nStart; |
| tr.iLength = piece.nCount; |
| tr.pFont = font_; |
| tr.fFontSize = font_size_; |
| tr.dwStyles = text_break_.GetLayoutStyles(); |
| tr.dwCharStyles = piece.dwCharStyles; |
| tr.pRect = &piece.rtPiece; |
| return text_break_.GetCharRects(tr); |
| } |
| |
| std::vector<TextCharPos> CFDE_TextEditEngine::GetDisplayPos( |
| const FDE_TEXTEDITPIECE& piece) { |
| if (piece.nCount < 1) |
| return std::vector<TextCharPos>(); |
| |
| CFGAS_TxtBreak::Run tr; |
| tr.pEdtEngine = this; |
| tr.iStart = piece.nStart; |
| tr.iLength = piece.nCount; |
| tr.pFont = font_; |
| tr.fFontSize = font_size_; |
| tr.dwStyles = text_break_.GetLayoutStyles(); |
| tr.dwCharStyles = piece.dwCharStyles; |
| tr.pRect = &piece.rtPiece; |
| |
| std::vector<TextCharPos> data(text_break_.GetDisplayPos(tr, nullptr)); |
| text_break_.GetDisplayPos(tr, data.data()); |
| return data; |
| } |
| |
| void CFDE_TextEditEngine::RebuildPieces() { |
| text_break_.EndBreak(CFGAS_Char::BreakType::kParagraph); |
| text_break_.ClearBreakPieces(); |
| |
| char_widths_.clear(); |
| text_piece_info_.clear(); |
| |
| // Must have a font set in order to break the text. |
| if (!CanGenerateCharacterInfo()) |
| return; |
| |
| bool initialized_bounding_box = false; |
| contents_bounding_box_ = CFX_RectF(); |
| size_t current_piece_start = 0; |
| float current_line_start = 0; |
| |
| CFDE_TextEditEngine::Iterator iter(this); |
| while (!iter.IsEOF(false)) { |
| iter.Next(false); |
| |
| CFGAS_Char::BreakType break_status = text_break_.AppendChar( |
| password_mode_ ? password_alias_ : iter.GetChar()); |
| if (iter.IsEOF(false) && CFX_BreakTypeNoneOrPiece(break_status)) |
| break_status = text_break_.EndBreak(CFGAS_Char::BreakType::kParagraph); |
| |
| if (CFX_BreakTypeNoneOrPiece(break_status)) |
| continue; |
| int32_t piece_count = text_break_.CountBreakPieces(); |
| for (int32_t i = 0; i < piece_count; ++i) { |
| const CFGAS_BreakPiece* piece = text_break_.GetBreakPieceUnstable(i); |
| |
| FDE_TEXTEDITPIECE txtEdtPiece; |
| txtEdtPiece.rtPiece.left = piece->GetStartPos() / 20000.0f; |
| txtEdtPiece.rtPiece.top = current_line_start; |
| txtEdtPiece.rtPiece.width = piece->GetWidth() / 20000.0f; |
| txtEdtPiece.rtPiece.height = line_spacing_; |
| txtEdtPiece.nStart = |
| pdfium::base::checked_cast<int32_t>(current_piece_start); |
| txtEdtPiece.nCount = piece->GetCharCount(); |
| txtEdtPiece.nBidiLevel = piece->GetBidiLevel(); |
| txtEdtPiece.dwCharStyles = piece->GetCharStyles(); |
| if (FX_IsOdd(piece->GetBidiLevel())) |
| txtEdtPiece.dwCharStyles |= FX_TXTCHARSTYLE_OddBidiLevel; |
| |
| text_piece_info_.push_back(txtEdtPiece); |
| |
| if (initialized_bounding_box) { |
| contents_bounding_box_.Union(txtEdtPiece.rtPiece); |
| } else { |
| contents_bounding_box_ = txtEdtPiece.rtPiece; |
| initialized_bounding_box = true; |
| } |
| |
| current_piece_start += txtEdtPiece.nCount; |
| for (int32_t k = 0; k < txtEdtPiece.nCount; ++k) |
| char_widths_.push_back(piece->GetChar(k)->m_iCharWidth); |
| } |
| |
| current_line_start += line_spacing_; |
| text_break_.ClearBreakPieces(); |
| } |
| |
| float delta = 0.0; |
| bool bounds_smaller = contents_bounding_box_.width < available_width_; |
| if (IsAlignedRight() && bounds_smaller) { |
| delta = available_width_ - contents_bounding_box_.width; |
| } else if (IsAlignedCenter() && bounds_smaller) { |
| delta = (available_width_ - contents_bounding_box_.width) / 2.0f; |
| } |
| |
| if (delta != 0.0) { |
| float offset = delta - contents_bounding_box_.left; |
| for (auto& info : text_piece_info_) |
| info.rtPiece.Offset(offset, 0.0f); |
| contents_bounding_box_.Offset(offset, 0.0f); |
| } |
| |
| // Shrink the last piece down to the font_size. |
| contents_bounding_box_.height -= line_spacing_ - font_size_; |
| text_piece_info_.back().rtPiece.height = font_size_; |
| } |
| |
| std::pair<int32_t, CFX_RectF> CFDE_TextEditEngine::GetCharacterInfo( |
| int32_t start_idx) { |
| DCHECK(start_idx >= 0); |
| DCHECK(static_cast<size_t>(start_idx) <= text_length_); |
| |
| // Make sure the current available data is fresh. |
| Layout(); |
| |
| auto it = text_piece_info_.begin(); |
| for (; it != text_piece_info_.end(); ++it) { |
| if (it->nStart <= start_idx && start_idx < it->nStart + it->nCount) |
| break; |
| } |
| if (it == text_piece_info_.end()) { |
| NOTREACHED(); |
| return {0, CFX_RectF()}; |
| } |
| |
| return {it->nBidiLevel, GetCharRects(*it)[start_idx - it->nStart]}; |
| } |
| |
| std::vector<CFX_RectF> CFDE_TextEditEngine::GetCharacterRectsInRange( |
| int32_t start_idx, |
| int32_t count) { |
| // Make sure the current available data is fresh. |
| Layout(); |
| |
| auto it = text_piece_info_.begin(); |
| for (; it != text_piece_info_.end(); ++it) { |
| if (it->nStart <= start_idx && start_idx < it->nStart + it->nCount) |
| break; |
| } |
| if (it == text_piece_info_.end()) |
| return std::vector<CFX_RectF>(); |
| |
| int32_t end_idx = start_idx + count - 1; |
| std::vector<CFX_RectF> rects; |
| while (it != text_piece_info_.end()) { |
| // If we end inside the current piece, extract what we need and we're done. |
| if (it->nStart <= end_idx && end_idx < it->nStart + it->nCount) { |
| std::vector<CFX_RectF> arr = GetCharRects(*it); |
| CFX_RectF piece = arr[0]; |
| piece.Union(arr[end_idx - it->nStart]); |
| rects.push_back(piece); |
| break; |
| } |
| rects.push_back(it->rtPiece); |
| ++it; |
| } |
| |
| return rects; |
| } |
| |
| std::pair<size_t, size_t> CFDE_TextEditEngine::BoundsForWordAt( |
| size_t idx) const { |
| if (idx > text_length_) |
| return {0, 0}; |
| |
| CFDE_TextEditEngine::Iterator iter(this); |
| iter.SetAt(idx); |
| |
| size_t start_idx = iter.FindNextBreakPos(true); |
| size_t end_idx = iter.FindNextBreakPos(false); |
| return {start_idx, end_idx - start_idx + 1}; |
| } |
| |
| CFDE_TextEditEngine::Iterator::Iterator(const CFDE_TextEditEngine* engine) |
| : engine_(engine) {} |
| |
| CFDE_TextEditEngine::Iterator::~Iterator() = default; |
| |
| void CFDE_TextEditEngine::Iterator::Next(bool bPrev) { |
| if (bPrev && current_position_ == -1) |
| return; |
| if (!bPrev && current_position_ > -1 && |
| static_cast<size_t>(current_position_) == engine_->GetLength()) { |
| return; |
| } |
| |
| if (bPrev) |
| --current_position_; |
| else |
| ++current_position_; |
| } |
| |
| wchar_t CFDE_TextEditEngine::Iterator::GetChar() const { |
| return engine_->GetChar(current_position_); |
| } |
| |
| void CFDE_TextEditEngine::Iterator::SetAt(size_t nIndex) { |
| nIndex = std::min(nIndex, engine_->GetLength()); |
| current_position_ = pdfium::base::checked_cast<int32_t>(nIndex); |
| } |
| |
| bool CFDE_TextEditEngine::Iterator::IsEOF(bool bPrev) const { |
| return bPrev ? current_position_ == -1 |
| : current_position_ > -1 && |
| static_cast<size_t>(current_position_) == |
| engine_->GetLength(); |
| } |
| |
| size_t CFDE_TextEditEngine::Iterator::FindNextBreakPos(bool bPrev) { |
| if (IsEOF(bPrev)) |
| return current_position_ > -1 ? current_position_ : 0; |
| |
| WordBreakProperty ePreType = WordBreakProperty::kNone; |
| if (!IsEOF(!bPrev)) { |
| Next(!bPrev); |
| ePreType = FX_GetWordBreakProperty(GetChar()); |
| Next(bPrev); |
| } |
| |
| WordBreakProperty eCurType = FX_GetWordBreakProperty(GetChar()); |
| bool bFirst = true; |
| while (!IsEOF(bPrev)) { |
| Next(bPrev); |
| |
| WordBreakProperty eNextType = FX_GetWordBreakProperty(GetChar()); |
| bool wBreak = FX_CheckStateChangeForWordBreak(eCurType, eNextType); |
| if (wBreak) { |
| if (IsEOF(bPrev)) { |
| Next(!bPrev); |
| break; |
| } |
| if (bFirst) { |
| int32_t nFlags = GetBreakFlagsFor(eCurType, eNextType); |
| if (nFlags > 0) { |
| if (BreakFlagsChanged(nFlags, ePreType)) { |
| Next(!bPrev); |
| break; |
| } |
| Next(bPrev); |
| wBreak = false; |
| } |
| } |
| if (wBreak) { |
| int32_t nFlags = GetBreakFlagsFor(eNextType, eCurType); |
| if (nFlags <= 0) { |
| Next(!bPrev); |
| break; |
| } |
| |
| Next(bPrev); |
| eNextType = FX_GetWordBreakProperty(GetChar()); |
| if (BreakFlagsChanged(nFlags, eNextType)) { |
| Next(!bPrev); |
| Next(!bPrev); |
| break; |
| } |
| } |
| } |
| eCurType = eNextType; |
| bFirst = false; |
| } |
| return current_position_ > -1 ? current_position_ : 0; |
| } |