]> git.tdb.fi Git - libs/gltk.git/blob - source/text.cpp
bffb6e6a86cc0b33f6b35285d6a22338cfb7c8ae
[libs/gltk.git] / source / text.cpp
1 #include <msp/core/algorithm.h>
2 #include <msp/gl/matrix.h>
3 #include <msp/gl/meshbuilder.h>
4 #include <msp/gl/texture2d.h>
5 #include "partcache.h"
6 #include "style.h"
7 #include "text.h"
8
9 using namespace std;
10
11 namespace Msp {
12 namespace GLtk {
13
14 struct Text::RenderData
15 {
16         GL::PrimitiveBuilder *bld;
17 };
18
19 struct Text::CoordsToGeomData
20 {
21         unsigned row;
22         unsigned col;
23         Geometry result;
24 };
25
26
27 Text::Text():
28         style(0)
29 {
30         Line line;
31         line.start = 0;
32         line.bytes = 0;
33         line.length = 0;
34         line.width = 0;
35         lines.push_back(line);
36 }
37
38 Text::Text(const Style &s, const string &t):
39         style(&s)
40 {
41         set(t);
42 }
43
44 void Text::set_style(const Style *s)
45 {
46         style = s;
47
48         if(style)
49         {
50                 const GL::Font &font = style->get_font();
51                 float font_size = style->get_font_size();
52                 for(Line &l: lines)
53                         l.width = static_cast<unsigned>(font.get_string_width(text.substr(l.start, l.bytes))*font_size);
54         }
55 }
56
57 unsigned Text::get_width() const
58 {
59         unsigned width = 0;
60         for(const Line &l: lines)
61                 width = max(width, l.width);
62         return width;
63 }
64
65 unsigned Text::get_height() const
66 {
67         if(!style)
68                 return lines.size();
69
70         const GL::Font &font = style->get_font();
71         float font_size = style->get_font_size();
72         unsigned line_height = static_cast<unsigned>((font.get_ascent()-font.get_descent())*font_size);
73         unsigned line_spacing = line_height*6/5;
74         return line_height+(lines.size()-1)*line_spacing;
75 }
76
77 void Text::autosize(const Part &part, Geometry &geom) const
78 {
79         const Sides &margin = part.get_margin();
80         geom.w = max(geom.w, get_width()+margin.left+margin.right);
81         geom.h = max(geom.h, get_height()+margin.top+margin.bottom);
82 }
83
84 void Text::set(const string &t)
85 {
86         text = t;
87         find_lines();
88 }
89
90 void Text::erase(unsigned pos, unsigned len)
91 {
92         check_alignment(pos);
93         check_alignment(pos+len);
94         text.erase(pos, len);
95
96         auto i = find_if(lines, [pos](const Line &l){ return l.start+l.bytes>=pos; });
97
98         if(pos+len>i->start+i->bytes)
99                 find_lines();
100         else
101         {
102                 i->bytes -= len;
103                 i->length = count_characters(i->start, i->bytes);
104
105                 for(++i; i!=lines.end(); ++i)
106                         i->start -= len;
107         }
108 }
109
110 void Text::insert(unsigned pos, const string &s)
111 {
112         check_alignment(pos);
113         text.insert(pos, s);
114
115         if(s.find('\n')!=string::npos)
116                 find_lines();
117         else
118         {
119                 auto i = find_if(lines, [pos](const Line &l){ return l.start+l.bytes>=pos; });
120
121                 i->bytes += s.size();
122                 i->length = count_characters(i->start, i->bytes);
123
124                 for(++i; i!=lines.end(); ++i)
125                         i->start += s.size();
126         }
127 }
128
129 unsigned Text::get_visible_lines(const Part &part, const Geometry &parent, unsigned *fit_height) const
130 {
131         const GL::Font &font = style->get_font();
132         float font_size = style->get_font_size();
133         unsigned line_height = static_cast<unsigned>((font.get_ascent()-font.get_descent())*font_size);
134         unsigned line_spacing = static_cast<unsigned>(font_size*6/5);
135
136         const Sides &margin = part.get_margin();
137         unsigned vmargin = margin.top+margin.bottom;
138         unsigned free_height = max(parent.h, vmargin)-vmargin+line_spacing-line_height;
139         unsigned n_lines = min<unsigned>(lines.size(), max(free_height/line_spacing, 1U));
140         if(fit_height)
141                 *fit_height = line_height+(n_lines-1)*line_spacing;
142
143         return n_lines;
144 }
145
146 unsigned Text::get_line_length(unsigned i) const
147 {
148         if(i>=lines.size())
149                 throw out_of_range("Text::get_line_length");
150         return lines[i].length;
151 }
152
153 unsigned Text::move_offset(unsigned offs, int change) const
154 {
155         check_alignment(offs);
156         if(!change)
157                 return offs;
158
159         StringCodec::Utf8::Decoder dec(StringCodec::IGNORE_ERRORS);
160         auto i = text.begin()+offs;
161         if(change>0)
162         {
163                 for(; change>0; --change)
164                         dec.decode_char(text, i);
165         }
166         else
167         {
168                 while(change<0 && i!=text.begin())
169                 {
170                         --i;
171                         auto j = i;
172                         if(dec.decode_char(text, j)!=-1)
173                                 ++change;
174                 }
175         }
176         return i-text.begin();
177 }
178
179 void Text::offset_to_coords(unsigned offs, unsigned &row, unsigned &col) const
180 {
181         if(lines.empty())
182         {
183                 row = 0;
184                 col = 0;
185                 return;
186         }
187
188         for(unsigned i=0; i<lines.size(); ++i)
189                 if(offs>=lines[i].start && offs<=lines[i].start+lines[i].bytes)
190                 {
191                         row = i;
192                         if(lines[i].length==lines[i].bytes)
193                                 col = offs-lines[i].start;
194                         else
195                                 col = count_characters(lines[i].start, offs-lines[i].start);
196                         return;
197                 }
198 }
199
200 unsigned Text::coords_to_offset(unsigned row, unsigned col) const
201 {
202         if(row>=lines.size())
203                 return text.size();
204         const Line &line = lines[row];
205         if(col>line.length)
206                 col = line.length;
207
208         if(line.length==line.bytes)
209                 return line.start+col;
210         else
211         {
212                 StringCodec::Utf8::Decoder dec;
213                 auto i = text.begin()+line.start;
214                 for(col=min(col, line.length); col; --col)
215                         dec.decode_char(text, i);
216                 return i-text.begin();
217         }
218 }
219
220 Geometry Text::coords_to_geometry(const Part &part, const Geometry &parent, unsigned first_row, unsigned row, unsigned col) const
221 {
222         if(row>=lines.size())
223                 row = lines.size()-1;
224         const Line &line = lines[row];
225         if(col>line.length)
226                 col = line.length;
227
228         CoordsToGeomData data;
229         data.row = row;
230         data.col = col;
231
232         process_lines(part, parent, first_row, &Text::coords_to_geom_line, data);
233
234         return data.result;
235 }
236
237 void Text::build(const Part &part, State state, const Geometry &parent, PartCache &cache) const
238 {
239         build(part, state, parent, 0, cache);
240 }
241
242 void Text::build(const Part &part, State state, const Geometry &parent, unsigned first_row, PartCache &cache) const
243 {
244         if(!style || lines.empty())
245                 return;
246
247         const GL::Font &font = style->get_font();
248         GL::MeshBuilder bld(cache.create_mesh(part, font.get_texture()));
249         bld.color(style->get_font_color(state));
250
251         RenderData data;
252         data.bld = &bld;
253
254         process_lines(part, parent, first_row, &Text::build_line, data);
255 }
256
257 Text &Text::operator=(const string &t)
258 {
259         set(t);
260         return *this;
261 }
262
263 void Text::find_lines()
264 {
265         lines.clear();
266         float font_size = (style ? style->get_font_size() : 1);
267         string::size_type start = 0;
268         while(1)
269         {
270                 string::size_type newline = text.find('\n', start);
271
272                 Line line;
273                 line.start = start;
274                 line.bytes = (newline==string::npos ? text.size() : newline)-start;
275                 line.length = count_characters(line.start, line.bytes);
276                 line.width = line.length;
277                 if(style)
278                 {
279                         string str = text.substr(line.start, line.bytes);
280                         line.width = static_cast<unsigned>(style->get_font().get_string_width(str)*font_size);
281                 }
282                 lines.push_back(line);
283
284                 if(newline==string::npos)
285                         break;
286                 start = newline+1;
287         }
288 }
289
290 unsigned Text::count_characters(unsigned start, unsigned bytes) const
291 {
292         StringCodec::Utf8::Decoder dec;
293         auto i = text.begin()+start;
294         auto end = i+bytes;
295         unsigned count = 0;
296         for(; i<end; dec.decode_char(text, i))
297                 ++count;
298         return count;
299 }
300
301 void Text::check_alignment(unsigned offs) const
302 {
303         StringCodec::Utf8::Decoder dec;
304         auto i = text.begin()+offs;
305         dec.decode_char(text, i);
306 }
307
308 template<typename T>
309 void Text::process_lines(const Part &part, const Geometry &parent, unsigned first_row, void (Text::*func)(unsigned, const Geometry &, T &) const, T &data) const
310 {
311         if(!style)
312                 return;
313
314         const GL::Font &font = style->get_font();
315         float font_size = style->get_font_size();
316         unsigned line_spacing = static_cast<unsigned>(font_size*6/5);
317         int y_offset = static_cast<int>(-font.get_descent()*font_size);
318
319         unsigned fit_height;
320         unsigned n_lines = get_visible_lines(part, parent, &fit_height);
321         first_row = min<unsigned>(first_row, lines.size()-n_lines);
322
323         for(unsigned i=0; i<n_lines; ++i)
324         {
325                 const Line &line = lines[first_row+i];
326
327                 Geometry rgeom;
328                 rgeom.w = line.width;
329                 rgeom.h = fit_height;
330                 rgeom.y = (n_lines-1-i)*line_spacing+y_offset;
331                 part.get_alignment().apply(rgeom, parent, part.get_margin());
332
333                 (this->*func)(first_row+i, rgeom, data);
334         }
335 }
336
337 void Text::build_line(unsigned i, const Geometry &rgeom, RenderData &data) const
338 {
339         const Line &line = lines[i];
340
341         GL::VertexBuilder::PushMatrix _pushm(*data.bld);
342         data.bld->transform(GL::Matrix::translation(rgeom.x, rgeom.y, 0));
343         data.bld->transform(GL::Matrix::scaling(style->get_font_size()));
344
345         style->get_font().build_string(text.substr(line.start, line.bytes), *data.bld);
346 }
347
348 void Text::coords_to_geom_line(unsigned i, const Geometry &rgeom, CoordsToGeomData &data) const
349 {
350         if(i==data.row)
351         {
352                 auto begin = text.begin()+lines[i].start;
353                 auto j = begin;
354                 if(lines[i].length==lines[i].bytes)
355                         j += data.col;
356                 else
357                 {
358                         StringCodec::Utf8::Decoder dec;
359                         for(unsigned c=data.col; c; --c)
360                                 dec.decode_char(text, j);
361                 }
362                 float w = style->get_font().get_string_width(string(begin, j));
363                 data.result = rgeom;
364                 data.result.x += static_cast<unsigned>(w*style->get_font_size());
365         }
366 }
367
368 } // namespace GLtk
369 } // namespace Msp