diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /src/libnrtype/Layout-TNG-OutIter.cpp | |
parent | Initial commit. (diff) | |
download | inkscape-upstream.tar.xz inkscape-upstream.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/libnrtype/Layout-TNG-OutIter.cpp | 1175 |
1 files changed, 1175 insertions, 0 deletions
diff --git a/src/libnrtype/Layout-TNG-OutIter.cpp b/src/libnrtype/Layout-TNG-OutIter.cpp new file mode 100644 index 0000000..dd2123e --- /dev/null +++ b/src/libnrtype/Layout-TNG-OutIter.cpp @@ -0,0 +1,1175 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape::Text::Layout - text layout engine output functions using iterators + * + * Authors: + * Richard Hughes <cyreve@users.sf.net> + * + * Copyright (C) 2005 Richard Hughes + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "Layout-TNG.h" +#include "livarot/Path.h" +#include "font-instance.h" +#include "svg/svg-length.h" +#include <2geom/transforms.h> +#include <2geom/line.h> +#include "style.h" + +namespace Inkscape { +namespace Text { + +// Comment 18 Sept 2019: +// Cursor code might be simpler if Character was turned into a proper +// class and kept track of its absolute position and extent. This would +// make handling multi-line text (including multi-line text using +// 'white-space:pre') easier. This would also avoid problems where +// 'dx','dy' moved the character a long distance from its nominal +// position. + +Layout::iterator Layout::_cursorXOnLineToIterator(unsigned line_index, double local_x, double local_y) const +{ + unsigned char_index = _lineToCharacter(line_index); + int best_char_index = -1; + double best_difference = DBL_MAX; + + if (char_index == _characters.size()) return end(); + for ( ; char_index < _characters.size() ; char_index++) { + if (_characters[char_index].chunk(this).in_line != line_index) break; + //if (_characters[char_index].char_attributes.is_mandatory_break) break; + if (!_characters[char_index].char_attributes.is_cursor_position) continue; + + double delta_x = + _characters[char_index].x + + _characters[char_index].span(this).x_start + + _characters[char_index].chunk(this).left_x - + local_x; + + double delta_y = + _characters[char_index].span(this).y_offset + + _characters[char_index].line(this).baseline_y - + local_y; + + double this_difference = std::sqrt(delta_x*delta_x + delta_y*delta_y); + + if (this_difference < best_difference) { + best_difference = this_difference; + best_char_index = char_index; + } + } + + // also try the very end of a para (not lines though because the space wraps) + if (char_index == _characters.size() || _characters[char_index].char_attributes.is_mandatory_break) { + + double delta_x = 0.0; + double delta_y = 0.0; + + if (char_index == 0) { + delta_x = _spans.front().x_end + _chunks.front().left_x - local_x; + delta_y = _spans.front().y_offset + _spans.front().line(this).baseline_y - local_y; + } else { + delta_x = _characters[char_index - 1].span(this).x_end + _characters[char_index - 1].chunk(this).left_x - local_x; + delta_y = _characters[char_index - 1].span(this).y_offset + _characters[char_index - 1].line(this).baseline_y - local_y; + } + + double this_difference = std::sqrt(delta_x*delta_x + delta_y*delta_y); + + if (this_difference < best_difference) { + best_char_index = char_index; + best_difference = this_difference; + } + } + + + if (best_char_index == -1) { + best_char_index = char_index; + } + + if (best_char_index == _characters.size()) { + return end(); + } + + return iterator(this, best_char_index); +} + +double Layout::_getChunkWidth(unsigned chunk_index) const +{ + double chunk_width = 0.0; + unsigned span_index; + if (chunk_index) { + span_index = _lineToSpan(_chunks[chunk_index].in_line); + for ( ; span_index < _spans.size() && _spans[span_index].in_chunk < chunk_index ; span_index++){}; + } else { + span_index = 0; + } + + for ( ; span_index < _spans.size() && _spans[span_index].in_chunk == chunk_index ; span_index++) { + chunk_width = std::max(chunk_width, (double)std::max(_spans[span_index].x_start, _spans[span_index].x_end)); + } + + return chunk_width; +} + +/* getting the cursor position for a mouse click is not as simple as it might +seem. The two major problems are flows set up in multiple columns and large +dy adjustments such that text does not belong to the line it appears to. In +the worst case it's possible to have two characters on top of each other, in +which case the one we pick is arbitrary. + +This is a 3-stage (2 pass) algorithm: +1) search all the spans to see if the point is contained in one, if so take + that. Note that this will collect all clicks from the current UI because + of how the hit detection of nrarena objects works. +2) if that fails, run through all the chunks finding a best guess of the one + the user wanted. This is the one whose y coordinate is nearest, or if + there's a tie, the x. +3) search in that chunk using x-coordinate only to find the position. +*/ +Layout::iterator Layout::getNearestCursorPositionTo(double x, double y) const +{ + if (_lines.empty()) return begin(); + double local_x = x; + double local_y = y; + + if (_path_fitted) { + Path::cut_position position = const_cast<Path*>(_path_fitted)->PointToCurvilignPosition(Geom::Point(x, y)); + local_x = const_cast<Path*>(_path_fitted)->PositionToLength(position.piece, position.t); + return _cursorXOnLineToIterator(0, local_x + _chunks.front().left_x); + } + + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + local_x = y; + local_y = x; + } + + // stage 1: + for (const auto & _span : _spans) { + double span_left, span_right; + if (_span.x_start < _span.x_end) { + span_left = _span.x_start; + span_right = _span.x_end; + } else { + span_left = _span.x_end; + span_right = _span.x_start; + } + + double y_line = _span.line(this).baseline_y + _span.baseline_shift + _span.y_offset; + if ( local_x >= _chunks[_span.in_chunk].left_x + span_left + && local_x <= _chunks[_span.in_chunk].left_x + span_right + && local_y >= y_line - _span.line_height.ascent + && local_y <= y_line + _span.line_height.descent) { + return _cursorXOnLineToIterator(_chunks[_span.in_chunk].in_line, local_x, local_y); + } + } + + // stage 2: + unsigned span_index = 0; + unsigned chunk_index; + int best_chunk_index = -1; + double best_y_range = DBL_MAX; + double best_x_range = DBL_MAX; + for (chunk_index = 0 ; chunk_index < _chunks.size() ; chunk_index++) { + FontMetrics line_height; + line_height *= 0.0; // Set all metrics to zero. + double chunk_width = 0.0; + for ( ; span_index < _spans.size() && _spans[span_index].in_chunk == chunk_index ; span_index++) { + line_height.max(_spans[span_index].line_height); + chunk_width = std::max(chunk_width, (double)std::max(_spans[span_index].x_start, _spans[span_index].x_end)); + } + double this_y_range; + if (local_y < _lines[_chunks[chunk_index].in_line].baseline_y - line_height.ascent) + this_y_range = _lines[_chunks[chunk_index].in_line].baseline_y - line_height.ascent - local_y; + else if (local_y > _lines[_chunks[chunk_index].in_line].baseline_y + line_height.descent) + this_y_range = local_y - (_lines[_chunks[chunk_index].in_line].baseline_y + line_height.descent); + else + this_y_range = 0.0; + if (this_y_range <= best_y_range) { + if (this_y_range < best_y_range) best_x_range = DBL_MAX; + double this_x_range; + if (local_x < _chunks[chunk_index].left_x) + this_x_range = _chunks[chunk_index].left_x - local_y; + else if (local_x > _chunks[chunk_index].left_x + chunk_width) + this_x_range = local_x - (_chunks[chunk_index].left_x + chunk_width); + else + this_x_range = 0.0; + if (this_x_range < best_x_range) { + best_y_range = this_y_range; + best_x_range = this_x_range; + best_chunk_index = chunk_index; + } + } + } + + // stage 3: + if (best_chunk_index == -1) return begin(); // never happens + return _cursorXOnLineToIterator(_chunks[best_chunk_index].in_line, local_x, local_y); +} + +Layout::iterator Layout::getLetterAt(double x, double y) const +{ + Geom::Point point(x, y); + + double rotation; + for (iterator it = begin() ; it != end() ; it.nextCharacter()) { + Geom::Rect box = characterBoundingBox(it, &rotation); + // todo: rotation + if (box.contains(point)) return it; + } + return end(); +} + +Layout::iterator Layout::sourceToIterator(SPObject *source /*, Glib::ustring::const_iterator text_iterator*/) const +{ + unsigned source_index; + if (_characters.empty()) return end(); + for (source_index = 0 ; source_index < _input_stream.size() ; source_index++) + if (_input_stream[source_index]->source == source) break; + if (source_index == _input_stream.size()) return end(); + + unsigned char_index = _sourceToCharacter(source_index); + + // Fix a bug when hidding content in flow box element + if (char_index >= _characters.size()) + return end(); + + if (_input_stream[source_index]->Type() != TEXT_SOURCE) + return iterator(this, char_index); + + return iterator(this, char_index); + /* This code was never used, the text_iterator argument was "NULL" in all calling code + InputStreamTextSource const *text_source = static_cast<InputStreamTextSource const *>(_input_stream[source_index]); + + if (text_iterator <= text_source->text_begin) return iterator(this, char_index); + if (text_iterator >= text_source->text_end) { + if (source_index == _input_stream.size() - 1) return end(); + return iterator(this, _sourceToCharacter(source_index + 1)); + } + Glib::ustring::const_iterator iter_text = text_source->text_begin; + for ( ; char_index < _characters.size() ; char_index++) { + if (iter_text == text_iterator) + return iterator(this, char_index); + iter_text++; + } + return end(); // never happens + */ +} + +Geom::OptRect Layout::glyphBoundingBox(iterator const &it, double *rotation) const +{ + if (rotation) *rotation = _glyphs[it._glyph_index].rotation; + return _glyphs[it._glyph_index].span(this).font->BBox(_glyphs[it._glyph_index].glyph); +} + +Geom::Point Layout::characterAnchorPoint(iterator const &it) const +{ + if (_characters.empty()) + return _empty_cursor_shape.position; + + Geom::Point res; + if (it._char_index == _characters.size()) { + res = Geom::Point(_chunks.back().left_x + _spans.back().x_end, _lines.back().baseline_y + _spans.back().baseline_shift); + } else { + res = Geom::Point(_characters[it._char_index].chunk(this).left_x + + _spans[_characters[it._char_index].in_span].x_start + + _characters[it._char_index].x, + _characters[it._char_index].line(this).baseline_y + + _characters[it._char_index].span(this).baseline_shift); + } + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + std::swap(res[Geom::X], res[Geom::Y]); + } + return res; +} + +std::optional<Geom::Point> Layout::baselineAnchorPoint() const +{ + iterator pos = this->begin(); + Geom::Point left_pt = this->characterAnchorPoint(pos); + pos.thisEndOfLine(); + Geom::Point right_pt = this->characterAnchorPoint(pos); + + switch (this->paragraphAlignment(pos)) { + case LEFT: + case FULL: + return left_pt; + break; + case CENTER: + return (left_pt + right_pt)/2; // middle point + break; + case RIGHT: + return right_pt; + break; + default: + return std::optional<Geom::Point>(); + break; + } +} + +Geom::Path Layout::baseline() const +{ + iterator pos = this->begin(); + Geom::Point left_pt = this->characterAnchorPoint(pos); + pos.thisEndOfLine(); + Geom::Point right_pt = this->characterAnchorPoint(pos); + + Geom::Path baseline; + baseline.start(left_pt); + baseline.appendNew<Geom::LineSegment>(right_pt); + + return baseline; +} + + +Geom::Point Layout::chunkAnchorPoint(iterator const &it) const +{ + unsigned chunk_index; + + if (_chunks.empty()) + return Geom::Point(0.0, 0.0); + + if (_characters.empty()) + chunk_index = 0; + else if (it._char_index == _characters.size()) + chunk_index = _chunks.size() - 1; + else chunk_index = _characters[it._char_index].span(this).in_chunk; + + Alignment alignment = _paragraphs[_lines[_chunks[chunk_index].in_line].in_paragraph].alignment; + double x = _chunks[chunk_index].left_x; + double y = _lines[_chunks[chunk_index].in_line].baseline_y; + double chunk_width = _getChunkWidth(chunk_index); + if (alignment == RIGHT) { + x += chunk_width; + } else if (alignment == CENTER) { + x += chunk_width * 0.5; + } + + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + return Geom::Point(y, x); + } else { + return Geom::Point(x, y); + } +} + +Geom::Rect Layout::characterBoundingBox(iterator const &it, double *rotation) const +{ + Geom::Point top_left, bottom_right; + unsigned char_index = it._char_index; + + if (_path_fitted) { + double cluster_half_width = 0.0; + for (int glyph_index = _characters[char_index].in_glyph ; _glyphs.size() != glyph_index ; glyph_index++) { + if (_glyphs[glyph_index].in_character != char_index) break; + cluster_half_width += _glyphs[glyph_index].advance; + } + cluster_half_width *= 0.5; + + double midpoint_offset = _characters[char_index].span(this).x_start + _characters[char_index].x + cluster_half_width; + int unused = 0; + Path::cut_position *midpoint_otp = const_cast<Path*>(_path_fitted)->CurvilignToPosition(1, &midpoint_offset, unused); + if (midpoint_offset >= 0.0 && midpoint_otp != nullptr && midpoint_otp[0].piece >= 0) { + Geom::Point midpoint; + Geom::Point tangent; + Span const &span = _characters[char_index].span(this); + + const_cast<Path*>(_path_fitted)->PointAndTangentAt(midpoint_otp[0].piece, midpoint_otp[0].t, midpoint, tangent); + top_left[Geom::X] = midpoint[Geom::X] - cluster_half_width; + top_left[Geom::Y] = midpoint[Geom::Y] - span.line_height.ascent; + bottom_right[Geom::X] = midpoint[Geom::X] + cluster_half_width; + bottom_right[Geom::Y] = midpoint[Geom::Y] + span.line_height.descent; + Geom::Point normal = tangent.cw(); + top_left += span.baseline_shift * normal; + bottom_right += span.baseline_shift * normal; + if (rotation) + *rotation = atan2(tangent[1], tangent[0]); + } + g_free(midpoint_otp); + } else { + if (it._char_index == _characters.size()) { + top_left[Geom::X] = bottom_right[Geom::X] = _chunks.back().left_x + _spans.back().x_end; + char_index--; + } else { + double span_x = _spans[_characters[it._char_index].in_span].x_start + _characters[it._char_index].chunk(this).left_x; + top_left[Geom::X] = span_x + _characters[it._char_index].x; + if (it._char_index + 1 == _characters.size() || _characters[it._char_index + 1].in_span != _characters[it._char_index].in_span) + bottom_right[Geom::X] = _spans[_characters[it._char_index].in_span].x_end + _characters[it._char_index].chunk(this).left_x; + else + bottom_right[Geom::X] = span_x + _characters[it._char_index + 1].x; + } + + double baseline_y = _characters[char_index].line(this).baseline_y + _characters[char_index].span(this).baseline_shift; + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + double span_height = _spans[_characters[char_index].in_span].line_height.emSize(); + top_left[Geom::Y] = top_left[Geom::X]; + top_left[Geom::X] = baseline_y - span_height * 0.5; + bottom_right[Geom::Y] = bottom_right[Geom::X]; + bottom_right[Geom::X] = baseline_y + span_height * 0.5; + } else { + top_left[Geom::Y] = baseline_y - _spans[_characters[char_index].in_span].line_height.ascent; + bottom_right[Geom::Y] = baseline_y + _spans[_characters[char_index].in_span].line_height.descent; + } + + if (rotation) { + if (it._glyph_index == -1) + *rotation = 0.0; + else if (it._glyph_index == (int)_glyphs.size()) + *rotation = _glyphs.back().rotation; + else + *rotation = _glyphs[it._glyph_index].rotation; + } + } + + return Geom::Rect(top_left, bottom_right); +} + +std::vector<Geom::Point> Layout::createSelectionShape(iterator const &it_start, iterator const &it_end, Geom::Affine const &transform) const +{ + std::vector<Geom::Point> quads; + unsigned char_index; + unsigned end_char_index; + + if (it_start._char_index < it_end._char_index) { + char_index = it_start._char_index; + end_char_index = it_end._char_index; + } else { + char_index = it_end._char_index; + end_char_index = it_start._char_index; + } + for ( ; char_index < end_char_index ; ) { + if (_characters[char_index].in_glyph == -1) { + char_index++; + continue; + } + double char_rotation = _glyphs[_characters[char_index].in_glyph].rotation; + unsigned span_index = _characters[char_index].in_span; + + Geom::Point top_left, bottom_right; + if (_path_fitted || char_rotation != 0.0) { + Geom::Rect box = characterBoundingBox(iterator(this, char_index), &char_rotation); + top_left = box.min(); + bottom_right = box.max(); + char_index++; + } else { // for straight text we can be faster by combining all the character boxes in a span into one box + double span_x = _spans[span_index].x_start + _spans[span_index].chunk(this).left_x; + top_left[Geom::X] = span_x + _characters[char_index].x; + while (char_index < end_char_index && _characters[char_index].in_span == span_index) + char_index++; + if (char_index == _characters.size() || _characters[char_index].in_span != span_index) + bottom_right[Geom::X] = _spans[span_index].x_end + _spans[span_index].chunk(this).left_x; + else + bottom_right[Geom::X] = span_x + _characters[char_index].x; + + double baseline_y = _spans[span_index].line(this).baseline_y + _spans[span_index].baseline_shift; + double vertical_scale = _glyphs.back().vertical_scale; + double offset_y = _spans[span_index].y_offset; + + if (_directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM)) { + double span_height = vertical_scale * _spans[span_index].line_height.emSize(); + top_left[Geom::Y] = top_left[Geom::X]; + top_left[Geom::X] = offset_y + baseline_y - span_height * 0.5; + bottom_right[Geom::Y] = bottom_right[Geom::X]; + bottom_right[Geom::X] = offset_y + baseline_y + span_height * 0.5; + } else { + top_left[Geom::Y] = offset_y + baseline_y - vertical_scale * _spans[span_index].line_height.ascent; + bottom_right[Geom::Y] = offset_y + baseline_y + vertical_scale * _spans[span_index].line_height.descent; + } + } + + Geom::Rect char_box(top_left, bottom_right); + if (char_box.dimensions()[Geom::X] == 0.0 || char_box.dimensions()[Geom::Y] == 0.0) + continue; + Geom::Point center_of_rotation((top_left[Geom::X] + bottom_right[Geom::X]) * 0.5, + top_left[Geom::Y] + _spans[span_index].line_height.ascent); + Geom::Affine total_transform = Geom::Translate(-center_of_rotation) * Geom::Rotate(char_rotation) * Geom::Translate(center_of_rotation) * transform; + for(int i = 0; i < 4; i ++) + quads.push_back(char_box.corner(i) * total_transform); + } + return quads; +} + +void Layout::queryCursorShape(iterator const &it, Geom::Point &position, double &height, double &rotation) const +{ + if (_characters.empty()) { + position = _empty_cursor_shape.position; + height = _empty_cursor_shape.height; + rotation = _empty_cursor_shape.rotation; + } else { + // we want to cursor to be positioned where the left edge of a character that is about to be typed will be. + // this means x & rotation are the current values but y & height belong to the previous character. + // this isn't quite right because dx attributes will be moved along, but it's good enough + Span const *span; + bool vertical_text = _directions_are_orthogonal(_blockProgression(), TOP_TO_BOTTOM); + if (_path_fitted) { + // text on a path + double x; + if (it._char_index >= _characters.size()) { + span = &_spans.back(); + x = span->x_end + _chunks.back().left_x - _chunks[0].left_x; + } else { + span = &_spans[_characters[it._char_index].in_span]; + x = _chunks[span->in_chunk].left_x + span->x_start + _characters[it._char_index].x - _chunks[0].left_x; + if (vertical_text) + x -= span->line_height.descent; + if (it._char_index != 0) + span = &_spans[_characters[it._char_index - 1].in_span]; + } + double path_length = const_cast<Path*>(_path_fitted)->Length(); + double x_on_path = x; + if (x_on_path < 0.0) x_on_path = 0.0; + + int unused = 0; + // as far as I know these functions are const, they're just not marked as such + Path::cut_position *path_parameter_list = const_cast<Path*>(_path_fitted)->CurvilignToPosition(1, &x_on_path, unused); + Path::cut_position path_parameter; + if (path_parameter_list != nullptr && path_parameter_list[0].piece >= 0) + path_parameter = path_parameter_list[0]; + else { + path_parameter.piece = _path_fitted->descr_cmd.size() - 1; + path_parameter.t = 0.9999; // 1.0 will get the wrong tangent + } + g_free(path_parameter_list); + + Geom::Point point; + Geom::Point tangent; + const_cast<Path*>(_path_fitted)->PointAndTangentAt(path_parameter.piece, path_parameter.t, point, tangent); + if (x < 0.0) + point += x * tangent; + if (x > path_length ) + point += (x - path_length) * tangent; + if (vertical_text) { + rotation = atan2(-tangent[Geom::X], tangent[Geom::Y]); + position[Geom::X] = point[Geom::Y] - tangent[Geom::X] * span->baseline_shift; + position[Geom::Y] = point[Geom::X] + tangent[Geom::Y] * span->baseline_shift; + } else { + rotation = atan2(tangent); + position[Geom::X] = point[Geom::X] - tangent[Geom::Y] * span->baseline_shift; + position[Geom::Y] = point[Geom::Y] + tangent[Geom::X] * span->baseline_shift; + } + + } else { + // text is not on a path + + bool last_char_is_newline = false; + if (it._char_index >= _characters.size()) { + span = &_spans.back(); + position[Geom::X] = _chunks[span->in_chunk].left_x + span->x_end; + rotation = _glyphs.empty() ? 0.0 : _glyphs.back().rotation; + + // Check if last character is new line. + if (_characters.back().the_char == '\n') { + last_char_is_newline = true; + position[Geom::X] = chunkAnchorPoint(it)[vertical_text ? Geom::Y : Geom::X]; + } + } else { + span = &_spans[_characters[it._char_index].in_span]; + position[Geom::X] = _chunks[span->in_chunk].left_x + span->x_start + _characters[it._char_index].x; + if (it._glyph_index == -1) { + rotation = 0.0; + } else if(it._glyph_index == 0) { + rotation = _glyphs.empty() ? 0.0 : _glyphs[0].rotation; + } else{ + rotation = _glyphs[it._glyph_index - 1].rotation; + } + // the first char in a line wants to have the y of the new line, so in that case we don't switch to the previous span + if (it._char_index != 0 && _characters[it._char_index - 1].chunk(this).in_line == _chunks[span->in_chunk].in_line) + span = &_spans[_characters[it._char_index - 1].in_span]; + } + position[Geom::Y] = span->line(this).baseline_y + span->baseline_shift + span->y_offset; + + if (last_char_is_newline) { + // Move cursor to empty new line. + double vertical_scale = _glyphs.empty() ? 1.0 : _glyphs.back().vertical_scale; + if (vertical_text) { + // Vertical text + position[Geom::Y] -= vertical_scale * span->line_height.emSize(); + } else { + position[Geom::Y] += vertical_scale * span->line_height.emSize(); + } + } + } + + // up to now *position is the baseline point, not the final point which will be the bottom of the descent + double vertical_scale = _glyphs.empty() ? 1.0 : _glyphs.back().vertical_scale; + + if (vertical_text) { + // Vertical text + height = vertical_scale * span->line_height.emSize(); + rotation += M_PI / 2; + std::swap(position[Geom::X], position[Geom::Y]); + position[Geom::X] -= vertical_scale * sin(rotation) * height * 0.5; + position[Geom::Y] += vertical_scale * cos(rotation) * height * 0.5; + } else { + // Horizontal text + double caret_slope_run = 0.0, caret_slope_rise = 1.0; + if (span->font) + const_cast<font_instance*>(span->font)->FontSlope(caret_slope_run, caret_slope_rise); + double caret_slope = atan2(caret_slope_run, caret_slope_rise); + height = vertical_scale * (span->line_height.emSize()) / cos(caret_slope); + rotation += caret_slope; + position[Geom::X] -= sin(rotation) * vertical_scale * span->line_height.descent; + position[Geom::Y] += cos(rotation) * vertical_scale * span->line_height.descent; + } + } +} + +bool Layout::isHidden(iterator const &it) const +{ + return _characters[it._char_index].line(this).hidden; +} + + +void Layout::getSourceOfCharacter(iterator const &it, SPObject **source, Glib::ustring::iterator *text_iterator) const +{ + if (it._char_index >= _characters.size()) { + *source = nullptr; + return; + } + InputStreamItem *stream_item = _input_stream[_spans[_characters[it._char_index].in_span].in_input_stream_item]; + *source = stream_item->source; + if (text_iterator && stream_item->Type() == TEXT_SOURCE) { + InputStreamTextSource *text_source = dynamic_cast<InputStreamTextSource *>(stream_item); + + // In order to return a non-const iterator in text_iterator, do the const_cast here. + // Note that, although ugly, it is safe because we do not write to *iterator anywhere. + Glib::ustring::iterator text_iter = const_cast<Glib::ustring *>(text_source->text)->begin(); + + unsigned char_index = it._char_index; + unsigned original_input_source_index = _spans[_characters[char_index].in_span].in_input_stream_item; + // confusing algorithm because the iterator goes forwards while the index goes backwards. + // It's just that it's faster doing it that way + while (char_index && _spans[_characters[char_index - 1].in_span].in_input_stream_item == original_input_source_index) { + ++text_iter; + char_index--; + } + + if (text_iterator) { + *text_iterator = text_iter; + } + } +} + +void Layout::simulateLayoutUsingKerning(iterator const &from, iterator const &to, OptionalTextTagAttrs *result) const +{ + SVGLength zero_length; + zero_length = 0.0; + + result->x.clear(); + result->y.clear(); + result->dx.clear(); + result->dy.clear(); + result->rotate.clear(); + if (to._char_index <= from._char_index) + return; + result->dx.reserve(to._char_index - from._char_index); + result->dy.reserve(to._char_index - from._char_index); + result->rotate.reserve(to._char_index - from._char_index); + for (unsigned char_index = from._char_index ; char_index < to._char_index ; char_index++) { + if (!_characters[char_index].char_attributes.is_char_break) + continue; + if (char_index == 0) + continue; + if (_characters[char_index].chunk(this).in_line != _characters[char_index - 1].chunk(this).in_line) + continue; + + unsigned prev_cluster_char_index; + for (prev_cluster_char_index = char_index - 1 ; + prev_cluster_char_index != 0 && !_characters[prev_cluster_char_index].char_attributes.is_cursor_position ; + prev_cluster_char_index--){}; + if (_characters[char_index].span(this).in_chunk == _characters[char_index - 1].span(this).in_chunk) { + // dx is zero for the first char in a chunk + // this algorithm works by comparing the summed widths of the glyphs with the observed + // difference in x coordinates of characters, and subtracting the two to produce the x kerning. + double glyphs_width = 0.0; + if (_characters[prev_cluster_char_index].in_glyph != -1) + for (int glyph_index = _characters[prev_cluster_char_index].in_glyph ; glyph_index < _characters[char_index].in_glyph ; glyph_index++) + glyphs_width += _glyphs[glyph_index].advance; + if (_characters[char_index].span(this).direction == RIGHT_TO_LEFT) + glyphs_width = -glyphs_width; + + double dx = (_characters[char_index].x + _characters[char_index].span(this).x_start + - _characters[prev_cluster_char_index].x - _characters[prev_cluster_char_index].span(this).x_start) + - glyphs_width; + + + InputStreamItem *input_item = _input_stream[_characters[char_index].span(this).in_input_stream_item]; + if (input_item->Type() == TEXT_SOURCE) { + SPStyle const *style = static_cast<InputStreamTextSource*>(input_item)->style; + if (_characters[char_index].char_attributes.is_white) + dx -= style->word_spacing.computed * getTextLengthMultiplierDue(); + if (_characters[char_index].char_attributes.is_cursor_position) + dx -= style->letter_spacing.computed * getTextLengthMultiplierDue(); + dx -= getTextLengthIncrementDue(); + } + + if (fabs(dx) > 0.0001) { + result->dx.resize(char_index - from._char_index + 1, zero_length); + result->dx.back() = dx; + } + } + double dy = _characters[char_index].span(this).baseline_shift - _characters[prev_cluster_char_index].span(this).baseline_shift; + if (fabs(dy) > 0.0001) { + result->dy.resize(char_index - from._char_index + 1, zero_length); + result->dy.back() = dy; + } + if (_characters[char_index].in_glyph != -1 && _glyphs[_characters[char_index].in_glyph].rotation != 0.0) { + result->rotate.resize(char_index - from._char_index + 1, zero_length); + result->rotate.back() = _glyphs[_characters[char_index].in_glyph].rotation; + } + } +} + +#define PREV_START_OF_ITEM(this_func) \ + { \ + _cursor_moving_vertically = false; \ + if (_char_index == 0) return false; \ + _char_index--; \ + return this_func(); \ + } +// end of macro + +#define THIS_START_OF_ITEM(item_getter) \ + { \ + _cursor_moving_vertically = false; \ + if (_char_index == 0) return false; \ + unsigned original_item; \ + if (_char_index == _parent_layout->_characters.size()) { \ + _char_index--; \ + original_item = item_getter; \ + } else { \ + original_item = item_getter; \ + _char_index--; \ + } \ + while (item_getter == original_item) { \ + if (_char_index == 0) { \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } \ + _char_index--; \ + } \ + _char_index++; \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +#define NEXT_START_OF_ITEM(item_getter) \ + { \ + _cursor_moving_vertically = false; \ + if (_char_index == _parent_layout->_characters.size()) return false; \ + unsigned original_item = item_getter; \ + for( ; ; ) { \ + _char_index++; \ + if (_char_index == _parent_layout->_characters.size()) { \ + _glyph_index = _parent_layout->_glyphs.size(); \ + return false; \ + } \ + if (item_getter != original_item) break; \ + } \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +bool Layout::iterator::prevStartOfSpan() + PREV_START_OF_ITEM(thisStartOfSpan); + +bool Layout::iterator::thisStartOfSpan() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].in_span); + +bool Layout::iterator::nextStartOfSpan() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].in_span); + + +bool Layout::iterator::prevStartOfChunk() + PREV_START_OF_ITEM(thisStartOfChunk); + +bool Layout::iterator::thisStartOfChunk() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_chunk); + +bool Layout::iterator::nextStartOfChunk() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_chunk); + + +bool Layout::iterator::prevStartOfLine() + PREV_START_OF_ITEM(thisStartOfLine); + +bool Layout::iterator::thisStartOfLine() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].chunk(_parent_layout).in_line); + +bool Layout::iterator::nextStartOfLine() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].chunk(_parent_layout).in_line); + + +bool Layout::iterator::prevStartOfShape() + PREV_START_OF_ITEM(thisStartOfShape); + +bool Layout::iterator::thisStartOfShape() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_shape); + +bool Layout::iterator::nextStartOfShape() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_shape); + + +bool Layout::iterator::prevStartOfParagraph() + PREV_START_OF_ITEM(thisStartOfParagraph); + +bool Layout::iterator::thisStartOfParagraph() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_paragraph); + +bool Layout::iterator::nextStartOfParagraph() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].line(_parent_layout).in_paragraph); + + +bool Layout::iterator::prevStartOfSource() + PREV_START_OF_ITEM(thisStartOfSource); + +bool Layout::iterator::thisStartOfSource() + THIS_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_input_stream_item); + +bool Layout::iterator::nextStartOfSource() + NEXT_START_OF_ITEM(_parent_layout->_characters[_char_index].span(_parent_layout).in_input_stream_item); + + +bool Layout::iterator::thisEndOfLine() +{ + if (_char_index == _parent_layout->_characters.size()) return false; + if (nextStartOfLine()) + { + if (_char_index && _parent_layout->_characters[_char_index - 1].char_attributes.is_white) + return prevCursorPosition(); + return true; + } + if (_char_index && _parent_layout->_characters[_char_index - 1].chunk(_parent_layout).in_line != _parent_layout->_lines.size() - 1) + return prevCursorPosition(); // for when the last paragraph is empty + return false; +} + +void Layout::iterator::beginCursorUpDown() +{ + if (_char_index == _parent_layout->_characters.size()) + _x_coordinate = _parent_layout->_chunks.back().left_x + _parent_layout->_spans.back().x_end; + else + _x_coordinate = _parent_layout->_characters[_char_index].x + _parent_layout->_characters[_char_index].span(_parent_layout).x_start + _parent_layout->_characters[_char_index].chunk(_parent_layout).left_x; + _cursor_moving_vertically = true; +} + +bool Layout::iterator::nextLineCursor(int n) +{ + if (!_cursor_moving_vertically) + beginCursorUpDown(); + if (_char_index == _parent_layout->_characters.size()) + return false; + unsigned line_index = _parent_layout->_characters[_char_index].chunk(_parent_layout).in_line; + if (line_index == _parent_layout->_lines.size() - 1) + return false; // nowhere to go + else + n = MIN (n, static_cast<int>(_parent_layout->_lines.size() - 1 - line_index)); + if (_parent_layout->_lines[line_index + n].in_shape != _parent_layout->_lines[line_index].in_shape) { + // switching between shapes: adjust the stored x to compensate + _x_coordinate += _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index + n)].in_chunk].left_x + - _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index)].in_chunk].left_x; + } + _char_index = _parent_layout->_cursorXOnLineToIterator(line_index + n, _x_coordinate)._char_index; + if (_char_index == _parent_layout->_characters.size()) + _glyph_index = _parent_layout->_glyphs.size(); + else + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; + return true; +} + +bool Layout::iterator::prevLineCursor(int n) +{ + if (!_cursor_moving_vertically) + beginCursorUpDown(); + int line_index; + if (_char_index == _parent_layout->_characters.size()) + line_index = _parent_layout->_lines.size() - 1; + else + line_index = _parent_layout->_characters[_char_index].chunk(_parent_layout).in_line; + if (line_index <= 0) + return false; // nowhere to go + else + n = MIN (n, static_cast<int>(line_index)); + if (_parent_layout->_lines[line_index - n].in_shape != _parent_layout->_lines[line_index].in_shape) { + // switching between shapes: adjust the stored x to compensate + _x_coordinate += _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index - n)].in_chunk].left_x + - _parent_layout->_chunks[_parent_layout->_spans[_parent_layout->_lineToSpan(line_index)].in_chunk].left_x; + } + _char_index = _parent_layout->_cursorXOnLineToIterator(line_index - n, _x_coordinate)._char_index; + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; + return true; +} + +#define NEXT_WITH_ATTRIBUTE_SET(attr) \ + { \ + _cursor_moving_vertically = false; \ + for ( ; ; ) { \ + if (_char_index + 1 >= _parent_layout->_characters.size()) { \ + _char_index = _parent_layout->_characters.size(); \ + _glyph_index = _parent_layout->_glyphs.size(); \ + return false; \ + } \ + _char_index++; \ + if (_parent_layout->_characters[_char_index].char_attributes.attr) break; \ + } \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +#define PREV_WITH_ATTRIBUTE_SET(attr) \ + { \ + _cursor_moving_vertically = false; \ + for ( ; ; ) { \ + if (_char_index == 0) { \ + _glyph_index = 0; \ + return false; \ + } \ + _char_index--; \ + if (_parent_layout->_characters[_char_index].char_attributes.attr) break; \ + } \ + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; \ + return true; \ + } +// end of macro + +bool Layout::iterator::nextCursorPosition() + NEXT_WITH_ATTRIBUTE_SET(is_cursor_position); + +bool Layout::iterator::prevCursorPosition() + PREV_WITH_ATTRIBUTE_SET(is_cursor_position); + +bool Layout::iterator::nextStartOfWord() + NEXT_WITH_ATTRIBUTE_SET(is_word_start); + +bool Layout::iterator::prevStartOfWord() + PREV_WITH_ATTRIBUTE_SET(is_word_start); + +bool Layout::iterator::nextEndOfWord() + NEXT_WITH_ATTRIBUTE_SET(is_word_end); + +bool Layout::iterator::prevEndOfWord() + PREV_WITH_ATTRIBUTE_SET(is_word_end); + +bool Layout::iterator::nextStartOfSentence() + NEXT_WITH_ATTRIBUTE_SET(is_sentence_start); + +bool Layout::iterator::prevStartOfSentence() + PREV_WITH_ATTRIBUTE_SET(is_sentence_start); + +bool Layout::iterator::nextEndOfSentence() + NEXT_WITH_ATTRIBUTE_SET(is_sentence_end); + +bool Layout::iterator::prevEndOfSentence() + PREV_WITH_ATTRIBUTE_SET(is_sentence_end); + +bool Layout::iterator::_cursorLeftOrRightLocalX(Direction direction) +{ + // the only reason this function is so complicated is to enable visual cursor + // movement moving in to or out of counterdirectional runs + if (_parent_layout->_characters.empty()) return false; + unsigned old_span_index; + Direction old_span_direction; + if (_char_index == _parent_layout->_characters.size()) + old_span_index = _parent_layout->_spans.size() - 1; + else + old_span_index = _parent_layout->_characters[_char_index].in_span; + old_span_direction = _parent_layout->_spans[old_span_index].direction; + Direction para_direction = _parent_layout->_spans[old_span_index].paragraph(_parent_layout).base_direction; + + int scan_direction; + unsigned old_char_index = _char_index; + if (old_span_direction != para_direction + && ((_char_index == 0 && direction == para_direction) + || (_char_index == _parent_layout->_characters.size() && direction != para_direction))) { + // the end of the text is actually in the middle because of reordering. Do cleverness + scan_direction = direction == para_direction ? +1 : -1; + } else { + if (direction == old_span_direction) { + if (!nextCursorPosition()) return false; + } else { + if (!prevCursorPosition()) return false; + } + + unsigned new_span_index = _parent_layout->_characters[_char_index].in_span; + if (new_span_index == old_span_index) return true; + if (old_span_direction != _parent_layout->_spans[new_span_index].direction) { + // we must jump to the other end of a counterdirectional run + scan_direction = direction == para_direction ? +1 : -1; + } else if (_parent_layout->_spans[old_span_index].in_chunk != _parent_layout->_spans[new_span_index].in_chunk) { + // we might have to do a weird jump when we would have crossed a chunk/line break + if (_parent_layout->_spans[old_span_index].line(_parent_layout).in_paragraph != _parent_layout->_spans[new_span_index].line(_parent_layout).in_paragraph) + return true; + if (old_span_direction == para_direction) + return true; + scan_direction = direction == para_direction ? +1 : -1; + } else + return true; // same direction, same chunk: no cleverness required + } + + unsigned new_span_index = old_span_index; + for ( ; ; ) { + if (scan_direction > 0) { + if (new_span_index == _parent_layout->_spans.size() - 1) { + if (_parent_layout->_spans[new_span_index].direction == old_span_direction) { + _char_index = old_char_index; + return false; // the visual end is in the logical middle + } + break; + } + new_span_index++; + } else { + if (new_span_index == 0) { + if (_parent_layout->_spans[new_span_index].direction == old_span_direction) { + _char_index = old_char_index; + return false; // the visual end is in the logical middle + } + break; + } + new_span_index--; + } + if (_parent_layout->_spans[new_span_index].direction == para_direction) { + if (para_direction == old_span_direction) + new_span_index -= scan_direction; + break; + } + if (_parent_layout->_spans[new_span_index].in_chunk != _parent_layout->_spans[old_span_index].in_chunk) { + if (_parent_layout->_spans[old_span_index].line(_parent_layout).in_paragraph == _parent_layout->_spans[new_span_index].line(_parent_layout).in_paragraph + && para_direction == old_span_direction) + new_span_index -= scan_direction; + break; + } + } + + // found the correct span, now find the correct character + if (_parent_layout->_spans[old_span_index].line(_parent_layout).in_paragraph != _parent_layout->_spans[new_span_index].line(_parent_layout).in_paragraph) { + if (new_span_index > old_span_index) + _char_index = _parent_layout->_spanToCharacter(new_span_index); + else + _char_index = _parent_layout->_spanToCharacter(new_span_index + 1) - 1; + } else { + if (_parent_layout->_spans[new_span_index].direction != direction) { + if (new_span_index >= _parent_layout->_spans.size() - 1) + _char_index = _parent_layout->_characters.size(); + else + _char_index = _parent_layout->_spanToCharacter(new_span_index + 1) - 1; + } else + _char_index = _parent_layout->_spanToCharacter(new_span_index); + } + if (_char_index == _parent_layout->_characters.size()) { + _glyph_index = _parent_layout->_glyphs.size(); + return false; + } + _glyph_index = _parent_layout->_characters[_char_index].in_glyph; + return _char_index != 0; +} + +bool Layout::iterator::_cursorLeftOrRightLocalXByWord(Direction direction) +{ + bool r; + while ((r = _cursorLeftOrRightLocalX(direction)) + && !_parent_layout->_characters[_char_index].char_attributes.is_word_start){}; + return r; +} + +bool Layout::iterator::cursorUp(int n) +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return prevLineCursor(n); + else if(block_progression == BOTTOM_TO_TOP) + return nextLineCursor(n); + else + return _cursorLeftOrRightLocalX(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorDown(int n) +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return nextLineCursor(n); + else if(block_progression == BOTTOM_TO_TOP) + return prevLineCursor(n); + else + return _cursorLeftOrRightLocalX(LEFT_TO_RIGHT); +} + +bool Layout::iterator::cursorLeft() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return prevLineCursor(); + else if(block_progression == RIGHT_TO_LEFT) + return nextLineCursor(); + else + return _cursorLeftOrRightLocalX(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorRight() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return nextLineCursor(); + else if(block_progression == RIGHT_TO_LEFT) + return prevLineCursor(); + else + return _cursorLeftOrRightLocalX(LEFT_TO_RIGHT); +} + +bool Layout::iterator::cursorUpWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return prevStartOfParagraph(); + else if(block_progression == BOTTOM_TO_TOP) + return nextStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorDownWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == TOP_TO_BOTTOM) + return nextStartOfParagraph(); + else if(block_progression == BOTTOM_TO_TOP) + return prevStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(LEFT_TO_RIGHT); +} + +bool Layout::iterator::cursorLeftWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return prevStartOfParagraph(); + else if(block_progression == RIGHT_TO_LEFT) + return nextStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(RIGHT_TO_LEFT); +} + +bool Layout::iterator::cursorRightWithControl() +{ + Direction block_progression = _parent_layout->_blockProgression(); + if(block_progression == LEFT_TO_RIGHT) + return nextStartOfParagraph(); + else if(block_progression == RIGHT_TO_LEFT) + return prevStartOfParagraph(); + else + return _cursorLeftOrRightLocalXByWord(LEFT_TO_RIGHT); +} + +}//namespace Text +}//namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : |