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

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

Powered by Google App Engine
This is Rietveld 408576698