Make CoreText manage the tabs

Rather than putting tabs in their own node, leave them in the text nodes and allow core text to handle the size. This fixes the issue with using tabs in right-to-left text.
This commit is contained in:
Steven Clukey
2014-05-24 00:39:14 -04:00
committed by Allan Odgaard
parent f392caf100
commit a36dd00613
5 changed files with 96 additions and 52 deletions

View File

@@ -102,7 +102,7 @@ namespace ct
// = line_t =
// ==========
line_t::line_t (std::string const& text, std::map<size_t, scope::scope_t> const& scopes, theme_ptr const& theme, CGColorRef textColor) : _text(text)
line_t::line_t (std::string const& text, std::map<size_t, scope::scope_t> const& scopes, theme_ptr const& theme, CGFloat tabSize, ct::metrics_t const& metrics, CGColorRef textColor) : _text(text)
{
ASSERT(utf8::is_valid(text.begin(), text.end()));
ASSERT(scopes.empty() || (--scopes.end())->first <= text.size());
@@ -148,6 +148,45 @@ namespace ct
fprintf(stderr, "%s: failed to create CFString for %.*s\n", getprogname(), int(j - i), text.data() + i);
}
}
CTLineRef tmpLine = CTLineCreateWithAttributedString(toDraw);
double tabWidth = tabSize * metrics.column_width();
double standardTabWidths = 0;
double newTabWidths = 0;
size_t j = 0;
std::vector<CTTextTabRef> tabs;
tabs.push_back(CTTextTabCreate(kCTNaturalTextAlignment, 0, NULL));
citerate(ch, diacritics::make_range(text.data(), text.data() + text.size()))
{
switch(*ch)
{
case '\t':
double x = CTLineGetOffsetForStringIndex(tmpLine, j, NULL);
double newX = (x - standardTabWidths + newTabWidths);
double stopLocation = (floor(newX / tabWidth)+1) * tabWidth;
if (stopLocation - newX < metrics.column_width()*0.5)
stopLocation += tabWidth;
newTabWidths += stopLocation - newX;
standardTabWidths += CTLineGetOffsetForStringIndex(tmpLine, j+1, NULL) - x;
tabs.push_back(CTTextTabCreate(kCTNaturalTextAlignment, stopLocation, NULL));
_tabLocations.push_back(j);
break;
}
++j;
}
CFArrayRef tabStops = CFArrayCreate(kCFAllocatorDefault, (const void**) (&tabs[0]), tabs.size(), &kCFTypeArrayCallBacks);
for(CTTextTabRef t : tabs)
CFRelease(t);
CTParagraphStyleSetting settings[] = {
{kCTParagraphStyleSpecifierTabStops, sizeof(CFArrayRef), &tabStops},
{kCTParagraphStyleSpecifierDefaultTabInterval, sizeof(tabWidth), &tabWidth}
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, 2);
CFAttributedStringSetAttribute(toDraw, CFRangeMake(0, CFAttributedStringGetLength(toDraw)), kCTParagraphStyleAttributeName, paragraphStyle);
CFRelease(paragraphStyle);
CFRelease(tabStops);
_line.reset(CTLineCreateWithAttributedString(toDraw), CFRelease);
CFRelease(toDraw);
}
@@ -181,11 +220,42 @@ namespace ct
}
}
void line_t::draw_foreground (CGPoint pos, ng::context_t const& context, bool isFlipped, std::vector< std::pair<size_t, size_t> > const& misspelled) const
void line_t::draw_invisible (std::vector<size_t> locations, CGPoint pos, std::string text, styles_t styles, ng::context_t const& context, bool isFlipped) const
{
CFMutableAttributedStringRef str = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFAttributedStringReplaceString(str, CFRangeMake(0, 0), cf::wrap(text));
CFAttributedStringSetAttribute(str, CFRangeMake(0, CFAttributedStringGetLength(str)), kCTFontAttributeName, styles.font());
CFAttributedStringSetAttribute(str, CFRangeMake(0, CFAttributedStringGetLength(str)), kCTForegroundColorAttributeName, styles.foreground());
CTLineRef line = CTLineCreateWithAttributedString(str);
CFRelease(str);
CGContextSaveGState(context);
if(isFlipped)
CGContextConcatCTM(context, CGAffineTransformMake(1, 0, 0, -1, 0, 2 * pos.y));
for(auto const& location : locations)
{
if (location > 5000) break;
CGFloat x1 = round(pos.x + offset_for_index(location));
CGFloat x2 = round(pos.x + offset_for_index(location+1));
CGFloat x = x2 < x1 ? x1 - CTLineGetTypographicBounds(line, NULL, NULL, NULL) : x1;
CGContextSetTextPosition(context, x, pos.y);
CTLineDraw(line, context);
}
CGContextRestoreGState(context);
CFRelease(line);
}
void line_t::draw_foreground (CGPoint pos, ng::context_t const& context, bool isFlipped, std::vector< std::pair<size_t, size_t> > const& misspelled, ng::invisibles_t const& invisibles, theme_ptr const& theme) const
{
if(!_line)
return;
if(invisibles.enabled)
{
if(invisibles.tab != "")
draw_invisible(_tabLocations, pos, invisibles.tab, theme->styles_for_scope("deco.invisible.tab"), context, isFlipped);
}
for(auto const& pair : _underlines) // Draw our own underline since CoreText does an awful job <rdar://5845224>
{
CGFloat x1 = round(pos.x + CTLineGetOffsetForStringIndex(_line.get(), pair.first.location, NULL));

View File

@@ -62,17 +62,20 @@ namespace ct
struct line_t
{
line_t (std::string const& text, std::map<size_t, scope::scope_t> const& scopes, theme_ptr const& theme, CGColorRef textColor = NULL);
line_t (std::string const& text, std::map<size_t, scope::scope_t> const& scopes, theme_ptr const& theme, CGFloat tabSize, ct::metrics_t const& metrics, CGColorRef textColor = NULL);
void draw_foreground (CGPoint pos, ng::context_t const& context, bool isFlipped, std::vector< std::pair<size_t, size_t> > const& misspelled) const;
void draw_foreground (CGPoint pos, ng::context_t const& context, bool isFlipped, std::vector< std::pair<size_t, size_t> > const& misspelled, ng::invisibles_t const& invisibles, theme_ptr const& theme) const;
void draw_background (CGPoint pos, CGFloat height, ng::context_t const& context, bool isFlipped, CGColorRef currentBackground) const;
CGFloat width (CGFloat* ascent = NULL, CGFloat* descent = NULL, CGFloat* leading = NULL) const;
size_t index_for_offset (CGFloat offset) const;
CGFloat offset_for_index (size_t index) const;
std::vector<size_t> _tabLocations;
private:
void draw_invisible (std::vector<size_t> locations, CGPoint pos, std::string text, styles_t styles, ng::context_t const& context, bool isFlipped) const;
typedef std::shared_ptr<struct __CTLine const> CTLinePtr;
typedef std::shared_ptr<struct CGColor> CGColorPtr;

View File

@@ -143,10 +143,7 @@ namespace ng
return;
_tab_size = tabSize;
iterate(row, _rows)
{
row->value.set_tab_size(tabSize, *_metrics);
update_row(row);
}
row->value.set_tab_size(*_metrics);
_dirty_rects.push_back(OakRectMake(0, 0, width(), height()));
}

View File

@@ -98,7 +98,7 @@ namespace ng
_line.reset();
}
void paragraph_t::node_t::layout (CGFloat x, CGFloat tabWidth, theme_ptr const& theme, bool softWrap, size_t wrapColumn, ct::metrics_t const& metrics, ng::buffer_t const& buffer, size_t bufferOffset, std::string const& fillStr)
void paragraph_t::node_t::layout (CGFloat x, CGFloat tabSize, theme_ptr const& theme, 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;
@@ -107,7 +107,7 @@ namespace ng
{
case kNodeTypeText:
{
_line = std::make_shared<ct::line_t>(buffer.substr(bufferOffset, bufferOffset + _length), buffer.scopes(bufferOffset, bufferOffset + _length), theme, nullptr);
_line = std::make_shared<ct::line_t>(buffer.substr(bufferOffset, bufferOffset + _length), buffer.scopes(bufferOffset, bufferOffset + _length), theme, tabSize, metrics, nullptr);
}
break;
@@ -115,13 +115,7 @@ namespace ng
{
scope::scope_t scope = buffer.scope(bufferOffset).right;
scope.push_scope("deco.unprintable");
_line = std::make_shared<ct::line_t>(representation_for(utf8::to_ch(buffer.substr(bufferOffset, bufferOffset + _length))), std::map<size_t, scope::scope_t>{ { 0, scope } }, theme, nullptr);
}
break;
case kNodeTypeTab:
{
update_tab_width(x, tabWidth, metrics);
_line = std::make_shared<ct::line_t>(representation_for(utf8::to_ch(buffer.substr(bufferOffset, bufferOffset + _length))), std::map<size_t, scope::scope_t>{ { 0, scope } }, theme, tabSize, metrics, nullptr);
}
break;
@@ -136,7 +130,7 @@ namespace ng
scope::context_t const context = buffer.scope(bufferOffset);
scope::scope_t scope = shared_prefix(context.left, context.right);
scope.push_scope("deco.indented-wrap");
_line = std::make_shared<ct::line_t>(fillStr, std::map<size_t, scope::scope_t>{ { 0, scope } }, theme, nullptr);
_line = std::make_shared<ct::line_t>(fillStr, std::map<size_t, scope::scope_t>{ { 0, scope } }, theme, tabSize, metrics, nullptr);
}
break;
}
@@ -152,12 +146,9 @@ namespace ng
return _line ? _line->width() : _width;
}
void paragraph_t::node_t::update_tab_width (CGFloat x, CGFloat tabWidth, ct::metrics_t const& metrics)
void paragraph_t::node_t::update_tab_width ()
{
double r = remainder(x, tabWidth);
_width = (r < 0 ? 0 : tabWidth) - r;
if(_width < 0.5 * metrics.column_width())
_width += tabWidth;
_line.reset();
}
void paragraph_t::node_t::draw_background (theme_ptr const& theme, ng::context_t const& context, bool isFlipped, CGRect visibleRect, ng::invisibles_t const& invisibles, CGColorRef backgroundColor, ng::buffer_t const& buffer, size_t bufferOffset, CGPoint anchor, CGFloat lineHeight) const
@@ -188,18 +179,14 @@ namespace ng
void paragraph_t::node_t::draw_foreground (theme_ptr const& theme, ng::context_t const& context, bool isFlipped, CGRect visibleRect, ng::invisibles_t const& invisibles, ng::buffer_t const& buffer, size_t bufferOffset, std::vector< std::pair<size_t, size_t> > const& misspelled, CGPoint anchor, CGFloat baseline) const
{
if(_line)
_line->draw_foreground(CGPointMake(anchor.x, anchor.y + baseline), context, isFlipped, misspelled);
_line->draw_foreground(CGPointMake(anchor.x, anchor.y + baseline), context, isFlipped, misspelled, invisibles, theme);
if(invisibles.enabled || (_type != kNodeTypeTab && _type != kNodeTypeNewline))
if(invisibles.enabled || _type != kNodeTypeNewline)
{
std::string str = NULL_STR;
scope::scope_t scope = buffer.scope(bufferOffset).right;
switch(_type)
{
case kNodeTypeTab:
str = invisibles.tab;
scope.push_scope("deco.invisible.tab");
break;
case kNodeTypeNewline:
str = invisibles.newline;
scope.push_scope("deco.invisible.newline");
@@ -248,14 +235,12 @@ namespace ng
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(*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')
if(*ch == '\n')
insert_newline(pos - bufferOffset + i, ch.length());
else
insert_unprintable(pos - bufferOffset + i, ch.length());
@@ -381,7 +366,7 @@ namespace ng
size_t i = bufferOffset;
for(auto& node : _nodes)
{
node.layout(x, tabSize * metrics.column_width(), theme, softWrap, wrapColumn, metrics, buffer, i, fillStr);
node.layout(x, tabSize, theme, softWrap, wrapColumn, metrics, buffer, i, fillStr);
x += node.width();
i += node.length();
}
@@ -484,11 +469,6 @@ namespace ng
_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));
@@ -504,20 +484,15 @@ namespace ng
_dirty = true;
}
void paragraph_t::set_tab_size (size_t tabSize, ct::metrics_t const& metrics)
void paragraph_t::set_tab_size (ct::metrics_t const& metrics)
{
double const tabWidth = tabSize * metrics.column_width();
_dirty = true;
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();
}
node->update_tab_width();
}
}

