1 /**
2  * \file MathRow.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Jean-Marc Lasgouttes
7  *
8  * Full author contact details are available in file CREDITS.
9  */
10 
11 #include <config.h>
12 
13 #include "MathRow.h"
14 
15 #include "InsetMath.h"
16 #include "MathClass.h"
17 #include "MathData.h"
18 #include "MathSupport.h"
19 
20 #include "BufferView.h"
21 #include "ColorSet.h"
22 #include "CoordCache.h"
23 #include "MetricsInfo.h"
24 
25 #include "frontends/FontMetrics.h"
26 #include "frontends/Painter.h"
27 
28 #include "support/debug.h"
29 #include "support/docstring.h"
30 #include "support/lassert.h"
31 
32 #include <algorithm>
33 #include <ostream>
34 
35 using namespace std;
36 
37 namespace lyx {
38 
39 
Element(MetricsInfo const & mi,Type t,MathClass mc)40 MathRow::Element::Element(MetricsInfo const & mi, Type t, MathClass mc)
41 	: type(t), mclass(mc), before(0), after(0), macro_nesting(mi.base.macro_nesting),
42 	  marker(InsetMath::NO_MARKER), inset(0), compl_unique_to(0), ar(0),
43 	  color(Color_red)
44 {}
45 
46 
47 namespace {
48 
49 // Helper functions for markers
50 
markerMargin(MathRow::Element const & e)51 int markerMargin(MathRow::Element const & e)
52 {
53 	switch(e.marker) {
54 	case InsetMath::MARKER:
55 	case InsetMath::MARKER2:
56 	case InsetMath::BOX_MARKER:
57 		return 2;
58 	case InsetMath::NO_MARKER:
59 		return 0;
60 	}
61 	// should not happen
62 	return 0;
63 }
64 
65 
afterMetricsMarkers(MetricsInfo const &,MathRow::Element & e,Dimension & dim)66 void afterMetricsMarkers(MetricsInfo const & , MathRow::Element & e,
67                             Dimension & dim)
68 {
69 	// handle vertical space for markers
70 	switch(e.marker) {
71 	case InsetMath::NO_MARKER:
72 		break;
73 	case InsetMath::MARKER:
74 		++dim.des;
75 		break;
76 	case InsetMath::MARKER2:
77 		++dim.asc;
78 		++dim.des;
79 		break;
80 	case InsetMath::BOX_MARKER:
81 		FontInfo font;
82 		font.setSize(FONT_SIZE_TINY);
83 		Dimension namedim;
84 		mathed_string_dim(font, e.inset->name(), namedim);
85 		int const namewid = 1 + namedim.wid + 1;
86 
87 		if (namewid > dim.wid)
88 			e.after += namewid - dim.wid;
89 		++dim.asc;
90 		dim.des += 3 + namedim.height();
91 	}
92 }
93 
94 
drawMarkers(PainterInfo const & pi,MathRow::Element const & e,int const x,int const y)95 void drawMarkers(PainterInfo const & pi, MathRow::Element const & e,
96                  int const x, int const y)
97 {
98 	if (e.marker == InsetMath::NO_MARKER)
99 		return;
100 
101 	CoordCache const & coords = pi.base.bv->coordCache();
102 	Dimension const dim = coords.getInsets().dim(e.inset);
103 
104 	// the marker is before/after the inset. Necessary space has been reserved already.
105 	int const l = x + e.before - (markerMargin(e) > 0 ? 1 : 0);
106 	int const r = x + dim.width() - e.after;
107 
108 	// Grey lower box
109 	if (e.marker == InsetMath::BOX_MARKER) {
110 		// draw header and rectangle around
111 		FontInfo font;
112 		font.setSize(FONT_SIZE_TINY);
113 		font.setColor(Color_mathmacrolabel);
114 		Dimension namedim;
115 		mathed_string_dim(font, e.inset->name(), namedim);
116 		pi.pain.fillRectangle(l, y + dim.des - namedim.height() - 2,
117 		                      dim.wid, namedim.height() + 2, Color_mathmacrobg);
118 		pi.pain.text(l, y + dim.des - namedim.des - 1, e.inset->name(), font);
119 	}
120 
121 	// Color for corners
122 	bool const highlight = e.inset->mouseHovered(pi.base.bv)
123 	                       || e.inset->editing(pi.base.bv);
124 	ColorCode const pen_color = highlight ? Color_mathframe : Color_mathcorners;
125 	// If the corners have the same color as the background, do not paint them.
126 	if (lcolor.getX11Name(Color_mathbg) == lcolor.getX11Name(pen_color))
127 		return;
128 
129 	// Lower corners in all cases
130 	int const d = y + dim.descent();
131 	pi.pain.line(l, d - 3, l, d, pen_color);
132 	pi.pain.line(r, d - 3, r, d, pen_color);
133 	pi.pain.line(l, d, l + 3, d, pen_color);
134 	pi.pain.line(r - 3, d, r, d, pen_color);
135 
136 	// Upper corners
137 	if (e.marker == InsetMath::BOX_MARKER
138 	    || e.marker == InsetMath::MARKER2) {
139 		int const a = y - dim.ascent();
140 		pi.pain.line(l, a + 3, l, a, pen_color);
141 		pi.pain.line(r, a + 3, r, a, pen_color);
142 		pi.pain.line(l, a, l + 3, a, pen_color);
143 		pi.pain.line(r - 3, a, r, a, pen_color);
144 	}
145 }
146 
147 } // namespace
148 
149 
MathRow(MetricsInfo & mi,MathData const * ar)150 MathRow::MathRow(MetricsInfo & mi, MathData const * ar)
151 {
152 	// First there is a dummy element of type "open"
153 	push_back(Element(mi, DUMMY, MC_OPEN));
154 
155 	// Then insert the MathData argument
156 	bool const has_contents = ar->addToMathRow(*this, mi);
157 
158 	// A MathRow should not be completely empty
159 	if (!has_contents) {
160 		Element e(mi, BOX, MC_ORD);
161 		// empty arrays are visible when they are editable
162 		e.color = mi.base.macro_nesting == 0 ? Color_mathline : Color_none;
163 		push_back(e);
164 	}
165 
166 	// Finally there is a dummy element of type "close"
167 	push_back(Element(mi, DUMMY, MC_CLOSE));
168 
169 	/* Do spacing only in math mode. This test is a bit clumsy,
170 	 * but it is used in other places for guessing the current mode.
171 	 */
172 	bool const dospacing = isMathFont(mi.base.fontname);
173 
174 	// update classes
175 	if (dospacing) {
176 		for (int i = 1 ; i != static_cast<int>(elements_.size()) - 1 ; ++i) {
177 			if (elements_[i].mclass != MC_UNKNOWN)
178 				update_class(elements_[i].mclass, elements_[before(i)].mclass,
179 							 elements_[after(i)].mclass);
180 		}
181 	}
182 
183 	// set spacing
184 	// We go to the end to handle spacing at the end of equation
185 	for (int i = 1 ; i != static_cast<int>(elements_.size()) ; ++i) {
186 		Element & e = elements_[i];
187 
188 		Element & bef = elements_[before(i)];
189 		if (dospacing && e.mclass != MC_UNKNOWN) {
190 			int spc = class_spacing(bef.mclass, e.mclass, mi.base);
191 			bef.after += spc / 2;
192 			// this is better than spc / 2 to avoid rounding problems
193 			e.before += spc - spc / 2;
194 		}
195 
196 		// finally reserve space for markers
197 		bef.after = max(bef.after, markerMargin(bef));
198 		if (e.mclass != MC_UNKNOWN)
199 			e.before = max(e.before, markerMargin(e));
200 		// for linearized insets (macros...) too
201 		if (e.type == BEGIN)
202 			bef.after = max(bef.after, markerMargin(e));
203 		if (e.type == END && e.marker != InsetMath::NO_MARKER) {
204 			Element & aft = elements_[after(i)];
205 			aft.before = max(aft.before, markerMargin(e));
206 		}
207 	}
208 
209 	// Do not lose spacing allocated to extremities
210 	if (!elements_.empty()) {
211 		elements_[after(0)].before += elements_.front().after;
212 		elements_[before(elements_.size() - 1)].after += elements_.back().before;
213 	}
214 }
215 
216 
before(int i) const217 int MathRow::before(int i) const
218 {
219 	do
220 		--i;
221 	while (elements_[i].mclass == MC_UNKNOWN);
222 
223 	return i;
224 }
225 
226 
after(int i) const227 int MathRow::after(int i) const
228 {
229 	do
230 		++i;
231 	while (elements_[i].mclass == MC_UNKNOWN);
232 
233 	return i;
234 }
235 
236 
metrics(MetricsInfo & mi,Dimension & dim)237 bool MathRow::metrics(MetricsInfo & mi, Dimension & dim)
238 {
239 	bool has_caret = false;
240 
241 	dim.wid = 0;
242 	// In order to compute the dimension of macros and their
243 	// arguments, it is necessary to keep track of them.
244 	vector<pair<InsetMath const *, Dimension>> dim_insets;
245 	vector<pair<MathData const *, Dimension>> dim_arrays;
246 	CoordCache & coords = mi.base.bv->coordCache();
247 	for (Element & e : elements_) {
248 		mi.base.macro_nesting = e.macro_nesting;
249 		Dimension d;
250 		switch (e.type) {
251 		case DUMMY:
252 			break;
253 		case INSET:
254 			e.inset->metrics(mi, d);
255 			d.wid += e.before + e.after;
256 			coords.insets().add(e.inset, d);
257 			break;
258 		case BEGIN:
259 			if (e.inset) {
260 				dim_insets.push_back(make_pair(e.inset, Dimension()));
261 				dim_insets.back().second.wid += e.before + e.after;
262 				d.wid = e.before + e.after;
263 				e.inset->beforeMetrics();
264 			}
265 			if (e.ar) {
266 				dim_arrays.push_back(make_pair(e.ar, Dimension()));
267 				has_caret |= e.ar->hasCaret(mi.base.bv);
268 			}
269 			break;
270 		case END:
271 			if (e.inset) {
272 				e.inset->afterMetrics();
273 				LATTEST(dim_insets.back().first == e.inset);
274 				d = dim_insets.back().second;
275 				afterMetricsMarkers(mi, e, d);
276 				d.wid += e.before + e.after;
277 				coords.insets().add(e.inset, d);
278 				dim_insets.pop_back();
279 				// We do not want to count the width again, but the
280 				// padding and the vertical dimension are meaningful.
281 				d.wid = e.before + e.after;
282 			}
283 			if (e.ar) {
284 				LATTEST(dim_arrays.back().first == e.ar);
285 				coords.arrays().add(e.ar, dim_arrays.back().second);
286 				dim_arrays.pop_back();
287 			}
288 			break;
289 		case BOX:
290 			d = theFontMetrics(mi.base.font).dimension('I');
291 			if (e.color != Color_none) {
292 				// allow for one pixel before/after the box.
293 				d.wid += e.before + e.after + 2;
294 			} else {
295 				// hide the box, but keep its height
296 				d.wid = 0;
297 			}
298 			break;
299 		}
300 
301 		if (!d.empty()) {
302 			dim += d;
303 			// Now add the dimension to current macros and arguments.
304 			for (auto & dim_macro : dim_insets)
305 				dim_macro.second += d;
306 			for (auto & dim_array : dim_arrays)
307 				dim_array.second += d;
308 		}
309 
310 		if (e.compl_text.empty())
311 			continue;
312 		FontInfo font = mi.base.font;
313 		augmentFont(font, "mathnormal");
314 		dim.wid += mathed_string_width(font, e.compl_text);
315 	}
316 	LATTEST(dim_insets.empty() && dim_arrays.empty());
317 	return has_caret;
318 }
319 
320 
draw(PainterInfo & pi,int x,int const y) const321 void MathRow::draw(PainterInfo & pi, int x, int const y) const
322 {
323 	CoordCache & coords = pi.base.bv->coordCache();
324 	for (Element const & e : elements_) {
325 		switch (e.type) {
326 		case INSET: {
327 			// This is hackish: the math inset does not know that space
328 			// has been added before and after it; we alter its dimension
329 			// while it is drawing, because it relies on this value.
330 			Dimension const d = coords.insets().dim(e.inset);
331 			Dimension d2 = d;
332 			d2.wid -= e.before + e.after;
333 			coords.insets().add(e.inset, d2);
334 			e.inset->draw(pi, x + e.before, y);
335 			coords.insets().add(e.inset, x, y);
336 			coords.insets().add(e.inset, d);
337 			drawMarkers(pi, e, x, y);
338 			x += d.wid;
339 			break;
340 		}
341 		case BEGIN:
342 			if (e.ar) {
343 				coords.arrays().add(e.ar, x, y);
344 				e.ar->drawSelection(pi, x, y);
345 			}
346 			if (e.inset) {
347 				coords.insets().add(e.inset, x, y);
348 				drawMarkers(pi, e, x, y);
349 				e.inset->beforeDraw(pi);
350 			}
351 			x += e.before + e.after;
352 			break;
353 		case END:
354 			if (e.inset)
355 				e.inset->afterDraw(pi);
356 			x += e.before + e.after;
357 			break;
358 		case BOX: {
359 			if (e.color == Color_none)
360 				break;
361 			Dimension const d = theFontMetrics(pi.base.font).dimension('I');
362 			pi.pain.rectangle(x + e.before + 1, y - d.ascent(),
363 			                  d.width() - 1, d.height() - 1, e.color);
364 			x += d.wid + 2 + e.before + e.after;
365 			break;
366 		}
367 		case DUMMY:
368 			break;
369 		}
370 
371 		if (e.compl_text.empty())
372 			continue;
373 		FontInfo f = pi.base.font;
374 		augmentFont(f, "mathnormal");
375 
376 		// draw the unique and the non-unique completion part
377 		// Note: this is not time-critical as it is
378 		// only done once per screen.
379 		docstring const s1 = e.compl_text.substr(0, e.compl_unique_to);
380 		docstring const s2 = e.compl_text.substr(e.compl_unique_to);
381 
382 		if (!s1.empty()) {
383 			f.setColor(Color_inlinecompletion);
384 			// offset the text by e.after to make sure that the
385 			// spacing is after the completion, not before.
386 			pi.pain.text(x - e.after, y, s1, f);
387 			x += mathed_string_width(f, s1);
388 		}
389 		if (!s2.empty()) {
390 			f.setColor(Color_nonunique_inlinecompletion);
391 			pi.pain.text(x - e.after, y, s2, f);
392 			x += mathed_string_width(f, s2);
393 		}
394 	}
395 }
396 
397 
kerning(BufferView const * bv) const398 int MathRow::kerning(BufferView const * bv) const
399 {
400 	if (elements_.empty())
401 		return 0;
402 	InsetMath const * inset = elements_[before(elements_.size() - 1)].inset;
403 	return inset ? inset->kerning(bv) : 0;
404 }
405 
406 
operator <<(ostream & os,MathRow::Element const & e)407 ostream & operator<<(ostream & os, MathRow::Element const & e)
408 {
409 	switch (e.type) {
410 	case MathRow::DUMMY:
411 		os << (e.mclass == MC_OPEN ? "{" : "}");
412 		break;
413 	case MathRow::INSET:
414 		os << "<" << e.before << "-"
415 		   << to_utf8(class_to_string(e.mclass))
416 		   << "-" << e.after << ">";
417 		break;
418 	case MathRow::BEGIN:
419 		if (e.inset)
420 			os << "\\" << to_utf8(e.inset->name())
421 			   << "^" << e.macro_nesting << "[";
422 		if (e.ar)
423 			os << "(";
424 		break;
425 	case MathRow::END:
426 		if (e.ar)
427 			os << ")";
428 		if (e.inset)
429 			os << "]";
430 		break;
431 	case MathRow::BOX:
432 		os << "<" << e.before << "-[]-" << e.after << ">";
433 		break;
434 	}
435 	return os;
436 }
437 
438 
operator <<(ostream & os,MathRow const & mrow)439 ostream & operator<<(ostream & os, MathRow const & mrow)
440 {
441 	for (MathRow::Element const & e : mrow)
442 		os << e << "  ";
443 	return os;
444 }
445 
446 } // namespace lyx
447