#include "paragraph.h" #include "ct.h" #include "render.h" #include #include #include #include namespace ng { namespace { static std::string representation_for (uint32_t ch) { static uint32_t const SpaceCharacters[] = { 0x200B, // ZERO WIDTH SPACE 0x200C, // ZERO WIDTH NON-JOINER 0x200D, // ZERO WIDTH JOINER 0x2028, // LINE SEPARATOR 0x2029, // PARAGRAPH SEPARATOR 0x2060, // WORD JOINER 0xFEFF // ZERO WIDTH NO-BREAK SPACE }; if(0x20 <= ch && ch <= 0x7E || ch == '\t' || ch == '\n') return NULL_STR; switch(ch) { case '\f': return ""; case '\r': return ""; case '\b': return ""; case 0x00: return ""; case 0x1B: return ""; case 0x1C: return ""; case 0x1D: return ""; case 0x1E: return ""; case 0x1F: return ""; case 0xA0: return "·"; default: { if(0x00 < ch && ch <= 'Z'-'A'+1) return "^" + std::string(1, ch-1+'A'); else if(ch < 0x20 || (0x7E < ch && ch < 0xA0)) return "◆"; else if(0xE000 <= ch && ch <= 0xF8FF && ch != utf8::to_ch("") || oak::contains(beginof(SpaceCharacters), endof(SpaceCharacters), ch)) return text::format("", ch); else if(0x0F0000 <= ch && ch <= 0x0FFFFD || 0x100000 <= ch && ch <= 0x10FFFD) return text::format("", ch); } break; } return NULL_STR; } static void draw_line (CGPoint pos, std::string const& text, CGColorRef color, CTFontRef font, CGContextRef context, bool isFlipped) { ASSERT(utf8::is_valid(text.begin(), text.end())); CFMutableAttributedStringRef str = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0); CFAttributedStringReplaceString(str, CFRangeMake(0, 0), cf::wrap(text)); CFAttributedStringSetAttribute(str, CFRangeMake(0, CFAttributedStringGetLength(str)), kCTFontAttributeName, font); CFAttributedStringSetAttribute(str, CFRangeMake(0, CFAttributedStringGetLength(str)), kCTForegroundColorAttributeName, color); CTLineRef line = CTLineCreateWithAttributedString(str); CFRelease(str); CGContextSaveGState(context); if(isFlipped) CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, 2 * pos.y)); CGContextSetTextPosition(context, pos.x, pos.y); CTLineDraw(line, context); CGContextRestoreGState(context); CFRelease(line); } } // ======================= // = paragraph_t::node_t = // ======================= void paragraph_t::node_t::insert (size_t i, size_t len) { _length += len; _line.reset(); } void paragraph_t::node_t::erase (size_t from, size_t to) { ASSERT_LE(from, to); ASSERT_LE(to, _length); _length -= to - from; _line.reset(); } void paragraph_t::node_t::did_update_scopes (size_t from, size_t to) { _line.reset(); } void paragraph_t::node_t::layout (CGFloat x, CGFloat tabWidth, theme_ptr const& theme, std::string const& fontName, CGFloat fontSize, bool softWrap, size_t wrapColumn, ct::metrics_t const& metrics, ng::buffer_t const& buffer, size_t bufferOffset, std::string const& fillStr) { if(_line) return; switch(_type) { case kNodeTypeText: { _line.reset(new ct::line_t(buffer.substr(bufferOffset, bufferOffset + _length), buffer.scopes(bufferOffset, bufferOffset + _length), theme, fontName, fontSize, NULL)); } break; case kNodeTypeUnprintable: { std::string str = representation_for(utf8::to_ch(buffer.substr(bufferOffset, bufferOffset + _length))); std::map scopes; scopes[0] = buffer.scope(bufferOffset).right.append("deco.unprintable"); _line.reset(new ct::line_t(str, scopes, theme, fontName, fontSize, NULL)); } break; case kNodeTypeTab: { update_tab_width(x, tabWidth, metrics); } break; case kNodeTypeFolding: { std::map scopes; scopes[0] = buffer.scope(bufferOffset).right.append("deco.folding"); _line.reset(new ct::line_t("…", scopes, theme, fontName, fontSize, NULL)); } break; case kNodeTypeSoftBreak: { std::map scopes; scopes[0] = buffer.scope(bufferOffset).right.append("deco.indented-wrap"); _line.reset(new ct::line_t(fillStr, scopes, theme, fontName, fontSize, NULL)); } break; } } void paragraph_t::node_t::reset_font_metrics (ct::metrics_t const& metrics) { _line.reset(); } CGFloat paragraph_t::node_t::width () const { return _line ? _line->width() : _width; } void paragraph_t::node_t::update_tab_width (CGFloat x, CGFloat tabWidth, ct::metrics_t const& metrics) { double r = remainder(x, tabWidth); _width = (r < 0 ? 0 : tabWidth) - r; if(_width < 0.5 * metrics.column_width()) _width += tabWidth; } void paragraph_t::node_t::draw_background (theme_ptr const& theme, std::string const& fontName, CGFloat fontSize, CGContextRef context, bool isFlipped, CGRect visibleRect, bool showInvisibles, CGColorRef backgroundColor, ng::buffer_t const& buffer, size_t bufferOffset, CGPoint anchor, CGFloat lineHeight) const { if(_line) _line->draw_background(CGPointMake(anchor.x, anchor.y), lineHeight, context, isFlipped, backgroundColor); if(_type != kNodeTypeText) { scope::scope_t scope = _type == kNodeTypeSoftBreak ? buffer.scope(bufferOffset).left : buffer.scope(bufferOffset).right; switch(_type) { case kNodeTypeUnprintable: scope = scope.append("deco.unprintable"); break; case kNodeTypeFolding: scope = scope.append("deco.folding"); break; case kNodeTypeSoftBreak: scope = scope.append("deco.indented-wrap"); break; } styles_t const styles = theme->styles_for_scope(scope, fontName, fontSize); if(!CFEqual(backgroundColor, styles.background())) { CGFloat x1 = round(anchor.x); CGFloat x2 = round(_type == kNodeTypeSoftBreak || _type == kNodeTypeNewline ? CGRectGetMaxX(visibleRect) : anchor.x + _width); render::fill_rect(context, styles.background(), CGRectMake(x1, anchor.y, x2 - x1, lineHeight)); } } } void paragraph_t::node_t::draw_foreground (theme_ptr const& theme, std::string const& fontName, CGFloat fontSize, CGContextRef context, bool isFlipped, CGRect visibleRect, bool showInvisibles, CGColorRef textColor, ng::buffer_t const& buffer, size_t bufferOffset, std::vector< std::pair > const& misspelled, CGPoint anchor, CGFloat baseline) const { if(_line) _line->draw_foreground(CGPointMake(anchor.x, anchor.y + baseline), context, isFlipped, misspelled); if(showInvisibles || (_type != kNodeTypeTab && _type != kNodeTypeNewline)) { std::string str = NULL_STR; scope::scope_t scope = buffer.scope(bufferOffset).right; switch(_type) { case kNodeTypeFolding: str = "…"; scope = scope.append("deco.folding"); break; case kNodeTypeTab: str = "‣"; scope = scope.append("deco.invisible.tab"); break; case kNodeTypeNewline: str = "¬"; scope = scope.append("deco.invisible.newline"); break; } if(str != NULL_STR) { styles_t const styles = theme->styles_for_scope(scope, fontName, fontSize); draw_line(CGPointMake(anchor.x, anchor.y + baseline), str, styles.foreground(), styles.font(), context, isFlipped); } } } // =============== // = paragraph_t = // =============== void paragraph_t::insert (size_t pos, size_t len, ng::buffer_t const& buffer, size_t bufferOffset) { std::vector newNodes; std::string const str = buffer.substr(pos, pos + len); size_t from = 0, i = 0; citerate(ch, diacritics::make_range(str.data(), str.data() + str.size())) { if(*ch == '\t' || *ch == '\n' || representation_for(*ch) != NULL_STR) { if(from != i) insert_text(pos - bufferOffset + from, i - from); if(*ch == '\t') insert_tab(pos - bufferOffset + i); else if(*ch == '\n') insert_newline(pos - bufferOffset + i, ch.length()); else insert_unprintable(pos - bufferOffset + i, ch.length()); from = i + ch.length(); } i += ch.length(); } if(from != str.size()) insert_text(pos - bufferOffset + from, str.size() - from); _dirty = true; } void paragraph_t::insert_folded (size_t pos, size_t len, ng::buffer_t const& buffer, size_t bufferOffset) { _nodes.insert(iterator_at(pos - bufferOffset), node_t(kNodeTypeFolding, len)); _dirty = true; } void paragraph_t::erase (size_t from, size_t to, ng::buffer_t const& buffer, size_t bufferOffset) { ASSERT_LE(bufferOffset, from); ASSERT_LE(to, bufferOffset + length()); size_t i = bufferOffset; iterate(node, _nodes) { size_t len = node->length(); if(i <= from && from < i + len) { size_t last = std::min(to - i, len); node->erase(from - i, last); from = i + last; if(to - i <= last) break; } i += len; } for(auto it = _nodes.begin(); it != _nodes.end(); ) { if(it->length() == 0 && it->type() != kNodeTypeSoftBreak) it = _nodes.erase(it); else ++it; } if(from != to) fprintf(stderr, "error erasing %zu-%zu, %zu\n", from, to, bufferOffset); _dirty = true; } void paragraph_t::did_update_scopes (size_t from, size_t to, ng::buffer_t const& buffer, size_t bufferOffset) { size_t i = bufferOffset; iterate(node, _nodes) { node->did_update_scopes(from - i, to - i); i += node->length(); } _dirty = true; } bool paragraph_t::layout (theme_ptr const& theme, std::string const& fontName, CGFloat fontSize, bool softWrap, size_t wrapColumn, ct::metrics_t const& metrics, CGRect visibleRect, ng::buffer_t const& buffer, size_t bufferOffset) { if(!_dirty) return false; std::vector newNodes; bool hasFoldings = false; iterate(node, _nodes) { if(node->type() != kNodeTypeSoftBreak) newNodes.push_back(*node); hasFoldings = hasFoldings || node->type() == kNodeTypeFolding; } _nodes.swap(newNodes); size_t const tabSize = buffer.indent().tab_size(); std::string fillStr = NULL_STR; if(!hasFoldings && softWrap) { fillStr = ""; size_t fillStrWidth = 0; std::string str = buffer.substr(bufferOffset, bufferOffset + length()); ASSERT(utf8::is_valid(str.begin(), str.end())); bundles::item_ptr indentedSoftWrapItem; scope::context_t scope(buffer.scope(bufferOffset, false).right, buffer.scope(bufferOffset + length(), false).left); plist::any_t const& indentedSoftWrapValue = bundles::value_for_setting("indentedSoftWrap", scope, &indentedSoftWrapItem); if(indentedSoftWrapItem) { std::string pattern, format; if(plist::get_key_path(indentedSoftWrapValue, "match", pattern) && plist::get_key_path(indentedSoftWrapValue, "format", format)) { if(regexp::match_t const& m = regexp::search(pattern, str.data(), str.data() + str.size())) { std::string tmp = format_string::expand(format, m.captures()); citerate(ch, diacritics::make_range(tmp.data(), tmp.data() + tmp.size())) { if(*ch == '\t') fillStr.append(std::string(tabSize - (fillStrWidth % tabSize), ' ')); else fillStr.append(&ch, ch.length()); fillStrWidth += (*ch == '\t' ? tabSize - (fillStrWidth % tabSize) : 1); } } } if(wrapColumn < fillStrWidth) { fillStr = " "; fillStrWidth = 4; } } citerate(offset, text::soft_breaks(str, wrapColumn, tabSize, fillStrWidth)) _nodes.insert(iterator_at(*offset), node_t(kNodeTypeSoftBreak, 0, fillStrWidth * metrics.column_width())); } CGFloat x = 0; size_t i = bufferOffset; iterate(node, _nodes) { node->layout(x, tabSize * metrics.column_width(), theme, fontName, fontSize, softWrap, wrapColumn, metrics, buffer, i, fillStr); x += node->width(); i += node->length(); } _dirty = false; return true; } std::vector paragraph_t::softlines (ct::metrics_t const& metrics, bool softBreaksOnNewline) const { std::vector softlines; CGFloat x = 0, y = 0; size_t first = 0, firstOffset = 0, offset = 0; CGFloat ascent = 0, descent = 0, leading = 0; for(size_t i = 0; i < _nodes.size(); ++i) { auto node = _nodes.begin() + i; if(node->type() == kNodeTypeSoftBreak) { softlines.push_back(softline_t(firstOffset, x, y, metrics.baseline(ascent), metrics.line_height(ascent, descent, leading), first, i + (softBreaksOnNewline ? 0 : 1))); firstOffset = offset; x = (softBreaksOnNewline ? 0 : node->width()); y += metrics.line_height(ascent, descent, leading); first = i + (softBreaksOnNewline ? 0 : 1); ascent = 0; descent = 0; leading = 0; } if(node->line()) { CGFloat a, d, l; node->line()->width(&a, &d, &l); ascent = std::max(ascent, a); descent = std::max(descent, d); leading = std::max(leading, l); } offset += node->length(); } softlines.push_back(softline_t(firstOffset, x, y, metrics.baseline(ascent), metrics.line_height(ascent, descent, leading), first, _nodes.size())); return softlines; } std::vector::iterator paragraph_t::iterator_at (size_t i) { size_t from = 0; iterate(node, _nodes) { if(from == i) return node; else if(from < i && i < from + node->length()) { ASSERT_EQ(node->type(), kNodeTypeText); size_t len = node->length() - (i - from); node->erase(i - from, node->length()); return _nodes.insert(++node, node_t(kNodeTypeText, len)); } from += node->length(); } return _nodes.end(); } void paragraph_t::insert_text (size_t i, size_t len) { size_t from = 0; iterate(node, _nodes) { if(from <= i && i <= from + node->length() && node->type() == kNodeTypeText) return node->insert(i - from, len); from += node->length(); } _nodes.insert(iterator_at(i), node_t(kNodeTypeText, len)); } void paragraph_t::insert_tab (size_t i) { _nodes.insert(iterator_at(i), node_t(kNodeTypeTab, 1, 10)); } void paragraph_t::insert_unprintable (size_t i, size_t len) { _nodes.insert(iterator_at(i), node_t(kNodeTypeUnprintable, len)); } void paragraph_t::insert_newline (size_t i, size_t len) { _nodes.insert(iterator_at(i), node_t(kNodeTypeNewline, len)); } void paragraph_t::set_wrapping (bool softWrap, size_t wrapColumn, ct::metrics_t const& metrics) { _dirty = true; } void paragraph_t::set_tab_size (size_t tabSize, ct::metrics_t const& metrics) { double const tabWidth = tabSize * metrics.column_width(); auto lines = softlines(metrics); for(size_t i = 0; i < lines.size(); ++i) { CGFloat x = lines[i].x; foreach(node, _nodes.begin() + lines[i].first, _nodes.begin() + lines[i].last) { if(node->type() == kNodeTypeTab) node->update_tab_width(x, tabWidth, metrics); x += node->width(); } } } void paragraph_t::reset_font_metrics (ct::metrics_t const& metrics) { _dirty = true; iterate(node, _nodes) node->reset_font_metrics(metrics); } size_t paragraph_t::length () const { size_t res = 0; iterate(node, _nodes) res += node->length(); return res; } CGFloat paragraph_t::width () const { CGFloat x = 0, res = 0; iterate(node, _nodes) { if(node->type() == kNodeTypeSoftBreak) x = 0; x += node->width(); res = std::max(x, res); } return res; } CGFloat paragraph_t::height (ct::metrics_t const& metrics) const { auto lines = softlines(metrics); return lines.back().y + lines.back().height; } ng::index_t paragraph_t::index_at_point (CGPoint point, ct::metrics_t const& metrics, ng::buffer_t const& buffer, size_t bufferOffset, CGPoint anchor) const { auto lines = softlines(metrics); for(size_t i = 0; i < lines.size(); ++i) { if(anchor.y + lines[i].y <= point.y && point.y < anchor.y + lines[i].y + lines[i].height) { CGFloat x = lines[i].x; size_t offset = lines[i].offset; foreach(node, _nodes.begin() + lines[i].first, _nodes.begin() + lines[i].last) { if(node->type() == kNodeTypeSoftBreak) { size_t res = bufferOffset + offset; if(i+1 != lines.size() && res == bufferOffset + lines[i+1].offset) res -= buffer[res-1].size(); return res; } else if(node->type() == kNodeTypeNewline) { size_t carry = point.x > anchor.x + x ? (size_t)floor((point.x - (anchor.x + x)) / metrics.column_width()) : 0; return ng::index_t(bufferOffset + offset, carry); } if(point.x <= anchor.x + x) return bufferOffset + offset; else if(anchor.x + x < point.x && point.x < anchor.x + x + node->width()) { CGFloat delta = point.x - (anchor.x + x); if(node->type() == kNodeTypeText && node->line()) { size_t res = bufferOffset + offset + node->line()->index_for_offset(delta); if(i+1 != lines.size() && res == bufferOffset + lines[i+1].offset) res -= buffer[res-1].size(); return res; } return bufferOffset + offset + lround(delta / node->width()) * node->length(); } x += node->width(); offset += node->length(); } } } return bufferOffset + length(); } CGRect paragraph_t::rect_at_index (ng::index_t const& index, ct::metrics_t const& metrics, ng::buffer_t const& buffer, size_t bufferOffset, CGPoint anchor) const { size_t needle = index.index - bufferOffset; CGFloat caretOffset = index.carry * metrics.column_width(); auto lines = softlines(metrics); for(size_t i = 0; i < lines.size(); ++i) { if(lines[i].offset <= needle && (i+1 == lines.size() || needle < lines[i+1].offset)) { CGFloat x = lines[i].x, y = lines[i].y; size_t offset = lines[i].offset; foreach(node, _nodes.begin() + lines[i].first, _nodes.begin() + lines[i].last) { if(offset <= needle && needle < offset + node->length()) { if(node->type() == kNodeTypeText && node->line()) return CGRectMake(anchor.x + x + node->line()->offset_for_index(needle - offset) + caretOffset, anchor.y + y, metrics.column_width(), lines[i].height); return CGRectMake(anchor.x + x + (needle - offset) * node->width() / node->length() + caretOffset, anchor.y + y, 1, lines[i].height); } x += node->width(); offset += node->length(); } return CGRectMake(anchor.x + x + caretOffset, anchor.y + y, 1, lines[i].height); } } return CGRectMake(anchor.x + caretOffset, anchor.y, 1, lines.back().height); } ng::line_record_t paragraph_t::line_record_for (size_t line, size_t pos, ct::metrics_t const& metrics, ng::buffer_t const& buffer, size_t bufferOffset, CGPoint anchor) const { size_t needle = pos - bufferOffset; auto lines = softlines(metrics); CGFloat y = anchor.y; for(size_t i = 0; i < lines.size(); ++i) { if(lines[i].offset <= needle && (i+1 == lines.size() || needle < lines[i+1].offset)) return ng::line_record_t(line, lines[i].offset, y, y + lines[i].height, lines[i].baseline); y += lines[i].height; } return ng::line_record_t(line, 0, 0, 0, 0); } size_t paragraph_t::bol (size_t index, ng::buffer_t const& buffer, size_t bufferOffset) const { size_t i = bufferOffset; size_t bol = i; iterate(node, _nodes) { if(index < i) break; i += node->length(); if(node->type() == kNodeTypeSoftBreak) bol = i; } return bol; } size_t paragraph_t::eol (size_t index, ng::buffer_t const& buffer, size_t bufferOffset) const { size_t i = bufferOffset; iterate(node, _nodes) { if(index < i && node->type() == kNodeTypeSoftBreak) return i - buffer[i-1].size(); if(index <= i && node->type() == kNodeTypeNewline) return i; i += node->length(); } return i; } size_t paragraph_t::index_left_of (size_t index, ng::buffer_t const& buffer, size_t bufferOffset) const { if(index != bufferOffset) index -= buffer[index-1].size();; size_t i = bufferOffset; iterate(node, _nodes) { if(i < index && index < i + node->length() && node->type() == kNodeTypeFolding) return i; i += node->length(); } return index; } size_t paragraph_t::index_right_of (size_t index, ng::buffer_t const& buffer, size_t bufferOffset) const { if(index != bufferOffset + length()) index += buffer[index].size(); size_t i = bufferOffset; iterate(node, _nodes) { if(i < index && index < i + node->length() && node->type() == kNodeTypeFolding) return i + node->length(); i += node->length(); } return index; } void paragraph_t::draw_background (theme_ptr const& theme, std::string const& fontName, CGFloat fontSize, ct::metrics_t const& metrics, CGContextRef context, bool isFlipped, CGRect visibleRect, bool showInvisibles, CGColorRef backgroundColor, ng::buffer_t const& buffer, size_t bufferOffset, CGPoint anchor) const { // render::fill_rect(context, cf::color_t("#FFAAAA"), CGRectInset(CGRectMake(anchor.x, anchor.y, width(), height(metrics)), -1, 0)); // render::fill_rect(context, backgroundColor, CGRectMake(anchor.x, anchor.y, width(), height(metrics))); auto lines = softlines(metrics); for(size_t i = 0; i < lines.size(); ++i) { CGFloat x = lines[i].x; size_t offset = lines[i].offset; foreach(node, _nodes.begin() + lines[i].first, _nodes.begin() + lines[i].last) { node->draw_background(theme, fontName, fontSize, context, isFlipped, visibleRect, showInvisibles, backgroundColor, buffer, bufferOffset + offset, CGPointMake(anchor.x + x, anchor.y + lines[i].y), lines[i].height); x += node->width(); offset += node->length(); } } } void paragraph_t::draw_foreground (theme_ptr const& theme, std::string const& fontName, CGFloat fontSize, ct::metrics_t const& metrics, CGContextRef context, bool isFlipped, CGRect visibleRect, bool showInvisibles, CGColorRef textColor, ng::buffer_t const& buffer, size_t bufferOffset, ng::ranges_t const& selection, CGPoint anchor) const { CGContextSetTextMatrix(context, CGAffineTransformMake(1, 0, 0, 1, 0, 0)); auto lines = softlines(metrics, false); for(size_t i = 0; i < lines.size(); ++i) { CGFloat x = lines[i].x; size_t offset = bufferOffset + lines[i].offset; foreach(node, _nodes.begin() + lines[i].first, _nodes.begin() + lines[i].last) { std::vector< std::pair > misspelled; if(node->type() == kNodeTypeText) { auto misspellings = buffer.misspellings(offset, offset + node->length()); for(auto it = misspellings.begin(); it != misspellings.end(); ) { bool flag = it->second; size_t from = it->first; size_t to = ++it != misspellings.end() ? it->first : node->length(); if(flag) { bool intersects = false; iterate(range, selection) intersects = intersects || !(to + offset < range->min().index || range->max().index < from + offset); if(!intersects) misspelled.push_back(std::make_pair(from, to)); } } } node->draw_foreground(theme, fontName, fontSize, context, isFlipped, visibleRect, showInvisibles, textColor, buffer, offset, misspelled, CGPointMake(anchor.x + x, anchor.y + lines[i].y), lines[i].baseline); x += node->width(); offset += node->length(); } } } // ======== // = to_s = // ======== std::string to_s (paragraph_t const& paragraph) { return "paragraph"; } } /* ng */