OLD | NEW |
---|---|
(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 | |
OLD | NEW |