Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(118)

Side by Side Diff: ui/gfx/render_text.cc

Issue 7265011: RenderText API Outline. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Exclude Mac, fix font comparison. Created 9 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ui/gfx/render_text.h"
6
7 #include <algorithm>
8
9 #include "base/i18n/break_iterator.h"
10 #include "base/logging.h"
11 #include "base/stl_util-inl.h"
12 #include "ui/gfx/canvas.h"
13 #include "ui/gfx/canvas_skia.h"
14
15 namespace {
16
17 // Strike line width.
18 const int kStrikeWidth = 2;
19
20 // Color settings for text, backgrounds and cursor.
21 // These are tentative, and should be derived from theme, system
22 // settings and current settings.
23 // TODO(oshima): Change this to match the standard chrome
24 // before dogfooding textfield views.
25 const SkColor kSelectedTextColor = SK_ColorWHITE;
26 const SkColor kFocusedSelectionColor = SK_ColorCYAN;
27 const SkColor kUnfocusedSelectionColor = SK_ColorLTGRAY;
28 const SkColor kCursorColor = SK_ColorBLACK;
29
30 #ifndef NDEBUG
31 // Check StyleRanges invariant conditions: sorted and non-overlapping ranges.
32 void CheckStyleRanges(const gfx::StyleRanges& style_ranges, size_t length) {
33 if (length == 0) {
34 DCHECK(style_ranges.empty()) << "Style ranges exist for empty text.";
35 return;
36 }
37 for (gfx::StyleRanges::size_type i = 0; i < style_ranges.size() - 1; i++) {
38 const ui::Range& former = style_ranges[i].range;
39 const ui::Range& latter = style_ranges[i + 1].range;
40 DCHECK(!former.is_empty()) << "Empty range at " << i << ":" << former;
41 DCHECK(former.IsValid()) << "Invalid range at " << i << ":" << former;
42 DCHECK(!former.is_reversed()) << "Reversed range at " << i << ":" << former;
43 DCHECK(former.end() == latter.start()) << "Ranges gap/overlap/unsorted." <<
44 "former:" << former << ", latter:" << latter;
45 }
46 const gfx::StyleRange& end_style = *style_ranges.rbegin();
47 DCHECK(!end_style.range.is_empty()) << "Empty range at end.";
48 DCHECK(end_style.range.IsValid()) << "Invalid range at end.";
49 DCHECK(!end_style.range.is_reversed()) << "Reversed range at end.";
50 DCHECK(end_style.range.end() == length) << "Style and text length mismatch.";
51 }
52 #endif
53
54 void ApplyStyleRangeImpl(gfx::StyleRanges& style_ranges,
55 gfx::StyleRange style_range) {
56 const ui::Range& new_range = style_range.range;
57 // Follow StyleRanges invariant conditions: sorted and non-overlapping ranges.
58 gfx::StyleRanges::iterator i;
59 for (i = style_ranges.begin(); i != style_ranges.end();) {
60 if (i->range.end() < new_range.start()) {
61 i++;
62 } else if (i->range.start() == new_range.end()) {
63 break;
64 } else if (new_range.Contains(i->range)) {
65 i = style_ranges.erase(i);
66 if (i == style_ranges.end())
67 break;
68 } else if (i->range.start() < new_range.start() &&
69 i->range.end() > new_range.end()) {
70 // Split the current style into two styles.
71 gfx::StyleRange split_style = gfx::StyleRange(*i);
72 split_style.range.set_end(new_range.start());
73 i = style_ranges.insert(i, split_style) + 1;
74 i->range.set_start(new_range.end());
75 break;
76 } else if (i->range.start() < new_range.start()) {
77 i->range.set_end(new_range.start());
78 i++;
79 } else if (i->range.end() > new_range.end()) {
80 i->range.set_start(new_range.end());
81 break;
82 } else
83 NOTREACHED();
84 }
85 // Add the new range in its sorted location.
86 style_ranges.insert(i, style_range);
87 }
88
89 } // namespace
90
91 namespace gfx {
92
93 void RenderText::SetText(const string16& text) {
94 size_t old_text_length = text_.length();
95 text_ = text;
96
97 // Update the style ranges as needed.
98 if (text_.empty()) {
99 style_ranges_.clear();
100 } else if (style_ranges_.empty()) {
101 ApplyDefaultStyle();
102 } else if (text_.length() > old_text_length) {
103 style_ranges_.back().range.set_end(text_.length());
104 } else if (text_.length() < old_text_length) {
105 StyleRanges::iterator i;
106 for (i = style_ranges_.begin(); i != style_ranges_.end(); i++) {
107 if (i->range.start() >= text_.length()) {
108 i = style_ranges_.erase(i);
109 if (i == style_ranges_.end())
110 break;
111 } else if (i->range.end() > text_.length()) {
112 i->range.set_end(text_.length());
113 }
114 }
115 style_ranges_.back().range.set_end(text_.length());
116 }
117 #ifndef NDEBUG
118 CheckStyleRanges(style_ranges_, text_.length());
119 #endif
120 }
121
122 size_t RenderText::GetCursor() const {
123 return GetSelection().end();
124 }
125
126 void RenderText::SetCursor(const size_t position) {
127 SetSelection(ui::Range(position, position));
128 }
129
130 void RenderText::MoveCursorLeft(bool select, bool move_by_word) {
131 size_t position = GetCursor();
132 // Cancelling a selection moves to the edge of the selection.
133 if (!GetSelection().is_empty() && !select) {
134 // Use the selection start if it is left of the selection end.
135 if (GetCursorBounds(GetSelection().start(), false).x() <
136 GetCursorBounds(position, false).x())
137 position = GetSelection().start();
138 // If |move_by_word|, use the nearest word boundary left of the selection.
139 if (move_by_word)
140 position = GetLeftCursorPosition(position, true);
141 } else {
142 position = GetLeftCursorPosition(position, move_by_word);
143 }
144 MoveCursorTo(position, select);
145 }
146
147 void RenderText::MoveCursorRight(bool select, bool move_by_word) {
148 size_t position = GetCursor();
149 // Cancelling a selection moves to the edge of the selection.
150 if (!GetSelection().is_empty() && !select) {
151 // Use the selection start if it is right of the selection end.
152 if (GetCursorBounds(GetSelection().start(), false).x() >
153 GetCursorBounds(position, false).x())
154 position = GetSelection().start();
155 // If |move_by_word|, use the nearest word boundary right of the selection.
156 if (move_by_word)
157 position = GetRightCursorPosition(position, true);
158 } else {
159 position = GetRightCursorPosition(position, move_by_word);
160 }
161 MoveCursorTo(position, select);
162 }
163
164 void RenderText::MoveCursorToLeftEnd(bool select) {
165 MoveCursorTo(0, select);
166 }
167
168 void RenderText::MoveCursorToRightEnd(bool select) {
169 MoveCursorTo(text().length(), select);
170 }
171
172 bool RenderText::MoveCursorTo(size_t position, bool select) {
173 bool changed = GetCursor() != position || select == GetSelection().is_empty();
174 if (select)
175 SetSelection(ui::Range(GetSelection().start(), position));
176 else
177 SetSelection(ui::Range(position, position));
178 return changed;
179 }
180
181 const ui::Range& RenderText::GetSelection() const {
182 return selection_range_;
183 }
184
185 void RenderText::SetSelection(const ui::Range& range) {
186 selection_range_.set_end(std::min(range.end(), text().length()));
187 selection_range_.set_start(std::min(range.start(), text().length()));
188
189 // Update |display_offset_| to ensure the current cursor is visible.
190 gfx::Rect cursor_bounds(GetCursorBounds(GetCursor(), get_insert_mode()));
191 int display_width = display_rect_.width();
192 int string_width = GetStringWidth();
193 if (string_width < display_width) {
194 // Show all text whenever the text fits to the size.
195 display_offset_.set_x(0);
196 } else if ((display_offset_.x() + cursor_bounds.right()) > display_width) {
197 // Pan to show the cursor when it overflows to the right,
198 display_offset_.set_x(display_width - cursor_bounds.right());
199 } else if ((display_offset_.x() + cursor_bounds.x()) < 0) {
200 // Pan to show the cursor when it overflows to the left.
201 display_offset_.set_x(-cursor_bounds.x());
202 }
203 }
204
205 void RenderText::ClearSelection() {
206 SetCursor(GetCursor());
207 }
208
209 void RenderText::SelectAll() {
210 SetSelection(ui::Range(0, text().length()));
211 }
212
213 void RenderText::SelectWord() {
214 size_t selection_start = GetSelection().start();
215 size_t cursor_position = GetCursor();
216 // First we setup selection_start_ and cursor_pos_. There are so many cases
217 // because we try to emulate what select-word looks like in a gtk textfield.
218 // See associated testcase for different cases.
219 if (cursor_position > 0 && cursor_position < text().length()) {
220 if (isalnum(text()[cursor_position])) {
221 selection_start = cursor_position;
222 cursor_position++;
223 } else
224 selection_start = cursor_position - 1;
225 } else if (cursor_position == 0) {
226 selection_start = cursor_position;
227 if (text().length() > 0)
228 cursor_position++;
229 } else {
230 selection_start = cursor_position - 1;
231 }
232
233 // Now we move selection_start_ to beginning of selection. Selection boundary
234 // is defined as the position where we have alpha-num character on one side
235 // and non-alpha-num char on the other side.
236 for (; selection_start > 0; selection_start--) {
237 if (IsPositionAtWordSelectionBoundary(selection_start))
238 break;
239 }
240
241 // Now we move cursor_pos_ to end of selection. Selection boundary
242 // is defined as the position where we have alpha-num character on one side
243 // and non-alpha-num char on the other side.
244 for (; cursor_position < text().length(); cursor_position++) {
245 if (IsPositionAtWordSelectionBoundary(cursor_position))
246 break;
247 }
248
249 SetSelection(ui::Range(selection_start, cursor_position));
250 }
251
252 const ui::Range& RenderText::GetCompositionRange() const {
253 return composition_range_;
254 }
255
256 void RenderText::SetCompositionRange(const ui::Range& composition_range) {
257 CHECK(!composition_range.IsValid() ||
258 ui::Range(0, text_.length()).Contains(composition_range));
259 composition_range_.set_end(composition_range.end());
260 composition_range_.set_start(composition_range.start());
261 }
262
263 void RenderText::ApplyStyleRange(StyleRange style_range) {
264 const ui::Range& new_range = style_range.range;
265 if (!new_range.IsValid() || new_range.is_empty())
266 return;
267 CHECK(!new_range.is_reversed());
268 CHECK(ui::Range(0, text_.length()).Contains(new_range));
269 ApplyStyleRangeImpl(style_ranges_, style_range);
270 #ifndef NDEBUG
271 CheckStyleRanges(style_ranges_, text_.length());
272 #endif
273 }
274
275 void RenderText::ApplyDefaultStyle() {
276 style_ranges_.clear();
277 StyleRange style = StyleRange(default_style_);
278 style.range.set_end(text_.length());
279 style_ranges_.push_back(style);
280 }
281
282 base::i18n::TextDirection RenderText::GetTextDirection() const {
283 return base::i18n::LEFT_TO_RIGHT;
xji 2011/07/20 01:33:33 why do we need this? and why it is always LTR?
msw 2011/07/20 09:22:46 It's just a stub right now, intended to replace th
284 }
285
286 int RenderText::GetStringWidth() const {
287 return GetSubstringBounds(ui::Range(0, text_.length()))[0].width();
288 }
289
290 void RenderText::Draw(gfx::Canvas* canvas) const {
291 // Clip the canvas to the text display area.
292 canvas->ClipRectInt(display_rect_.x(), display_rect_.y(),
293 display_rect_.width(), display_rect_.height());
294
295 // Draw the selection.
296 std::vector<gfx::Rect> selection(GetSubstringBounds(GetSelection()));
297 SkColor selection_color =
298 is_focused_ ? kFocusedSelectionColor : kUnfocusedSelectionColor;
299 for (std::vector<gfx::Rect>::const_iterator i = selection.begin();
300 i < selection.end(); ++i) {
301 gfx::Rect r(*i);
302 r.Offset(display_offset_);
303 canvas->FillRectInt(selection_color, r.x(), r.y(), r.width(), r.height());
304 }
305
306 // Create a temporary copy of the style ranges for composition and selection.
307 // TODO(msw): This pattern ought to be reconsidered; what about composition
308 // and selection overlaps, retain existing local style features?
309 StyleRanges style_ranges(style_ranges_);
310 // Apply a composition style override to a copy of the style ranges.
311 if (composition_range_.IsValid() && !composition_range_.is_empty()) {
312 StyleRange composition_style(default_style_);
313 composition_style.underline = true;
314 composition_style.range.set_start(composition_range_.start());
315 composition_style.range.set_end(composition_range_.end());
316 ApplyStyleRangeImpl(style_ranges, composition_style);
317 }
318 // Apply a selection style override to a copy of the style ranges.
319 if (selection_range_.IsValid() && !selection_range_.is_empty()) {
320 StyleRange selection_style(default_style_);
321 selection_style.foreground = kSelectedTextColor;
322 selection_style.range.set_start(selection_range_.GetMin());
323 selection_style.range.set_end(selection_range_.GetMax());
324 ApplyStyleRangeImpl(style_ranges, selection_style);
325 }
326
327 // Draw the text.
328 gfx::Rect bounds(display_rect_);
329 bounds.Offset(display_offset_);
330 for (StyleRanges::const_iterator i = style_ranges.begin();
331 i < style_ranges.end(); ++i) {
332 Font font = !i->underline ? i->font :
333 i->font.DeriveFont(0, i->font.GetStyle() | Font::UNDERLINED);
334 string16 text = text_.substr(i->range.start(), i->range.length());
335 bounds.set_width(font.GetStringWidth(text));
336 canvas->DrawStringInt(text, font, i->foreground, bounds);
337
338 // Draw the strikethrough.
339 if (i->strike) {
340 SkPaint paint;
341 paint.setAntiAlias(true);
342 paint.setStyle(SkPaint::kFill_Style);
343 paint.setColor(i->foreground);
344 paint.setStrokeWidth(kStrikeWidth);
345 canvas->AsCanvasSkia()->drawLine(SkIntToScalar(bounds.x()),
346 SkIntToScalar(bounds.bottom()),
347 SkIntToScalar(bounds.right()),
348 SkIntToScalar(bounds.y()),
349 paint);
350 }
351
352 bounds.set_x(bounds.x() + bounds.width());
353 }
354
355 // Paint cursor. Replace cursor is drawn as rectangle for now.
356 if (is_cursor_visible_ && is_focused_) {
357 gfx::Rect cursor_bounds(GetCursorBounds(GetCursor(), get_insert_mode()));
358 cursor_bounds.Offset(display_offset_);
359 if (!cursor_bounds.IsEmpty())
360 canvas->DrawRectInt(kCursorColor,
361 cursor_bounds.x(),
362 cursor_bounds.y(),
363 cursor_bounds.width(),
364 cursor_bounds.height());
365 }
366 }
367
368 size_t RenderText::FindCursorPosition(const gfx::Point& point) const {
369 const gfx::Font& font = Font();
370 int left = 0;
371 int left_pos = 0;
372 int right = font.GetStringWidth(text());
373 int right_pos = text().length();
374
375 int x = point.x();
376 if (x <= left) return left_pos;
377 if (x >= right) return right_pos;
378 // binary searching the cursor position.
379 // TODO(oshima): use the center of character instead of edge.
380 // Binary search may not work for language like arabic.
381 while (std::abs(static_cast<long>(right_pos - left_pos) > 1)) {
382 int pivot_pos = left_pos + (right_pos - left_pos) / 2;
383 int pivot = font.GetStringWidth(text().substr(0, pivot_pos));
384 if (pivot < x) {
385 left = pivot;
386 left_pos = pivot_pos;
387 } else if (pivot == x) {
388 return pivot_pos;
389 } else {
390 right = pivot;
391 right_pos = pivot_pos;
392 }
393 }
394 return left_pos;
395 }
396
397 std::vector<gfx::Rect> RenderText::GetSubstringBounds(
398 const ui::Range& range) const {
399 size_t start = range.GetMin();
400 size_t end = range.GetMax();
401 gfx::Font font;
402 int start_x = font.GetStringWidth(text().substr(0, start));
403 int end_x = font.GetStringWidth(text().substr(0, end));
404 std::vector<gfx::Rect> bounds;
405 bounds.push_back(gfx::Rect(start_x, 0, end_x - start_x, font.GetHeight()));
406 return bounds;
407 }
408
409 gfx::Rect RenderText::GetCursorBounds(size_t cursor_pos,
410 bool insert_mode) const {
411 gfx::Font font;
412 int x = font.GetStringWidth(text_.substr(0U, cursor_pos));
413 DCHECK_GE(x, 0);
414 int h = std::min(display_rect_.height(), font.GetHeight());
415 gfx::Rect bounds(x, (display_rect_.height() - h) / 2, 1, h);
416 if (!insert_mode && text_.length() != cursor_pos)
417 bounds.set_width(font.GetStringWidth(text_.substr(0, cursor_pos + 1)) - x);
418 return bounds;
419 }
420
421 size_t RenderText::GetLeftCursorPosition(size_t position,
422 bool move_by_word) const {
423 if (!move_by_word)
424 return position == 0? position : position - 1;
425 // Notes: We always iterate words from the begining.
426 // This is probably fast enough for our usage, but we may
427 // want to modify WordIterator so that it can start from the
428 // middle of string and advance backwards.
429 base::i18n::BreakIterator iter(text(), base::i18n::BreakIterator::BREAK_WORD);
430 bool success = iter.Init();
431 DCHECK(success);
432 if (!success)
433 return position;
434 int last = 0;
435 while (iter.Advance()) {
436 if (iter.IsWord()) {
437 size_t begin = iter.pos() - iter.GetString().length();
438 if (begin == position) {
439 // The cursor is at the beginning of a word.
440 // Move to previous word.
441 break;
442 } else if(iter.pos() >= position) {
443 // The cursor is in the middle or at the end of a word.
444 // Move to the top of current word.
445 last = begin;
446 break;
447 } else {
448 last = iter.pos() - iter.GetString().length();
449 }
450 }
451 }
452
453 return last;
454 }
455
456 size_t RenderText::GetRightCursorPosition(size_t position,
457 bool move_by_word) const {
458 if (!move_by_word)
459 return std::min(position + 1, text().length());
460 base::i18n::BreakIterator iter(text(), base::i18n::BreakIterator::BREAK_WORD);
461 bool success = iter.Init();
462 DCHECK(success);
463 if (!success)
464 return position;
465 size_t pos = 0;
466 while (iter.Advance()) {
467 pos = iter.pos();
468 if (iter.IsWord() && pos > position) {
469 break;
470 }
471 }
472 return pos;
473 }
474
475 RenderText::RenderText()
476 : text_(),
477 selection_range_(),
478 is_cursor_visible_(false),
479 insert_mode_(true),
480 composition_range_(),
481 style_ranges_(),
482 default_style_(),
483 display_rect_(),
484 display_offset_() {
485 }
486
487 RenderText::~RenderText() {
488 }
489
490 bool RenderText::IsPositionAtWordSelectionBoundary(size_t pos) {
491 return pos == 0 || (isalnum(text()[pos - 1]) && !isalnum(text()[pos])) ||
492 (!isalnum(text()[pos - 1]) && isalnum(text()[pos]));
493 }
494
495 } // namespace gfx
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698