// SPDX-License-Identifier: GPL-2.0-or-later /* * Inkscape::Text::Layout - text layout engine output functions using iterators * * Authors: * Richard Hughes * * 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_fitted)->PointToCurvilignPosition(Geom::Point(x, y)); local_x = const_cast(_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(_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 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(); 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(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_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_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 Layout::createSelectionShape(iterator const &it_start, iterator const &it_end, Geom::Affine const &transform) const { std::vector 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 = std::min((size_t)it_end._char_index, _characters.size()); } else { char_index = it_end._char_index; end_char_index = std::min((size_t)it_start._char_index, _characters.size()); } 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_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_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_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) { 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(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(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(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 = _parent_layout->_characters.size() - 1; \ 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(_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(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 :