View File

@@ -52,7 +52,7 @@ namespace ng
ng::range_t range_for_softline (size_t softline, ng::buffer_t const& buffer, size_t bufferOffset, size_t softlineOffset, ct::metrics_t const& metrics, bool softBreaksOnNewline = false) const;
void set_wrapping (bool softWrap, size_t wrapColumn, ct::metrics_t const& metrics);
void set_tab_size (size_t tabSize, ct::metrics_t const& metrics);
void set_tab_size (ct::metrics_t const& metrics);
void reset_font_metrics (ct::metrics_t const& metrics);
CGFloat width () const;
@@ -61,7 +61,7 @@ namespace ng
bool structural_integrity () const { return true; }
private:
enum node_type_t { kNodeTypeText, kNodeTypeTab, kNodeTypeUnprintable, kNodeTypeFolding, kNodeTypeSoftBreak, kNodeTypeNewline };
enum node_type_t { kNodeTypeText, kNodeTypeUnprintable, kNodeTypeFolding, kNodeTypeSoftBreak, kNodeTypeNewline };
struct node_t
{
@@ -80,7 +80,7 @@ namespace ng
size_t length () const { return _length; }
std::shared_ptr<ct::line_t> line () const { return _line; }
CGFloat width () const;
void update_tab_width (CGFloat x, CGFloat tabWidth, ct::metrics_t const& metrics);
void update_tab_width ();
private:
node_type_t _type;
@@ -93,7 +93,6 @@ namespace ng
std::vector<node_t>::iterator iterator_at (size_t i);
void insert_text (size_t i, size_t len);
void insert_tab (size_t i);
void insert_unprintable (size_t i, size_t len);
void insert_newline (size_t i, size_t len);