1// Aseprite
2// Copyright (C) 2019-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This program is distributed under the terms of
6// the End-User License Agreement for Aseprite.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "app/ui/skin/skin_theme.h"
13
14#include "app/app.h"
15#include "app/console.h"
16#include "app/extensions.h"
17#include "app/font_path.h"
18#include "app/modules/gui.h"
19#include "app/pref/preferences.h"
20#include "app/resource_finder.h"
21#include "app/ui/app_menuitem.h"
22#include "app/ui/keyboard_shortcuts.h"
23#include "app/ui/skin/font_data.h"
24#include "app/ui/skin/skin_property.h"
25#include "app/ui/skin/skin_slider_property.h"
26#include "app/xml_document.h"
27#include "app/xml_exception.h"
28#include "base/fs.h"
29#include "base/log.h"
30#include "base/string.h"
31#include "base/utf8_decode.h"
32#include "gfx/border.h"
33#include "gfx/point.h"
34#include "gfx/rect.h"
35#include "gfx/size.h"
36#include "os/draw_text.h"
37#include "os/font.h"
38#include "os/surface.h"
39#include "os/system.h"
40#include "ui/intern.h"
41#include "ui/ui.h"
42
43#include "tinyxml.h"
44
45#include <algorithm>
46#include <cstring>
47
48#define BGCOLOR (getWidgetBgColor(widget))
49
50namespace app {
51namespace skin {
52
53using namespace gfx;
54using namespace ui;
55
56// TODO For backward compatibility, in future versions we should remove this (extensions are preferred)
57const char* SkinTheme::kThemesFolderName = "themes";
58
59// This class offer backward compatibility with old themes, completing
60// or changing styles from the default theme to match the default
61// theme of previous versions, so third-party themes can look like
62// they are running in the old Aseprite without any modification.
63struct app::skin::SkinTheme::BackwardCompatibility {
64 bool hasSliderStyle = false;
65 void notifyStyleExistence(const char* styleId) {
66 if (std::strcmp(styleId, "slider") == 0)
67 hasSliderStyle = true;
68 }
69 void createMissingStyles(SkinTheme* theme) {
70 if (!hasSliderStyle &&
71 theme->styles.slider() &&
72 theme->styles.miniSlider()) {
73 // Old slider style
74 ui::Style style(nullptr);
75 os::Font* font = theme->getDefaultFont();
76 const int h = font->height();
77
78 style.setId(theme->styles.slider()->id());
79 style.setFont(AddRef(font));
80
81 auto part = theme->parts.sliderEmpty();
82 style.setBorder(
83 gfx::Border(part->bitmapW()->width()-1*guiscale(),
84 part->bitmapN()->height()+h/2,
85 part->bitmapE()->width()-1*guiscale(),
86 part->bitmapS()->height()-1*guiscale()+h/2));
87
88 *theme->styles.slider() = style;
89 *theme->styles.miniSlider() = style;
90 }
91 }
92};
93
94static const char* g_cursor_names[kCursorTypes] = {
95 "null", // kNoCursor
96 "normal", // kArrowCursor
97 "normal_add", // kArrowPlusCursor
98 "crosshair", // kCrosshairCursor
99 "forbidden", // kForbiddenCursor
100 "hand", // kHandCursor
101 "scroll", // kScrollCursor
102 "move", // kMoveCursor
103 "size_ns", // kSizeNSCursor
104 "size_we", // kSizeWECursor
105 "size_n", // kSizeNCursor
106 "size_ne", // kSizeNECursor
107 "size_e", // kSizeECursor
108 "size_se", // kSizeSECursor
109 "size_s", // kSizeSCursor
110 "size_sw", // kSizeSWCursor
111 "size_w", // kSizeWCursor
112 "size_nw", // kSizeNWCursor
113};
114
115static FontData* load_font(std::map<std::string, FontData*>& fonts,
116 const TiXmlElement* xmlFont,
117 const std::string& xmlFilename)
118{
119 const char* fontRef = xmlFont->Attribute("font");
120 if (fontRef) {
121 auto it = fonts.find(fontRef);
122 if (it == fonts.end())
123 throw base::Exception("Font named '%s' not found\n", fontRef);
124 return it->second;
125 }
126
127 const char* nameStr = xmlFont->Attribute("name");
128 if (!nameStr)
129 throw base::Exception("No \"name\" or \"font\" attributes specified on <font>");
130
131 std::string name(nameStr);
132
133 // Use cached font data
134 auto it = fonts.find(name);
135 if (it != fonts.end())
136 return it->second;
137
138 LOG(VERBOSE, "THEME: Loading font '%s'\n", name.c_str());
139
140 const char* typeStr = xmlFont->Attribute("type");
141 if (!typeStr)
142 throw base::Exception("<font> without 'type' attribute in '%s'\n",
143 xmlFilename.c_str());
144
145 std::string type(typeStr);
146 std::string xmlDir(base::get_file_path(xmlFilename));
147 std::unique_ptr<FontData> font(nullptr);
148
149 if (type == "spritesheet") {
150 const char* fileStr = xmlFont->Attribute("file");
151 if (fileStr) {
152 font.reset(new FontData(os::FontType::SpriteSheet));
153 font->setFilename(base::join_path(xmlDir, fileStr));
154 }
155 }
156 else if (type == "truetype") {
157 const char* platformFileAttrName =
158#ifdef _WIN32
159 "file_win"
160#elif defined __APPLE__
161 "file_mac"
162#else
163 "file_linux"
164#endif
165 ;
166
167 const char* platformFileStr = xmlFont->Attribute(platformFileAttrName);
168 const char* fileStr = xmlFont->Attribute("file");
169 bool antialias = true;
170 if (xmlFont->Attribute("antialias"))
171 antialias = bool_attr(xmlFont, "antialias", false);
172
173 std::string fontFilename;
174 if (platformFileStr)
175 fontFilename = app::find_font(xmlDir, platformFileStr);
176 if (fileStr && fontFilename.empty())
177 fontFilename = app::find_font(xmlDir, fileStr);
178
179 // The filename can be empty if the font was not found, anyway we
180 // want to keep the font information (e.g. to use the fallback
181 // information of this font).
182 font.reset(new FontData(os::FontType::FreeType));
183 font->setFilename(fontFilename);
184 font->setAntialias(antialias);
185
186 if (!fontFilename.empty())
187 LOG(VERBOSE, "THEME: Font file '%s' found\n", fontFilename.c_str());
188 }
189 else {
190 throw base::Exception("Invalid type=\"%s\" in '%s' for <font name=\"%s\" ...>\n",
191 type.c_str(), xmlFilename.c_str(), name.c_str());
192 }
193
194 FontData* result = nullptr;
195 if (font) {
196 fonts[name] = result = font.get();
197 font.release();
198
199 // Fallback font
200 const TiXmlElement* xmlFallback =
201 (const TiXmlElement*)xmlFont->FirstChild("fallback");
202 if (xmlFallback) {
203 FontData* fallback = load_font(fonts, xmlFallback, xmlFilename);
204 if (fallback) {
205 int size = 10;
206 const char* sizeStr = xmlFont->Attribute("size");
207 if (sizeStr)
208 size = std::strtol(sizeStr, nullptr, 10);
209
210 result->setFallback(fallback, size);
211 }
212 }
213 }
214 return result;
215}
216
217// static
218SkinTheme* SkinTheme::instance()
219{
220 if (auto mgr = ui::Manager::getDefault())
221 return SkinTheme::get(mgr);
222 else
223 return nullptr;
224}
225
226// static
227SkinTheme* SkinTheme::get(const ui::Widget* widget)
228{
229 ASSERT(widget);
230 ASSERT(widget->theme());
231 ASSERT(dynamic_cast<SkinTheme*>(widget->theme()));
232 return static_cast<SkinTheme*>(widget->theme());
233}
234
235SkinTheme::SkinTheme()
236 : m_sheet(nullptr)
237 , m_defaultFont(nullptr)
238 , m_miniFont(nullptr)
239 , m_preferredScreenScaling(-1)
240 , m_preferredUIScaling(-1)
241{
242 m_standardCursors.fill(nullptr);
243}
244
245SkinTheme::~SkinTheme()
246{
247 // Delete all cursors.
248 for (auto& it : m_cursors)
249 delete it.second; // Delete cursor
250
251 m_sheet.reset();
252 m_parts_by_id.clear();
253
254 // Delete all styles.
255 for (auto style : m_styles)
256 delete style.second;
257 m_styles.clear();
258
259 // Destroy fonts
260 for (auto& kv : m_fonts)
261 delete kv.second; // Delete all FontDatas
262 m_fonts.clear();
263}
264
265void SkinTheme::onRegenerateTheme()
266{
267 Preferences& pref = Preferences::instance();
268
269 // First we load the skin from default theme, which is more proper
270 // to have every single needed skin part/color/dimension.
271 loadAll(pref.theme.selected.defaultValue());
272
273 // Then we load the selected theme to redefine default theme parts.
274 if (pref.theme.selected.defaultValue() != pref.theme.selected()) {
275 try {
276 BackwardCompatibility backward;
277 loadAll(pref.theme.selected(), &backward);
278 }
279 catch (const std::exception& e) {
280 LOG("THEME: Error loading user-theme: %s\n", e.what());
281
282 // Load default theme again
283 loadAll(pref.theme.selected.defaultValue());
284
285 if (ui::get_theme())
286 Console::showException(e);
287
288 // We can continue, as we've already loaded the default theme
289 // anyway. Here we restore the setting to its default value.
290 pref.theme.selected(pref.theme.selected.defaultValue());
291 }
292 }
293}
294
295void SkinTheme::loadFontData()
296{
297 LOG("THEME: Loading fonts\n");
298
299 std::string fonstFilename("fonts/fonts.xml");
300
301 ResourceFinder rf;
302 rf.includeDataDir(fonstFilename.c_str());
303 if (!rf.findFirst())
304 throw base::Exception("File %s not found", fonstFilename.c_str());
305
306 XmlDocumentRef doc = open_xml(rf.filename());
307 TiXmlHandle handle(doc.get());
308
309 TiXmlElement* xmlFont = handle
310 .FirstChild("fonts")
311 .FirstChild("font").ToElement();
312 while (xmlFont) {
313 load_font(m_fonts, xmlFont, rf.filename());
314 xmlFont = xmlFont->NextSiblingElement();
315 }
316}
317
318void SkinTheme::loadAll(const std::string& themeId,
319 BackwardCompatibility* backward)
320{
321 LOG("THEME: Loading theme %s\n", themeId.c_str());
322
323 if (m_fonts.empty())
324 loadFontData();
325
326 m_path = findThemePath(themeId);
327 if (m_path.empty())
328 throw base::Exception("Theme %s not found", themeId.c_str());
329
330 loadSheet();
331 loadXml(backward);
332}
333
334void SkinTheme::loadSheet()
335{
336 // Load the skin sheet
337 std::string sheet_filename(base::join_path(m_path, "sheet.png"));
338 os::SurfaceRef newSheet;
339 try {
340 newSheet = os::instance()->loadRgbaSurface(sheet_filename.c_str());
341 }
342 catch (...) {
343 // Ignore the error, newSheet is nullptr and we will throw our own
344 // exception.
345 }
346 if (!newSheet)
347 throw base::Exception("Error loading %s file", sheet_filename.c_str());
348
349 // Replace the sprite sheet
350 if (m_sheet)
351 m_sheet.reset();
352 m_sheet = newSheet;
353 if (m_sheet)
354 m_sheet->applyScale(guiscale());
355 m_sheet->setImmutable();
356
357 // Reset sprite sheet and font of all layer styles (to avoid
358 // dangling pointers to os::Surface or os::Font).
359 for (auto& it : m_styles) {
360 for (auto& layer : it.second->layers()) {
361 layer.setIcon(nullptr);
362 layer.setSpriteSheet(nullptr);
363 }
364 it.second->setFont(nullptr);
365 }
366}
367
368void SkinTheme::loadXml(BackwardCompatibility* backward)
369{
370 const int scale = guiscale();
371
372 // Load the skin XML
373 std::string xml_filename(base::join_path(m_path, "theme.xml"));
374
375 XmlDocumentRef doc = open_xml(xml_filename);
376 TiXmlHandle handle(doc.get());
377
378 // Load Preferred scaling
379 m_preferredScreenScaling = -1;
380 m_preferredUIScaling = -1;
381 {
382 TiXmlElement* xmlTheme = handle
383 .FirstChild("theme").ToElement();
384 if (xmlTheme) {
385 const char* screenScaling = xmlTheme->Attribute("screenscaling");
386 const char* uiScaling = xmlTheme->Attribute("uiscaling");
387 if (screenScaling)
388 m_preferredScreenScaling = std::strtol(screenScaling, nullptr, 10);
389 if (uiScaling)
390 m_preferredUIScaling = std::strtol(uiScaling, nullptr, 10);
391 }
392 }
393
394 // Load fonts
395 {
396 TiXmlElement* xmlFont = handle
397 .FirstChild("theme")
398 .FirstChild("fonts")
399 .FirstChild("font").ToElement();
400 while (xmlFont) {
401 const char* idStr = xmlFont->Attribute("id");
402 FontData* fontData = load_font(m_fonts, xmlFont, xml_filename);
403 if (idStr && fontData) {
404 std::string id(idStr);
405 LOG(VERBOSE, "THEME: Loading theme font %s\n", idStr);
406
407 int size = 10;
408 const char* sizeStr = xmlFont->Attribute("size");
409 if (sizeStr)
410 size = std::strtol(sizeStr, nullptr, 10);
411
412 os::FontRef font = fontData->getFont(size);
413 m_themeFonts[idStr] = font;
414
415 if (id == "default")
416 m_defaultFont = font;
417 else if (id == "mini")
418 m_miniFont = font;
419 }
420
421 xmlFont = xmlFont->NextSiblingElement();
422 }
423 }
424
425 // No available font to run the program
426 if (!m_defaultFont)
427 throw base::Exception("There is no default font");
428 if (!m_miniFont)
429 m_miniFont = m_defaultFont;
430
431 // Load dimension
432 {
433 TiXmlElement* xmlDim = handle
434 .FirstChild("theme")
435 .FirstChild("dimensions")
436 .FirstChild("dim").ToElement();
437 while (xmlDim) {
438 std::string id = xmlDim->Attribute("id");
439 uint32_t value = strtol(xmlDim->Attribute("value"), NULL, 10);
440
441 LOG(VERBOSE, "THEME: Loading dimension %s\n", id.c_str());
442
443 m_dimensions_by_id[id] = value;
444 xmlDim = xmlDim->NextSiblingElement();
445 }
446 }
447
448 // Load colors
449 {
450 TiXmlElement* xmlColor = handle
451 .FirstChild("theme")
452 .FirstChild("colors")
453 .FirstChild("color").ToElement();
454 while (xmlColor) {
455 std::string id = xmlColor->Attribute("id");
456 uint32_t value = strtol(xmlColor->Attribute("value")+1, NULL, 16);
457 gfx::Color color = gfx::rgba(
458 (value & 0xff0000) >> 16,
459 (value & 0xff00) >> 8,
460 (value & 0xff));
461
462 LOG(VERBOSE, "THEME: Loading color %s\n", id.c_str());
463
464 m_colors_by_id[id] = color;
465 xmlColor = xmlColor->NextSiblingElement();
466 }
467 }
468
469 // Load parts
470 {
471 TiXmlElement* xmlPart = handle
472 .FirstChild("theme")
473 .FirstChild("parts")
474 .FirstChild("part").ToElement();
475 while (xmlPart) {
476 // Get the tool-icon rectangle
477 const char* part_id = xmlPart->Attribute("id");
478 int x = scale*strtol(xmlPart->Attribute("x"), nullptr, 10);
479 int y = scale*strtol(xmlPart->Attribute("y"), nullptr, 10);
480 int w = (xmlPart->Attribute("w") ? scale*strtol(xmlPart->Attribute("w"), nullptr, 10): 0);
481 int h = (xmlPart->Attribute("h") ? scale*strtol(xmlPart->Attribute("h"), nullptr, 10): 0);
482
483 LOG(VERBOSE, "THEME: Loading part %s\n", part_id);
484
485 SkinPartPtr part = m_parts_by_id[part_id];
486 if (!part)
487 part = m_parts_by_id[part_id] = SkinPartPtr(new SkinPart);
488
489 if (w > 0 && h > 0) {
490 part->setSpriteBounds(gfx::Rect(x, y, w, h));
491 part->setBitmap(0, sliceSheet(part->bitmapRef(0), gfx::Rect(x, y, w, h)));
492 }
493 else if (xmlPart->Attribute("w1")) { // 3x3-1 part (NW, N, NE, E, SE, S, SW, W)
494 int w1 = scale*strtol(xmlPart->Attribute("w1"), nullptr, 10);
495 int w2 = scale*strtol(xmlPart->Attribute("w2"), nullptr, 10);
496 int w3 = scale*strtol(xmlPart->Attribute("w3"), nullptr, 10);
497 int h1 = scale*strtol(xmlPart->Attribute("h1"), nullptr, 10);
498 int h2 = scale*strtol(xmlPart->Attribute("h2"), nullptr, 10);
499 int h3 = scale*strtol(xmlPart->Attribute("h3"), nullptr, 10);
500
501 part->setSpriteBounds(gfx::Rect(x, y, w1+w2+w3, h1+h2+h3));
502 part->setSlicesBounds(gfx::Rect(w1, h1, w2, h2));
503
504 part->setBitmap(0, sliceSheet(part->bitmapRef(0), gfx::Rect(x, y, w1, h1))); // NW
505 part->setBitmap(1, sliceSheet(part->bitmapRef(1), gfx::Rect(x+w1, y, w2, h1))); // N
506 part->setBitmap(2, sliceSheet(part->bitmapRef(2), gfx::Rect(x+w1+w2, y, w3, h1))); // NE
507 part->setBitmap(3, sliceSheet(part->bitmapRef(3), gfx::Rect(x+w1+w2, y+h1, w3, h2))); // E
508 part->setBitmap(4, sliceSheet(part->bitmapRef(4), gfx::Rect(x+w1+w2, y+h1+h2, w3, h3))); // SE
509 part->setBitmap(5, sliceSheet(part->bitmapRef(5), gfx::Rect(x+w1, y+h1+h2, w2, h3))); // S
510 part->setBitmap(6, sliceSheet(part->bitmapRef(6), gfx::Rect(x, y+h1+h2, w1, h3))); // SW
511 part->setBitmap(7, sliceSheet(part->bitmapRef(7), gfx::Rect(x, y+h1, w1, h2))); // W
512 }
513
514 // Is it a mouse cursor?
515 if (std::strncmp(part_id, "cursor_", 7) == 0) {
516 std::string cursorName = std::string(part_id).substr(7);
517 int focusx = scale*std::strtol(xmlPart->Attribute("focusx"), NULL, 10);
518 int focusy = scale*std::strtol(xmlPart->Attribute("focusy"), NULL, 10);
519
520 LOG(VERBOSE, "THEME: Loading cursor '%s'\n", cursorName.c_str());
521
522 auto it = m_cursors.find(cursorName);
523 if (it != m_cursors.end() && it->second != nullptr) {
524 delete it->second;
525 it->second = nullptr;
526 }
527
528 os::SurfaceRef slice = sliceSheet(nullptr, gfx::Rect(x, y, w, h));
529 Cursor* cursor = new Cursor(slice, gfx::Point(focusx, focusy));
530 m_cursors[cursorName] = cursor;
531
532 for (int c=0; c<kCursorTypes; ++c) {
533 if (cursorName == g_cursor_names[c]) {
534 m_standardCursors[c] = cursor;
535 break;
536 }
537 }
538 }
539
540 xmlPart = xmlPart->NextSiblingElement();
541 }
542 }
543
544 // Load styles
545 {
546 TiXmlElement* xmlStyle = handle
547 .FirstChild("theme")
548 .FirstChild("styles")
549 .FirstChild("style").ToElement();
550
551 if (!xmlStyle) // Without styles?
552 throw base::Exception("There are no styles");
553
554 while (xmlStyle) {
555 const char* style_id = xmlStyle->Attribute("id");
556 if (!style_id) {
557 throw base::Exception("<style> without 'id' attribute in '%s'\n",
558 xml_filename.c_str());
559 }
560
561 const char* extends_id = xmlStyle->Attribute("extends");
562 const ui::Style* base = nullptr;
563 if (extends_id)
564 base = m_styles[extends_id];
565
566 if (backward)
567 backward->notifyStyleExistence(style_id);
568
569 ui::Style* style = m_styles[style_id];
570 if (!style) {
571 m_styles[style_id] = style = new ui::Style(base);
572 }
573 else {
574 *style = ui::Style(base);
575 }
576 style->setId(style_id);
577
578 // Margin
579 {
580 const char* m = xmlStyle->Attribute("margin");
581 const char* l = xmlStyle->Attribute("margin-left");
582 const char* t = xmlStyle->Attribute("margin-top");
583 const char* r = xmlStyle->Attribute("margin-right");
584 const char* b = xmlStyle->Attribute("margin-bottom");
585 gfx::Border margin = ui::Style::UndefinedBorder();
586 if (m || l) margin.left(scale*std::strtol(l ? l: m, nullptr, 10));
587 if (m || t) margin.top(scale*std::strtol(t ? t: m, nullptr, 10));
588 if (m || r) margin.right(scale*std::strtol(r ? r: m, nullptr, 10));
589 if (m || b) margin.bottom(scale*std::strtol(b ? b: m, nullptr, 10));
590 style->setMargin(margin);
591 }
592
593 // Border
594 {
595 const char* m = xmlStyle->Attribute("border");
596 const char* l = xmlStyle->Attribute("border-left");
597 const char* t = xmlStyle->Attribute("border-top");
598 const char* r = xmlStyle->Attribute("border-right");
599 const char* b = xmlStyle->Attribute("border-bottom");
600 gfx::Border border = ui::Style::UndefinedBorder();
601 if (m || l) border.left(scale*std::strtol(l ? l: m, nullptr, 10));
602 if (m || t) border.top(scale*std::strtol(t ? t: m, nullptr, 10));
603 if (m || r) border.right(scale*std::strtol(r ? r: m, nullptr, 10));
604 if (m || b) border.bottom(scale*std::strtol(b ? b: m, nullptr, 10));
605 style->setBorder(border);
606 }
607
608 // Padding
609 {
610 const char* m = xmlStyle->Attribute("padding");
611 const char* l = xmlStyle->Attribute("padding-left");
612 const char* t = xmlStyle->Attribute("padding-top");
613 const char* r = xmlStyle->Attribute("padding-right");
614 const char* b = xmlStyle->Attribute("padding-bottom");
615 gfx::Border padding = ui::Style::UndefinedBorder();
616 if (m || l) padding.left(scale*std::strtol(l ? l: m, nullptr, 10));
617 if (m || t) padding.top(scale*std::strtol(t ? t: m, nullptr, 10));
618 if (m || r) padding.right(scale*std::strtol(r ? r: m, nullptr, 10));
619 if (m || b) padding.bottom(scale*std::strtol(b ? b: m, nullptr, 10));
620 style->setPadding(padding);
621 }
622
623 // Font
624 {
625 const char* fontId = xmlStyle->Attribute("font");
626 if (fontId) {
627 os::FontRef font = m_themeFonts[fontId];
628 style->setFont(font);
629 }
630 }
631
632 TiXmlElement* xmlLayer = xmlStyle->FirstChildElement();
633 while (xmlLayer) {
634 const std::string layerName = xmlLayer->Value();
635
636 LOG(VERBOSE, "THEME: Layer %s for %s\n", layerName.c_str(), style_id);
637
638 ui::Style::Layer layer;
639
640 // Layer type
641 if (layerName == "background") {
642 layer.setType(ui::Style::Layer::Type::kBackground);
643 }
644 else if (layerName == "background-border") {
645 layer.setType(ui::Style::Layer::Type::kBackgroundBorder);
646 }
647 else if (layerName == "border") {
648 layer.setType(ui::Style::Layer::Type::kBorder);
649 }
650 else if (layerName == "icon") {
651 layer.setType(ui::Style::Layer::Type::kIcon);
652 }
653 else if (layerName == "text") {
654 layer.setType(ui::Style::Layer::Type::kText);
655 }
656 else if (layerName == "newlayer") {
657 layer.setType(ui::Style::Layer::Type::kNewLayer);
658 }
659
660 // Parse state condition
661 const char* stateValue = xmlLayer->Attribute("state");
662 if (stateValue) {
663 std::string state(stateValue);
664 int flags = 0;
665 if (state.find("disabled") != std::string::npos) flags |= ui::Style::Layer::kDisabled;
666 if (state.find("selected") != std::string::npos) flags |= ui::Style::Layer::kSelected;
667 if (state.find("focus") != std::string::npos) flags |= ui::Style::Layer::kFocus;
668 if (state.find("mouse") != std::string::npos) flags |= ui::Style::Layer::kMouse;
669 layer.setFlags(flags);
670 }
671
672 // Align
673 const char* alignValue = xmlLayer->Attribute("align");
674 if (alignValue) {
675 std::string alignString(alignValue);
676 int align = 0;
677 if (alignString.find("left") != std::string::npos) align |= LEFT;
678 if (alignString.find("center") != std::string::npos) align |= CENTER;
679 if (alignString.find("right") != std::string::npos) align |= RIGHT;
680 if (alignString.find("top") != std::string::npos) align |= TOP;
681 if (alignString.find("middle") != std::string::npos) align |= MIDDLE;
682 if (alignString.find("bottom") != std::string::npos) align |= BOTTOM;
683 if (alignString.find("wordwrap") != std::string::npos) align |= WORDWRAP;
684 layer.setAlign(align);
685 }
686
687 // Color
688 const char* colorId = xmlLayer->Attribute("color");
689 if (colorId) {
690 auto it = m_colors_by_id.find(colorId);
691 if (it != m_colors_by_id.end())
692 layer.setColor(it->second);
693 else if (std::strcmp(colorId, "none") == 0) {
694 layer.setColor(gfx::ColorNone);
695 }
696 else {
697 throw base::Exception("Color <%s color='%s' ...> was not found in '%s'\n",
698 layerName.c_str(), colorId,
699 xml_filename.c_str());
700 }
701 }
702
703 // Offset
704 const char* x = xmlLayer->Attribute("x");
705 const char* y = xmlLayer->Attribute("y");
706 if (x || y) {
707 gfx::Point offset(0, 0);
708 if (x) offset.x = std::strtol(x, nullptr, 10);
709 if (y) offset.y = std::strtol(y, nullptr, 10);
710 layer.setOffset(offset*scale);
711 }
712
713 // Sprite sheet
714 const char* partId = xmlLayer->Attribute("part");
715 if (partId) {
716 auto it = m_parts_by_id.find(partId);
717 if (it != m_parts_by_id.end()) {
718 SkinPartPtr part = it->second;
719 if (part) {
720 if (layer.type() == ui::Style::Layer::Type::kIcon)
721 layer.setIcon(AddRef(part->bitmap(0)));
722 else {
723 layer.setSpriteSheet(m_sheet);
724 layer.setSpriteBounds(part->spriteBounds());
725 layer.setSlicesBounds(part->slicesBounds());
726 }
727 }
728 }
729 else if (std::strcmp(partId, "none") == 0) {
730 layer.setIcon(nullptr);
731 layer.setSpriteSheet(nullptr);
732 layer.setSpriteBounds(gfx::Rect(0, 0, 0, 0));
733 layer.setSlicesBounds(gfx::Rect(0, 0, 0, 0));
734 }
735 else {
736 throw base::Exception("Part <%s part='%s' ...> was not found in '%s'\n",
737 layerName.c_str(), partId,
738 xml_filename.c_str());
739 }
740 }
741
742 if (layer.type() != ui::Style::Layer::Type::kNone)
743 style->addLayer(layer);
744
745 xmlLayer = xmlLayer->NextSiblingElement();
746 }
747
748 xmlStyle = xmlStyle->NextSiblingElement();
749 }
750 }
751
752 if (backward)
753 backward->createMissingStyles(this);
754
755 ThemeFile<SkinTheme>::updateInternals();
756}
757
758os::SurfaceRef SkinTheme::sliceSheet(os::SurfaceRef sur, const gfx::Rect& bounds)
759{
760 if (sur && (sur->width() != bounds.w ||
761 sur->height() != bounds.h)) {
762 sur = nullptr;
763 }
764
765 if (!bounds.isEmpty()) {
766 if (!sur)
767 sur = os::instance()->makeRgbaSurface(bounds.w, bounds.h);
768
769 os::SurfaceLock lockSrc(m_sheet.get());
770 os::SurfaceLock lockDst(sur.get());
771 m_sheet->blitTo(sur.get(), bounds.x, bounds.y, 0, 0, bounds.w, bounds.h);
772
773 // The new surface is immutable because we're going to re-use the
774 // surface if we reload the theme.
775 //
776 // TODO Add sub-surfaces (SkBitmap::extractSubset())
777 //sur->setImmutable();
778 }
779 else {
780 ASSERT(!sur);
781 }
782
783 return sur;
784}
785
786os::Font* SkinTheme::getWidgetFont(const Widget* widget) const
787{
788 auto skinPropery = std::static_pointer_cast<SkinProperty>(widget->getProperty(SkinProperty::Name));
789 if (skinPropery && skinPropery->hasMiniFont())
790 return getMiniFont();
791 else
792 return getDefaultFont();
793}
794
795Cursor* SkinTheme::getStandardCursor(CursorType type)
796{
797 if (type >= kFirstCursorType && type <= kLastCursorType)
798 return m_standardCursors[type];
799 else
800 return nullptr;
801}
802
803void SkinTheme::initWidget(Widget* widget)
804{
805#define BORDER(n) \
806 widget->setBorder(gfx::Border(n))
807
808#define BORDER4(L,T,R,B) \
809 widget->setBorder(gfx::Border((L), (T), (R), (B)))
810
811 const int scale = guiscale();
812
813 switch (widget->type()) {
814
815 case kBoxWidget:
816 widget->setStyle(styles.box());
817 BORDER(0);
818 widget->setChildSpacing(4 * scale);
819 break;
820
821 case kButtonWidget:
822 widget->setStyle(styles.button());
823 break;
824
825 case kCheckWidget:
826 widget->setStyle(styles.checkBox());
827 break;
828
829 case kRadioWidget:
830 widget->setStyle(styles.radioButton());
831 break;
832
833 case kEntryWidget:
834 BORDER4(
835 parts.sunkenNormal()->bitmapW()->width(),
836 parts.sunkenNormal()->bitmapN()->height(),
837 parts.sunkenNormal()->bitmapE()->width(),
838 parts.sunkenNormal()->bitmapS()->height());
839 widget->setChildSpacing(3 * scale);
840 break;
841
842 case kGridWidget:
843 widget->setStyle(styles.grid());
844 BORDER(0);
845 widget->setChildSpacing(4 * scale);
846 break;
847
848 case kLabelWidget:
849 widget->setStyle(styles.label());
850 break;
851
852 case kLinkLabelWidget:
853 widget->setStyle(styles.link());
854 break;
855
856 case kListBoxWidget:
857 BORDER(0);
858 widget->setChildSpacing(0);
859 break;
860
861 case kListItemWidget:
862 widget->setStyle(styles.listItem());
863 break;
864
865 case kComboBoxWidget: {
866 ComboBox* combobox = static_cast<ComboBox*>(widget);
867 Button* button = combobox->getButtonWidget();
868 combobox->setChildSpacing(0);
869 button->setStyle(styles.comboboxButton());
870 break;
871 }
872
873 case kMenuWidget:
874 widget->setStyle(styles.menu());
875 break;
876
877 case kMenuBarWidget:
878 widget->setStyle(styles.menubar());
879 break;
880
881 case kMenuBoxWidget:
882 widget->setStyle(styles.menubox());
883 break;
884
885 case kMenuItemWidget:
886 BORDER(2 * scale);
887 widget->setChildSpacing(18 * scale);
888 break;
889
890 case kSplitterWidget:
891 widget->setChildSpacing(3 * scale);
892 widget->setStyle(styles.splitter());
893 break;
894
895 case kSeparatorWidget:
896 // Horizontal bar
897 if (widget->align() & HORIZONTAL) {
898 if (dynamic_cast<MenuSeparator*>(widget)) {
899 widget->setStyle(styles.menuSeparator());
900 BORDER(2 * scale);
901 }
902 else
903 widget->setStyle(styles.horizontalSeparator());
904 }
905 // Vertical bar
906 else {
907 widget->setStyle(styles.verticalSeparator());
908 }
909 break;
910
911 case kSliderWidget:
912 widget->setStyle(styles.slider());
913 break;
914
915 case kTextBoxWidget:
916 widget->setChildSpacing(0);
917 widget->setStyle(styles.textboxText());
918 break;
919
920 case kViewWidget:
921 widget->setChildSpacing(0);
922 widget->setBgColor(colors.windowFace());
923 widget->setStyle(styles.view());
924 break;
925
926 case kViewScrollbarWidget:
927 widget->setStyle(styles.scrollbar());
928 static_cast<ScrollBar*>(widget)->setThumbStyle(styles.scrollbarThumb());
929 break;
930
931 case kViewViewportWidget:
932 BORDER(0);
933 widget->setChildSpacing(0);
934 break;
935
936 case kManagerWidget:
937 widget->setStyle(styles.desktop());
938 break;
939
940 case kWindowWidget:
941 if (TipWindow* window = dynamic_cast<TipWindow*>(widget)) {
942 window->setStyle(styles.tooltipWindow());
943 window->setArrowStyle(styles.tooltipWindowArrow());
944 window->textBox()->setStyle(styles.tooltipText());
945 }
946 else if (dynamic_cast<TransparentPopupWindow*>(widget)) {
947 widget->setStyle(styles.transparentPopupWindow());
948 }
949 else if (dynamic_cast<PopupWindow*>(widget)) {
950 widget->setStyle(styles.popupWindow());
951 }
952 else if (static_cast<Window*>(widget)->isDesktop()) {
953 widget->setStyle(styles.desktop());
954 }
955 else {
956 if (widget->hasText()) {
957 widget->setStyle(styles.windowWithTitle());
958 }
959 else {
960 widget->setStyle(styles.windowWithoutTitle());
961 }
962 }
963 break;
964
965 case kWindowTitleLabelWidget:
966 widget->setStyle(styles.windowTitleLabel());
967 break;
968
969 case kWindowCloseButtonWidget:
970 widget->setStyle(styles.windowCloseButton());
971 break;
972
973 default:
974 break;
975 }
976}
977
978void SkinTheme::getWindowMask(Widget* widget, Region& region)
979{
980 region = widget->bounds();
981}
982
983int SkinTheme::getScrollbarSize()
984{
985 return dimensions.scrollbarSize();
986}
987
988gfx::Size SkinTheme::getEntryCaretSize(Widget* widget)
989{
990 if (widget->font()->type() == os::FontType::FreeType)
991 return gfx::Size(2*guiscale(), widget->textHeight());
992 else
993 return gfx::Size(2*guiscale(), widget->textHeight()+2*guiscale());
994}
995
996void SkinTheme::paintEntry(PaintEvent& ev)
997{
998 Graphics* g = ev.graphics();
999 Entry* widget = static_cast<Entry*>(ev.getSource());
1000 gfx::Rect bounds = widget->clientBounds();
1001
1002 // Outside borders
1003 g->fillRect(BGCOLOR, bounds);
1004
1005 bool isMiniLook = false;
1006 auto skinPropery = std::static_pointer_cast<SkinProperty>(widget->getProperty(SkinProperty::Name));
1007 if (skinPropery)
1008 isMiniLook = (skinPropery->getLook() == MiniLook);
1009
1010 drawRect(g, bounds,
1011 (widget->hasFocus() ?
1012 (isMiniLook ? parts.sunkenMiniFocused().get(): parts.sunkenFocused().get()):
1013 (isMiniLook ? parts.sunkenMiniNormal().get() : parts.sunkenNormal().get())));
1014
1015 drawEntryText(g, widget);
1016}
1017
1018namespace {
1019
1020class DrawEntryTextDelegate : public os::DrawTextDelegate {
1021public:
1022 DrawEntryTextDelegate(Entry* widget, Graphics* graphics,
1023 const gfx::Point& pos, const int h)
1024 : m_widget(widget)
1025 , m_graphics(graphics)
1026 , m_caretDrawn(false)
1027 // m_lastX is an absolute position on screen
1028 , m_lastX(pos.x+m_widget->bounds().x)
1029 , m_y(pos.y)
1030 , m_h(h)
1031 {
1032 m_widget->getEntryThemeInfo(&m_index, &m_caret, &m_state, &m_range);
1033 }
1034
1035 int index() const { return m_index; }
1036 bool caretDrawn() const { return m_caretDrawn; }
1037 const gfx::Rect& textBounds() const { return m_textBounds; }
1038
1039 void preProcessChar(const int index,
1040 const int codepoint,
1041 gfx::Color& fg,
1042 gfx::Color& bg,
1043 const gfx::Rect& charBounds) override {
1044 auto theme = SkinTheme::get(m_widget);
1045
1046 // Normal text
1047 auto& colors = theme->colors;
1048 bg = ColorNone;
1049 fg = colors.text();
1050
1051 // Selected
1052 if ((m_index >= m_range.from) &&
1053 (m_index < m_range.to)) {
1054 if (m_widget->hasFocus())
1055 bg = colors.selected();
1056 else
1057 bg = colors.disabled();
1058 fg = colors.selectedText();
1059 }
1060
1061 // Disabled
1062 if (!m_widget->isEnabled()) {
1063 bg = ColorNone;
1064 fg = colors.disabled();
1065 }
1066
1067 m_bg = bg;
1068 }
1069
1070 bool preDrawChar(const gfx::Rect& charBounds) override {
1071 m_textBounds |= charBounds;
1072 m_charStartX = charBounds.x;
1073
1074 if (charBounds.x2()-m_widget->bounds().x < m_widget->clientBounds().x2()) {
1075 if (m_bg != ColorNone) {
1076 // Fill background e.g. needed for selected/highlighted
1077 // regions with TTF fonts where the char is smaller than the
1078 // text bounds [m_y,m_y+m_h)
1079 gfx::Rect fillThisRect(m_lastX-m_widget->bounds().x,
1080 m_y, charBounds.x2()-m_lastX, m_h);
1081 if (charBounds != fillThisRect)
1082 m_graphics->fillRect(m_bg, fillThisRect);
1083 }
1084 m_lastX = charBounds.x2();
1085 return true;
1086 }
1087 else
1088 return false;
1089 }
1090
1091 void postDrawChar(const gfx::Rect& charBounds) override {
1092 // Caret
1093 if (m_state &&
1094 m_index == m_caret &&
1095 m_widget->hasFocus() &&
1096 m_widget->isEnabled()) {
1097 auto theme = SkinTheme::get(m_widget);
1098 theme->drawEntryCaret(
1099 m_graphics, m_widget,
1100 m_charStartX-m_widget->bounds().x, m_y);
1101 m_caretDrawn = true;
1102 }
1103
1104 ++m_index;
1105 }
1106
1107private:
1108 Entry* m_widget;
1109 Graphics* m_graphics;
1110 int m_index;
1111 int m_caret;
1112 int m_state;
1113 Entry::Range m_range;
1114 gfx::Rect m_textBounds;
1115 bool m_caretDrawn;
1116 gfx::Color m_bg;
1117 int m_lastX; // Last position used to fill the background
1118 int m_y, m_h;
1119 int m_charStartX;
1120};
1121
1122} // anonymous namespace
1123
1124void SkinTheme::drawEntryText(ui::Graphics* g, ui::Entry* widget)
1125{
1126 // Draw the text
1127 gfx::Rect bounds = widget->getEntryTextBounds();
1128
1129 DrawEntryTextDelegate delegate(widget, g, bounds.origin(), widget->textHeight());
1130 int scroll = delegate.index();
1131
1132 const std::string& textString = widget->text();
1133 base::utf8_decode dec(textString);
1134 auto pos = dec.pos();
1135 for (int i=0; i<scroll && dec.next(); ++i)
1136 pos = dec.pos();
1137
1138 // TODO use a string_view()
1139 g->drawText(std::string(pos, textString.end()),
1140 colors.text(), ColorNone,
1141 bounds.origin(), &delegate);
1142
1143 bounds.x += delegate.textBounds().w;
1144
1145 // Draw suffix if there is enough space
1146 if (!widget->getSuffix().empty()) {
1147 Rect sufBounds(bounds.x, bounds.y,
1148 bounds.x2()-widget->childSpacing()*guiscale()-bounds.x,
1149 widget->textHeight());
1150 IntersectClip clip(g, sufBounds & widget->clientChildrenBounds());
1151 if (clip) {
1152 drawText(
1153 g, widget->getSuffix().c_str(),
1154 colors.entrySuffix(), ColorNone,
1155 widget, sufBounds, widget->align(), 0);
1156 }
1157 }
1158
1159 // Draw caret at the end of the text
1160 if (!delegate.caretDrawn()) {
1161 gfx::Rect charBounds(bounds.x+widget->bounds().x,
1162 bounds.y+widget->bounds().y, 0, widget->textHeight());
1163 delegate.preDrawChar(charBounds);
1164 delegate.postDrawChar(charBounds);
1165 }
1166}
1167
1168void SkinTheme::paintListBox(PaintEvent& ev)
1169{
1170 Graphics* g = ev.graphics();
1171
1172 g->fillRect(colors.background(), g->getClipBounds());
1173}
1174
1175void SkinTheme::paintMenu(PaintEvent& ev)
1176{
1177 Widget* widget = static_cast<Widget*>(ev.getSource());
1178 Graphics* g = ev.graphics();
1179
1180 g->fillRect(BGCOLOR, g->getClipBounds());
1181}
1182
1183void SkinTheme::paintMenuItem(ui::PaintEvent& ev)
1184{
1185 int scale = guiscale();
1186 Graphics* g = ev.graphics();
1187 MenuItem* widget = static_cast<MenuItem*>(ev.getSource());
1188 gfx::Rect bounds = widget->clientBounds();
1189 gfx::Color fg, bg;
1190 int c, bar;
1191
1192 // TODO ASSERT?
1193 if (!widget->parent()->parent())
1194 return;
1195
1196 bar = (widget->parent()->parent()->type() == kMenuBarWidget);
1197
1198 // Colors
1199 if (!widget->isEnabled()) {
1200 fg = ColorNone;
1201 bg = colors.menuitemNormalFace();
1202 }
1203 else {
1204 if (widget->isHighlighted()) {
1205 fg = colors.menuitemHighlightText();
1206 bg = colors.menuitemHighlightFace();
1207 }
1208 else if (widget->hasMouse()) {
1209 fg = colors.menuitemHotText();
1210 bg = colors.menuitemHotFace();
1211 }
1212 else {
1213 fg = colors.menuitemNormalText();
1214 bg = colors.menuitemNormalFace();
1215 }
1216 }
1217
1218 // Background
1219 g->fillRect(bg, bounds);
1220
1221 // Draw an indicator for selected items
1222 if (widget->isSelected()) {
1223 os::Surface* icon =
1224 (widget->isEnabled() ?
1225 parts.checkSelected()->bitmap(0):
1226 parts.checkDisabled()->bitmap(0));
1227
1228 int x = bounds.x+4*scale-icon->width()/2;
1229 int y = bounds.y+bounds.h/2-icon->height()/2;
1230 g->drawRgbaSurface(icon, x, y);
1231 }
1232
1233 // Text
1234 if (bar)
1235 widget->setAlign(CENTER | MIDDLE);
1236 else
1237 widget->setAlign(LEFT | MIDDLE);
1238
1239 Rect pos = bounds;
1240 if (!bar)
1241 pos.offset(widget->childSpacing()/2, 0);
1242 drawText(g, nullptr, fg, ColorNone, widget, pos,
1243 widget->align(), widget->mnemonic());
1244
1245 // For menu-box
1246 if (!bar) {
1247 // Draw the arrown (to indicate which this menu has a sub-menu)
1248 if (widget->getSubmenu()) {
1249 // Enabled
1250 if (widget->isEnabled()) {
1251 for (c=0; c<3*scale; c++)
1252 g->drawVLine(fg,
1253 bounds.x2()-3*scale-c,
1254 bounds.y+bounds.h/2-c, 2*c+1);
1255 }
1256 // Disabled
1257 else {
1258 for (c=0; c<3*scale; c++)
1259 g->drawVLine(colors.background(),
1260 bounds.x2()-3*scale-c+1,
1261 bounds.y+bounds.h/2-c+1, 2*c+1);
1262
1263 for (c=0; c<3*scale; c++)
1264 g->drawVLine(colors.disabled(),
1265 bounds.x2()-3*scale-c,
1266 bounds.y+bounds.h/2-c, 2*c+1);
1267 }
1268 }
1269 // Draw the keyboard shortcut
1270 else if (AppMenuItem* appMenuItem = dynamic_cast<AppMenuItem*>(widget)) {
1271 if (appMenuItem->key() && !appMenuItem->key()->accels().empty()) {
1272 int old_align = appMenuItem->align();
1273
1274 pos = bounds;
1275 pos.w -= widget->childSpacing()/4;
1276
1277 std::string buf = appMenuItem->key()->accels().front().toString();
1278
1279 widget->setAlign(RIGHT | MIDDLE);
1280 drawText(g, buf.c_str(), fg, ColorNone, widget, pos, widget->align(), 0);
1281 widget->setAlign(old_align);
1282 }
1283 }
1284 }
1285}
1286
1287void SkinTheme::paintSlider(PaintEvent& ev)
1288{
1289 Graphics* g = ev.graphics();
1290 Slider* widget = static_cast<Slider*>(ev.getSource());
1291 const Rect bounds = widget->clientBounds();
1292 int min, max, value;
1293
1294 // Outside borders
1295 gfx::Color bgcolor = widget->bgColor();
1296 if (!is_transparent(bgcolor))
1297 g->fillRect(bgcolor, bounds);
1298
1299 widget->getSliderThemeInfo(&min, &max, &value);
1300
1301 Rect rc = bounds;
1302 rc.shrink(widget->border());
1303 int x;
1304 if (min != max)
1305 x = rc.x + rc.w * (value-min) / (max-min);
1306 else
1307 x = rc.x;
1308
1309 rc = bounds;
1310
1311 // The mini-look is used for sliders with tiny borders.
1312 bool isMiniLook = false;
1313
1314 // The BG painter is used for sliders without a number-indicator and
1315 // customized background (e.g. RGB sliders)
1316 ISliderBgPainter* bgPainter = NULL;
1317
1318 const auto skinPropery = std::static_pointer_cast<SkinProperty>(widget->getProperty(SkinProperty::Name));
1319 if (skinPropery)
1320 isMiniLook = (skinPropery->getLook() == MiniLook);
1321
1322 const auto skinSliderPropery = std::static_pointer_cast<SkinSliderProperty>(widget->getProperty(SkinSliderProperty::Name));
1323 if (skinSliderPropery)
1324 bgPainter = skinSliderPropery->getBgPainter();
1325
1326 // Draw customized background
1327 if (bgPainter) {
1328 SkinPartPtr nw = parts.miniSliderEmpty();
1329 os::Surface* thumb =
1330 (widget->hasFocus() ? parts.miniSliderThumbFocused()->bitmap(0):
1331 parts.miniSliderThumb()->bitmap(0));
1332
1333 // Draw background
1334 g->fillRect(BGCOLOR, rc);
1335
1336 // Draw thumb
1337 int thumb_y = rc.y;
1338 if (rc.h > thumb->height()*3)
1339 rc.shrink(Border(0, thumb->height(), 0, 0));
1340
1341 // Draw borders
1342 if (rc.h > 4*guiscale()) {
1343 rc.shrink(Border(3, 0, 3, 1) * guiscale());
1344 drawRect(g, rc, nw.get());
1345 }
1346
1347 // Draw background (using the customized ISliderBgPainter implementation)
1348 rc.shrink(Border(1, 1, 1, 2) * guiscale());
1349 if (!rc.isEmpty())
1350 bgPainter->paint(widget, g, rc);
1351
1352 g->drawRgbaSurface(thumb, x-thumb->width()/2, thumb_y);
1353 }
1354 else {
1355 // Draw borders
1356 SkinPartPtr full_part;
1357 SkinPartPtr empty_part;
1358
1359 if (isMiniLook) {
1360 full_part = widget->hasMouseOver() ? parts.miniSliderFullFocused():
1361 parts.miniSliderFull();
1362 empty_part = widget->hasMouseOver() ? parts.miniSliderEmptyFocused():
1363 parts.miniSliderEmpty();
1364 }
1365 else {
1366 full_part = widget->hasFocus() ? parts.sliderFullFocused():
1367 parts.sliderFull();
1368 empty_part = widget->hasFocus() ? parts.sliderEmptyFocused():
1369 parts.sliderEmpty();
1370 }
1371
1372 if (value == min)
1373 drawRect(g, rc, empty_part.get());
1374 else if (value == max)
1375 drawRect(g, rc, full_part.get());
1376 else
1377 drawRect2(g, rc, x,
1378 full_part.get(), empty_part.get());
1379
1380 // Draw text
1381 std::string old_text = widget->text();
1382 widget->setTextQuiet(widget->convertValueToText(value));
1383
1384 gfx::Rect textrc;
1385 int textAlign;
1386 calcTextInfo(widget, widget->style(), bounds, textrc, textAlign);
1387
1388 {
1389 IntersectClip clip(g, Rect(rc.x, rc.y, x-rc.x+1, rc.h));
1390 if (clip) {
1391 drawText(g, nullptr,
1392 colors.sliderFullText(), ColorNone,
1393 widget, textrc, textAlign, widget->mnemonic());
1394 }
1395 }
1396
1397 {
1398 IntersectClip clip(g, Rect(x+1, rc.y, rc.w-(x-rc.x+1), rc.h));
1399 if (clip) {
1400 drawText(g, nullptr,
1401 colors.sliderEmptyText(),
1402 ColorNone, widget, textrc, textAlign, widget->mnemonic());
1403 }
1404 }
1405
1406 widget->setTextQuiet(old_text.c_str());
1407 }
1408}
1409
1410void SkinTheme::paintComboBoxEntry(ui::PaintEvent& ev)
1411{
1412 Graphics* g = ev.graphics();
1413 Entry* widget = static_cast<Entry*>(ev.getSource());
1414 gfx::Rect bounds = widget->clientBounds();
1415
1416 // Outside borders
1417 g->fillRect(BGCOLOR, bounds);
1418
1419 drawRect(g, bounds,
1420 (widget->hasFocus() ?
1421 parts.sunken2Focused().get():
1422 parts.sunken2Normal().get()));
1423
1424 drawEntryText(g, widget);
1425}
1426
1427void SkinTheme::paintTextBox(ui::PaintEvent& ev)
1428{
1429 Graphics* g = ev.graphics();
1430 Widget* widget = static_cast<Widget*>(ev.getSource());
1431
1432 Theme::paintTextBoxWithStyle(g, widget);
1433}
1434
1435void SkinTheme::paintViewViewport(PaintEvent& ev)
1436{
1437 Viewport* widget = static_cast<Viewport*>(ev.getSource());
1438 Graphics* g = ev.graphics();
1439 gfx::Color bg = BGCOLOR;
1440
1441 if (!is_transparent(bg))
1442 g->fillRect(bg, widget->clientBounds());
1443}
1444
1445gfx::Color SkinTheme::getWidgetBgColor(Widget* widget)
1446{
1447 gfx::Color c = widget->bgColor();
1448 bool decorative = widget->isDecorative();
1449
1450 if (!is_transparent(c) ||
1451 widget->type() == kWindowWidget)
1452 return (widget->isTransparent() ? gfx::ColorNone: c);
1453 else if (decorative)
1454 return colors.selected();
1455 else
1456 return colors.face();
1457}
1458
1459void SkinTheme::drawText(Graphics* g, const char* t,
1460 const gfx::Color fgColor,
1461 const gfx::Color bgColor,
1462 const Widget* widget,
1463 const Rect& rc,
1464 const int textAlign,
1465 const int mnemonic)
1466{
1467 if (t || widget->hasText()) {
1468 Rect textrc;
1469
1470 g->setFont(AddRef(widget->font()));
1471
1472 if (!t)
1473 t = widget->text().c_str();
1474
1475 textrc.setSize(g->measureUIText(t));
1476
1477 // Horizontally text alignment
1478
1479 if (textAlign & RIGHT)
1480 textrc.x = rc.x + rc.w - textrc.w - 1;
1481 else if (textAlign & CENTER)
1482 textrc.x = rc.center().x - textrc.w/2;
1483 else
1484 textrc.x = rc.x;
1485
1486 // Vertically text alignment
1487
1488 if (textAlign & BOTTOM)
1489 textrc.y = rc.y + rc.h - textrc.h - 1;
1490 else if (textAlign & MIDDLE)
1491 textrc.y = rc.center().y - textrc.h/2;
1492 else
1493 textrc.y = rc.y;
1494
1495 // Background
1496 if (!is_transparent(bgColor)) {
1497 if (!widget->isEnabled())
1498 g->fillRect(bgColor, Rect(textrc).inflate(guiscale(), guiscale()));
1499 else
1500 g->fillRect(bgColor, textrc);
1501 }
1502
1503 // Text
1504 Rect textWrap = textrc.createIntersection(
1505 // TODO add ui::Widget::getPadding() property
1506 // Rect(widget->clientBounds()).shrink(widget->border()));
1507 widget->clientBounds()).inflate(0, 1*guiscale());
1508
1509 IntersectClip clip(g, textWrap);
1510 if (clip) {
1511 if (!widget->isEnabled()) {
1512 // Draw white part
1513 g->drawUIText(
1514 t,
1515 colors.background(),
1516 gfx::ColorNone,
1517 textrc.origin() + Point(guiscale(), guiscale()),
1518 mnemonic);
1519 }
1520
1521 g->drawUIText(
1522 t,
1523 (!widget->isEnabled() ?
1524 colors.disabled():
1525 (gfx::geta(fgColor) > 0 ? fgColor :
1526 colors.text())),
1527 bgColor, textrc.origin(),
1528 mnemonic);
1529 }
1530 }
1531}
1532
1533void SkinTheme::drawEntryCaret(ui::Graphics* g, Entry* widget, int x, int y)
1534{
1535 gfx::Color color = colors.text();
1536 int textHeight = widget->textHeight();
1537 gfx::Size caretSize = getEntryCaretSize(widget);
1538
1539 for (int u=x; u<x+caretSize.w; ++u)
1540 g->drawVLine(color, u, y+textHeight/2-caretSize.h/2, caretSize.h);
1541}
1542
1543SkinPartPtr SkinTheme::getToolPart(const char* toolId) const
1544{
1545 return getPartById(std::string("tool_") + toolId);
1546}
1547
1548os::Surface* SkinTheme::getToolIcon(const char* toolId) const
1549{
1550 SkinPartPtr part = getToolPart(toolId);
1551 if (part)
1552 return part->bitmap(0);
1553 else
1554 return nullptr;
1555}
1556
1557void SkinTheme::drawRect(Graphics* g, const Rect& rc,
1558 os::Surface* nw, os::Surface* n, os::Surface* ne,
1559 os::Surface* e, os::Surface* se, os::Surface* s,
1560 os::Surface* sw, os::Surface* w)
1561{
1562 int x, y;
1563
1564 // Top
1565
1566 g->drawRgbaSurface(nw, rc.x, rc.y);
1567 {
1568 IntersectClip clip(g, Rect(rc.x+nw->width(), rc.y,
1569 rc.w-nw->width()-ne->width(), rc.h));
1570 if (clip) {
1571 for (x = rc.x+nw->width();
1572 x < rc.x+rc.w-ne->width();
1573 x += n->width()) {
1574 g->drawRgbaSurface(n, x, rc.y);
1575 }
1576 }
1577 }
1578
1579 g->drawRgbaSurface(ne, rc.x+rc.w-ne->width(), rc.y);
1580
1581 // Bottom
1582
1583 g->drawRgbaSurface(sw, rc.x, rc.y+rc.h-sw->height());
1584 {
1585 IntersectClip clip(g, Rect(rc.x+sw->width(), rc.y,
1586 rc.w-sw->width()-se->width(), rc.h));
1587 if (clip) {
1588 for (x = rc.x+sw->width();
1589 x < rc.x+rc.w-se->width();
1590 x += s->width()) {
1591 g->drawRgbaSurface(s, x, rc.y+rc.h-s->height());
1592 }
1593 }
1594 }
1595
1596 g->drawRgbaSurface(se, rc.x+rc.w-se->width(), rc.y+rc.h-se->height());
1597 {
1598 IntersectClip clip(g, Rect(rc.x, rc.y+nw->height(),
1599 rc.w, rc.h-nw->height()-sw->height()));
1600 if (clip) {
1601 // Left
1602 for (y = rc.y+nw->height();
1603 y < rc.y+rc.h-sw->height();
1604 y += w->height()) {
1605 g->drawRgbaSurface(w, rc.x, y);
1606 }
1607
1608 // Right
1609 for (y = rc.y+ne->height();
1610 y < rc.y+rc.h-se->height();
1611 y += e->height()) {
1612 g->drawRgbaSurface(e, rc.x+rc.w-e->width(), y);
1613 }
1614 }
1615 }
1616}
1617
1618void SkinTheme::drawRect(ui::Graphics* g, const gfx::Rect& rc,
1619 SkinPart* skinPart, const bool drawCenter)
1620{
1621 Theme::drawSlices(g, m_sheet.get(), rc,
1622 skinPart->spriteBounds(),
1623 skinPart->slicesBounds(),
1624 gfx::ColorNone,
1625 drawCenter);
1626}
1627
1628void SkinTheme::drawRect2(Graphics* g, const Rect& rc, int x_mid,
1629 SkinPart* nw1, SkinPart* nw2)
1630{
1631 Rect rc2(rc.x, rc.y, x_mid-rc.x+1, rc.h);
1632 {
1633 IntersectClip clip(g, rc2);
1634 if (clip)
1635 drawRect(g, rc, nw1);
1636 }
1637
1638 rc2.x += rc2.w;
1639 rc2.w = rc.w - rc2.w;
1640
1641 IntersectClip clip(g, rc2);
1642 if (clip)
1643 drawRect(g, rc, nw2);
1644}
1645
1646void SkinTheme::drawHline(ui::Graphics* g, const gfx::Rect& rc, SkinPart* part)
1647{
1648 int x;
1649
1650 for (x = rc.x;
1651 x < rc.x2()-part->size().w;
1652 x += part->size().w) {
1653 g->drawRgbaSurface(part->bitmap(0), x, rc.y);
1654 }
1655
1656 if (x < rc.x2()) {
1657 Rect rc2(x, rc.y, rc.w-(x-rc.x), part->size().h);
1658 IntersectClip clip(g, rc2);
1659 if (clip)
1660 g->drawRgbaSurface(part->bitmap(0), x, rc.y);
1661 }
1662}
1663
1664void SkinTheme::drawVline(ui::Graphics* g, const gfx::Rect& rc, SkinPart* part)
1665{
1666 int y;
1667
1668 for (y = rc.y;
1669 y < rc.y2()-part->size().h;
1670 y += part->size().h) {
1671 g->drawRgbaSurface(part->bitmap(0), rc.x, y);
1672 }
1673
1674 if (y < rc.y2()) {
1675 Rect rc2(rc.x, y, part->size().w, rc.h-(y-rc.y));
1676 IntersectClip clip(g, rc2);
1677 if (clip)
1678 g->drawRgbaSurface(part->bitmap(0), rc.x, y);
1679 }
1680}
1681
1682void SkinTheme::paintProgressBar(ui::Graphics* g, const gfx::Rect& rc0, double progress)
1683{
1684 gfx::Color border = colors.text();
1685 border = gfx::rgba(gfx::getr(border),
1686 gfx::getg(border),
1687 gfx::getb(border), 64);
1688 g->drawRect(border, rc0);
1689
1690 gfx::Rect rc = rc0;
1691 rc.shrink(1);
1692
1693 int u = (int)((double)rc.w*progress);
1694 u = std::clamp(u, 0, rc.w);
1695
1696 if (u > 0)
1697 g->fillRect(colors.selected(), gfx::Rect(rc.x, rc.y, u, rc.h));
1698
1699 if (1+u < rc.w)
1700 g->fillRect(colors.background(), gfx::Rect(rc.x+u, rc.y, rc.w-u, rc.h));
1701}
1702
1703std::string SkinTheme::findThemePath(const std::string& themeId) const
1704{
1705 // First we try to find the theme on an extensions
1706 std::string path = App::instance()->extensions().themePath(themeId);
1707 if (path.empty()) {
1708 // Then we try a theme in the old themes/ folder
1709 path = base::join_path(SkinTheme::kThemesFolderName, themeId);
1710 path = base::join_path(path, "theme.xml");
1711
1712 ResourceFinder rf;
1713 rf.includeDataDir(path.c_str());
1714 if (!rf.findFirst())
1715 return std::string();
1716
1717 path = base::get_file_path(rf.filename());
1718 }
1719 return base::normalize_path(path);
1720}
1721
1722} // namespace skin
1723} // namespace app
1724