Rich Text Label widget for Help Browser GUI2 port (#8655)

A rich text label widget that can show text marked up with help markup.
Also includes the GUI Test Window, accessible in the title screen after launching wesnoth using --clock option. It can be used as dialog template/example or as a place to test GUI2 code.
This commit is contained in:
Subhraman Sarkar 2024-06-06 16:33:37 +05:30 committed by GitHub
parent 9dd2d5d94b
commit 43f5644e36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 2057 additions and 112 deletions

View File

@ -33,6 +33,7 @@ test_gui2/modal_dialog_test_game_save
test_gui2/modal_dialog_test_game_save_message
test_gui2/modal_dialog_test_game_save_oos
test_gui2/modal_dialog_test_generator_settings
test_gui2/modal_dialog_test_gui_test_dialog
test_gui2/modal_dialog_test_hotkey_bind
test_gui2/modal_dialog_test_install_dependencies
test_gui2/modal_dialog_test_language_selection

View File

@ -0,0 +1,89 @@
#textdomain wesnoth-lib
###
### Definition of a rich label.
###
### Defines the following labels
### - default, the one for general usage.
### - title, for titles in dialogs.
#define _GUI_RESOLUTION RESOLUTION FONT_FAMILY FONT_SIZE FONT_STYLE FONT_COLOR_ENABLED FONT_COLOR_DISABLED
[resolution]
{RESOLUTION}
min_width = 0
min_height = 0
default_width = 0
default_height = 0
max_width = 0
max_height = 0
text_font_family = {FONT_FAMILY}
text_font_size = {FONT_SIZE}
text_font_style = {FONT_STYLE}
[state_enabled]
[draw]
[/draw]
[/state_enabled]
[state_disabled]
[draw]
[/draw]
[/state_disabled]
[/resolution]
#enddef
#define _GUI_DEFINITION ID DESCRIPTION FONT_FAMILY FONT_SIZE FONT_STYLE FONT_COLOR
[rich_label_definition]
id = {ID}
description = {DESCRIPTION}
{_GUI_RESOLUTION
()
({FONT_FAMILY})
({GUI_FONT_SIZE_{FONT_SIZE}})
({FONT_STYLE})
({GUI__FONT_COLOR_ENABLED__{FONT_COLOR} ALPHA=""})
({GUI__FONT_COLOR_DISABLED__{FONT_COLOR} ALPHA=""})
}
{_GUI_RESOLUTION
({GUI_BIG_RESOLUTION})
({FONT_FAMILY})
({GUI_SCALE_RESOLUTION {GUI_FONT_SIZE_{FONT_SIZE}}})
({FONT_STYLE})
({GUI__FONT_COLOR_ENABLED__{FONT_COLOR} ALPHA=""})
({GUI__FONT_COLOR_DISABLED__{FONT_COLOR} ALPHA=""})
}
[/rich_label_definition]
#enddef
{_GUI_DEFINITION "default" "default label" () DEFAULT () DEFAULT }
{_GUI_DEFINITION "default_bold" "default label, bold font" () DEFAULT "bold" DEFAULT }
{_GUI_DEFINITION "default_italic" "default label, italic font" () DEFAULT "italic" DEFAULT }
{_GUI_DEFINITION "title" "label used for titles" () TITLE () TITLE }
{_GUI_DEFINITION "default_large" "default, large font size" () LARGE () DEFAULT }
{_GUI_DEFINITION "default_huge" "default, huge font size" () HUGE () DEFAULT }
{_GUI_DEFINITION "default_small" "default, small font size" () SMALL () DEFAULT }
{_GUI_DEFINITION "default_tiny" "default, small font size" () TINY () DEFAULT }
{_GUI_DEFINITION "gold" "regular gold label" () DEFAULT () TITLE }
{_GUI_DEFINITION "gold_small" "small gold label" () SMALL () TITLE }
{_GUI_DEFINITION "gold_large" "small gold label" () LARGE () TITLE }
{_GUI_DEFINITION "bad" "regular red label" () DEFAULT () BAD }
{_GUI_DEFINITION "bad_small" "small red label" () SMALL () BAD }
{_GUI_DEFINITION "monospace" "fixed width scroll label" monospace DEFAULT () DEFAULT }
#undef _GUI_SCALE_RES SIZE
#undef _GUI_BIG_RES
#undef _GUI_DEFINITION
#undef _GUI_RESOLUTION

View File

@ -0,0 +1,78 @@
#textdomain wesnoth-test
###
### GUI Test Window
### To be used for testing GUI2 WML code
### Put your gui2 wml code here, then launch it via ./wesnoth --clock
### That should show the GUI Test Window button on the title screen
###
[window]
id = "gui_test_dialog"
description = "GUI Test Dialog"
[resolution]
definition = "default"
automatic_placement = true
vertical_placement = "center"
horizontal_placement = "center"
height = 600
[tooltip]
id = "tooltip"
[/tooltip]
[helptip]
id = "tooltip"
[/helptip]
[grid]
[row]
[column]
[rich_label]
width = 500
label = _ "GUI Test Dialog"
[/rich_label]
[/column]
[/row]
[row]
[column]
horizontal_alignment = "right"
[grid]
[row]
grow_factor = 0
[column]
border = "all"
border_size = 5
horizontal_alignment = "right"
[button]
id = "ok"
definition = "default"
label = _ "OK"
[/button]
[/column]
[/row]
[/grid]
[/column]
[/row]
[/grid]
[/resolution]
[/window]

View File

@ -264,6 +264,7 @@ where
# This debug feature doesn't need to be translated, so put it in the test textdomain.
#textdomain wesnoth-test
{_GUI_BUTTON "clock" _"Clock" _"Show debug clock"}
{_GUI_BUTTON "test_dialog" _"Test Dialog" _"Show GUI Test Dialog"}
#textdomain wesnoth-lib
[/grid]

View File

@ -137,8 +137,13 @@
{REQUIRED_KEY "font_size" f_unsigned}
{DEFAULT_KEY "font_style" font_style ""}
{DEFAULT_KEY "highlight_color" string "#215380"}
{DEFAULT_KEY "highlight_start" f_unsigned 0}
{DEFAULT_KEY "highlight_end" f_unsigned 0}
{DEFAULT_KEY "highlight_start" string ""}
{DEFAULT_KEY "highlight_end" string ""}
{DEFAULT_KEY "actions" string ""}
{DEFAULT_KEY "attr_name" string ""}
{DEFAULT_KEY "attr_start" string ""}
{DEFAULT_KEY "attr_end" string ""}
{DEFAULT_KEY "attr_data" string ""}
{DEFAULT_KEY "maximum_height" f_int -1}
{DEFAULT_KEY "maximum_width" f_int -1}
{DEFAULT_KEY "text" f_t_string ""}
@ -152,7 +157,6 @@
{DEFAULT_KEY "w" f_unsigned 0}
{DEFAULT_KEY "x" f_unsigned 0}
{DEFAULT_KEY "y" f_unsigned 0}
{DEFAULT_KEY "actions" string ""}
[/tag]
[/tag]
[/tag]

View File

@ -217,6 +217,31 @@
{DEFAULT_KEY "link_color" string "#ffff00"}
[/tag]
[/tag]
[tag]
name="rich_label_definition"
min="0"
max="infinite"
super="$generic/widget_definition"
[tag]
name="resolution"
min="0"
max="infinite"
super="$generic/widget_definition/resolution"
[tag]
name="state_disabled"
min="0"
max="1"
super="$generic/state"
[/tag]
[tag]
name="state_enabled"
min="0"
max="1"
super="$generic/state"
[/tag]
{DEFAULT_KEY "link_color" string "#ffff00"}
[/tag]
[/tag]
[tag]
name="listbox_definition"
min="0"

View File

@ -185,6 +185,16 @@
{DEFAULT_KEY "wrap" bool false}
{DEFAULT_KEY "link_aware" bool false}
[/tag]
[tag]
name="rich_label"
min="0"
max="infinite"
super="$generic/widget_instance"
{DEFAULT_KEY "text_alignment" h_align "left"}
{DEFAULT_KEY "link_aware" bool false}
{DEFAULT_KEY "width" unsigned 500}
[/tag]
[tag]
name="grid_listbox"
min="0"

View File

@ -629,6 +629,8 @@
<Unit filename="../../src/gui/dialogs/editor/tod_new_schedule.hpp" />
<Unit filename="../../src/gui/dialogs/end_credits.cpp" />
<Unit filename="../../src/gui/dialogs/end_credits.hpp" />
<Unit filename="../../src/gui/dialogs/gui_test_dialog.cpp" />
<Unit filename="../../src/gui/dialogs/gui_test_dialog.hpp" />
<Unit filename="../../src/gui/dialogs/prompt.cpp" />
<Unit filename="../../src/gui/dialogs/prompt.hpp" />
<Unit filename="../../src/gui/dialogs/file_dialog.cpp" />
@ -823,6 +825,8 @@
<Unit filename="../../src/gui/widgets/repeating_button.cpp" />
<Unit filename="../../src/gui/widgets/repeating_button.hpp" />
<Unit filename="../../src/gui/widgets/retval.hpp" />
<Unit filename="../../src/gui/widgets/rich_label.cpp" />
<Unit filename="../../src/gui/widgets/rich_label.hpp" />
<Unit filename="../../src/gui/widgets/scroll_label.cpp" />
<Unit filename="../../src/gui/widgets/scroll_label.hpp" />
<Unit filename="../../src/gui/widgets/scroll_text.cpp" />

View File

@ -13,6 +13,8 @@
000000000000000000000008 /* achievements_dialog.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000003 /* achievements_dialog.cpp */; };
000000000000000000000011 /* network_download_file.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000009 /* network_download_file.cpp */; };
000000000000000000000012 /* network_download_file.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 000000000000000000000009 /* network_download_file.cpp */; };
00324A11ACB0DB64FBD97896 /* community_dialog.hpp in Headers */ = {isa = PBXBuildFile; fileRef = CD5045B9A988F034F8D17C85 /* community_dialog.hpp */; };
022640C59A7BD907C810FA99 /* rich_label.hpp in Headers */ = {isa = PBXBuildFile; fileRef = C8FF4A8788B6B7E6BE7AB7BB /* rich_label.hpp */; };
02A44BEAA567595C902031CF /* edit_pbl_translation.hpp in Headers */ = {isa = PBXBuildFile; fileRef = C679447D91FD3623CC852FF8 /* edit_pbl_translation.hpp */; };
04C748F7835C62498D27442D /* edit_pbl.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6FA542D78393E8FF067775DA /* edit_pbl.cpp */; };
0554467DB5FE99D85ABCDCA0 /* edit_pbl_translation.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 00574699A982AA23F12B39E0 /* edit_pbl_translation.cpp */; };
@ -30,6 +32,7 @@
393E4C9DAEE19E12B2B168B5 /* edit_unit.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A05D48F0A2C022FC128C8B3E /* edit_unit.cpp */; };
3C254DF5B7DF196F2041955F /* mp_report.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 58C649488B3014E6F7254B62 /* mp_report.cpp */; };
3CC6488695A7293C9CFC2CB6 /* tab_container.hpp in Headers */ = {isa = PBXBuildFile; fileRef = BF3B41AF9FEBC9C3A11736A2 /* tab_container.hpp */; };
3DED4C43AC1B737C15549CEC /* rich_label.hpp in Headers */ = {isa = PBXBuildFile; fileRef = C8FF4A8788B6B7E6BE7AB7BB /* rich_label.hpp */; };
3E9A4297B4A2828C569C8927 /* statistics_record.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27764FB68F02032F1C0B6748 /* statistics_record.cpp */; };
4291489DA38012477DA3BA7C /* general.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 84234C54BB84519421FD4136 /* general.cpp */; };
44CA4F8598147FDAE871B7CB /* prompt.hpp in Headers */ = {isa = PBXBuildFile; fileRef = D4594633BF3F8A06D6AE752F /* prompt.hpp */; };
@ -667,12 +670,14 @@
62D24F2F1519982500350848 /* editor_toolkit.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 62D24F2B1519982500350848 /* editor_toolkit.cpp */; };
62D24F321519987400350848 /* context_manager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 62D24F311519987400350848 /* context_manager.cpp */; };
62D24F351519995200350848 /* palette_manager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 62D24F341519995200350848 /* palette_manager.cpp */; };
6A1F44688986D07EB5DBEF75 /* gui_test_dialog.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 0110429EAA81AED07D53B749 /* gui_test_dialog.hpp */; };
6D574EACA3483ABEE72819F0 /* statistics_record.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27764FB68F02032F1C0B6748 /* statistics_record.cpp */; };
77D94146A5FA29849D1A9BD8 /* multiline_text.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 0B0F48CE9CF65D9813BE6CDC /* multiline_text.cpp */; };
7A0347D48BDB52B1430D9E79 /* migrate_version_selection.hpp in Headers */ = {isa = PBXBuildFile; fileRef = B3DE4F95AF72C6F6BC37E695 /* migrate_version_selection.hpp */; };
7A7146D7893AA09891352019 /* test_schema_validator.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 7CF14AB694764953E2CB3AF7 /* test_schema_validator.cpp */; };
7BFC4DF5BFF8CF75855BA662 /* prompt.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 67044415B63F5888193BD7A6 /* prompt.cpp */; };
7FDF4E8D9C94E7DA8F41F7BB /* tod_new_schedule.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 5D46466DBCD81B13621C7342 /* tod_new_schedule.hpp */; };
805143B8BABF92CA79BEC8F5 /* gui_test_dialog.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 7FBD4033B4B52E9424819B5F /* gui_test_dialog.cpp */; };
867141839BDB89BFE876E310 /* carryover_show_gold.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 09A440B1A671C45BE2924FB4 /* carryover_show_gold.cpp */; };
87744447951D17AA38BE5F48 /* mp_report.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 58C649488B3014E6F7254B62 /* mp_report.cpp */; };
8C704B6F99C824A4073EEBEE /* prompt.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 67044415B63F5888193BD7A6 /* prompt.cpp */; };
@ -1128,6 +1133,7 @@
97714C7A9FF444E29DCEF0BA /* carryover_show_gold.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 09A440B1A671C45BE2924FB4 /* carryover_show_gold.cpp */; };
9B4B41D29C90B05F03DE21B0 /* edit_pbl_translation.hpp in Headers */ = {isa = PBXBuildFile; fileRef = C679447D91FD3623CC852FF8 /* edit_pbl_translation.hpp */; };
9C2743DE8100448B66F7E0AF /* combobox.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 875E45698F8A8D5B750E7317 /* combobox.cpp */; };
9C6342BC8A95B6D23D384486 /* gui_test_dialog.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 0110429EAA81AED07D53B749 /* gui_test_dialog.hpp */; };
AC4242F78B39C571E34AF48F /* edit_unit.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A05D48F0A2C022FC128C8B3E /* edit_unit.cpp */; };
B508D193100146E300B12852 /* engine_fai.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B508D191100146E300B12852 /* engine_fai.cpp */; };
B513B2290ED36BFB0006E551 /* libcairo.2.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = B513B2270ED36BFB0006E551 /* libcairo.2.dylib */; };
@ -1284,6 +1290,8 @@
D09A4D40A36568E32D8723F7 /* combobox.hpp in Headers */ = {isa = PBXBuildFile; fileRef = B3534BCB9BB2673B5E513D67 /* combobox.hpp */; };
D1254FCA82471825B83AA786 /* multiline_text.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 0B0F48CE9CF65D9813BE6CDC /* multiline_text.cpp */; };
D2E9440BBCDCE2A75C93F85D /* migrate_version_selection.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F7B1444E9B79504502208B82 /* migrate_version_selection.cpp */; };
DC764C9F94D8B634B47A92B0 /* rich_label.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E5E2430098E9A628933A1DB1 /* rich_label.cpp */; };
DDA14069BCE29DE0FE71B970 /* gui_test_dialog.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 7FBD4033B4B52E9424819B5F /* gui_test_dialog.cpp */; };
DF974010BB46B844E83F7DDA /* tod_new_schedule.cpp in Sources */ = {isa = PBXBuildFile; fileRef = C61F473D9AC43768A445E218 /* tod_new_schedule.cpp */; };
E1DA41878F0C255769B8239D /* scroll_text.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 755D4555A1DEA29125E7F338 /* scroll_text.cpp */; };
E2F24C0CBC863C3DC8A041EF /* edit_pbl.hpp in Headers */ = {isa = PBXBuildFile; fileRef = B2CC45FEA71445AE817CAA6B /* edit_pbl.hpp */; };
@ -1439,6 +1447,7 @@
ECFB9FA8193BFAD900146ED0 /* carryover.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ECFB9FA7193BFAD900146ED0 /* carryover.cpp */; };
ECFB9FAA193BFB4B00146ED0 /* game_board.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ECFB9FA9193BFB4B00146ED0 /* game_board.cpp */; };
ECFB9FAC193BFB6E00146ED0 /* rect.cpp in Sources */ = {isa = PBXBuildFile; fileRef = ECFB9FAB193BFB6E00146ED0 /* rect.cpp */; };
F00C4D628A6DEFF4F2A66243 /* rich_label.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E5E2430098E9A628933A1DB1 /* rich_label.cpp */; };
F13D4C33BAA4CB9E9AACFCC2 /* spinner.cpp in Sources */ = {isa = PBXBuildFile; fileRef = E4214F3DA80B54080C4B548F /* spinner.cpp */; };
F40A13BC1A3A88BA00C4D071 /* apple_notification.mm in Sources */ = {isa = PBXBuildFile; fileRef = F40A13BB1A3A88BA00C4D071 /* apple_notification.mm */; };
F419A1F414E21246002F9ADC /* game_end_exceptions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = F419A1F314E21246002F9ADC /* game_end_exceptions.cpp */; };
@ -1573,6 +1582,7 @@
000000000000000000000009 /* network_download_file.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = network_download_file.cpp; sourceTree = "<group>"; };
000000000000000000000010 /* network_download_file.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = network_download_file.hpp; sourceTree = "<group>"; };
00574699A982AA23F12B39E0 /* edit_pbl_translation.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = edit_pbl_translation.cpp; sourceTree = "<group>"; };
0110429EAA81AED07D53B749 /* gui_test_dialog.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = gui_test_dialog.hpp; path = gui_test_dialog.hpp; sourceTree = "<group>"; };
09A440B1A671C45BE2924FB4 /* carryover_show_gold.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = carryover_show_gold.cpp; sourceTree = "<group>"; };
0B0F48CE9CF65D9813BE6CDC /* multiline_text.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = multiline_text.cpp; path = multiline_text.cpp; sourceTree = "<group>"; };
1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
@ -2209,6 +2219,7 @@
6FA542D78393E8FF067775DA /* edit_pbl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = edit_pbl.cpp; sourceTree = "<group>"; };
755D4555A1DEA29125E7F338 /* scroll_text.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = scroll_text.cpp; path = scroll_text.cpp; sourceTree = "<group>"; };
7CF14AB694764953E2CB3AF7 /* test_schema_validator.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = test_schema_validator.cpp; path = test_schema_validator.cpp; sourceTree = "<group>"; };
7FBD4033B4B52E9424819B5F /* gui_test_dialog.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = gui_test_dialog.cpp; path = gui_test_dialog.cpp; sourceTree = "<group>"; };
84234C54BB84519421FD4136 /* general.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = general.cpp; sourceTree = "<group>"; };
875E45698F8A8D5B750E7317 /* combobox.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = combobox.cpp; path = combobox.cpp; sourceTree = "<group>"; };
8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -2735,11 +2746,13 @@
BF3B41AF9FEBC9C3A11736A2 /* tab_container.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = tab_container.hpp; path = tab_container.hpp; sourceTree = "<group>"; };
C61F473D9AC43768A445E218 /* tod_new_schedule.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = tod_new_schedule.cpp; sourceTree = "<group>"; };
C679447D91FD3623CC852FF8 /* edit_pbl_translation.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = edit_pbl_translation.hpp; sourceTree = "<group>"; };
C8FF4A8788B6B7E6BE7AB7BB /* rich_label.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = rich_label.hpp; path = rich_label.hpp; sourceTree = "<group>"; };
D4594633BF3F8A06D6AE752F /* prompt.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = prompt.hpp; sourceTree = "<group>"; };
D911474D925FA88D5B856A0E /* test_sdl.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = test_sdl.cpp; path = test_sdl.cpp; sourceTree = "<group>"; };
D9A141EAAE90E98B6F6171D6 /* choose_addon.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = choose_addon.hpp; sourceTree = "<group>"; };
DA034C90BB2E6C060B0A0B93 /* back_edge_detector.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = back_edge_detector.hpp; path = back_edge_detector.hpp; sourceTree = "<group>"; };
E4214F3DA80B54080C4B548F /* spinner.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = spinner.cpp; path = spinner.cpp; sourceTree = "<group>"; };
E5E2430098E9A628933A1DB1 /* rich_label.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = rich_label.cpp; path = rich_label.cpp; sourceTree = "<group>"; };
EC0341DF1ECF46FE000F2E2B /* config_attribute_value.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = config_attribute_value.cpp; sourceTree = "<group>"; };
EC0341E01ECF46FE000F2E2B /* config_attribute_value.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = config_attribute_value.hpp; sourceTree = "<group>"; };
EC0680231EA920A300EEE03B /* random_deterministic.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = random_deterministic.cpp; sourceTree = "<group>"; };
@ -3860,6 +3873,8 @@
D4594633BF3F8A06D6AE752F /* prompt.hpp */,
F7B1444E9B79504502208B82 /* migrate_version_selection.cpp */,
B3DE4F95AF72C6F6BC37E695 /* migrate_version_selection.hpp */,
7FBD4033B4B52E9424819B5F /* gui_test_dialog.cpp */,
0110429EAA81AED07D53B749 /* gui_test_dialog.hpp */,
);
path = dialogs;
sourceTree = "<group>";
@ -4111,6 +4126,8 @@
B3534BCB9BB2673B5E513D67 /* combobox.hpp */,
162C4B1E9F7373592D0F3B89 /* tab_container.cpp */,
BF3B41AF9FEBC9C3A11736A2 /* tab_container.hpp */,
E5E2430098E9A628933A1DB1 /* rich_label.cpp */,
C8FF4A8788B6B7E6BE7AB7BB /* rich_label.hpp */,
);
path = widgets;
sourceTree = "<group>";
@ -5155,6 +5172,8 @@
8E1D442FB4DA43385F58F77F /* combobox.hpp in Headers */,
B7B34687A61290490C1616D3 /* tab_container.hpp in Headers */,
365D4F89BD511BC074E639D7 /* migrate_version_selection.hpp in Headers */,
3DED4C43AC1B737C15549CEC /* rich_label.hpp in Headers */,
9C6342BC8A95B6D23D384486 /* gui_test_dialog.hpp in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -5171,6 +5190,8 @@
D09A4D40A36568E32D8723F7 /* combobox.hpp in Headers */,
3CC6488695A7293C9CFC2CB6 /* tab_container.hpp in Headers */,
7A0347D48BDB52B1430D9E79 /* migrate_version_selection.hpp in Headers */,
022640C59A7BD907C810FA99 /* rich_label.hpp in Headers */,
6A1F44688986D07EB5DBEF75 /* gui_test_dialog.hpp in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -5928,6 +5949,8 @@
1C3D48879EAC414AE3DB122E /* combobox.cpp in Sources */,
1BC74FED857215A162E9E0F2 /* tab_container.cpp in Sources */,
D2E9440BBCDCE2A75C93F85D /* migrate_version_selection.cpp in Sources */,
F00C4D628A6DEFF4F2A66243 /* rich_label.cpp in Sources */,
805143B8BABF92CA79BEC8F5 /* gui_test_dialog.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -6614,6 +6637,8 @@
9C2743DE8100448B66F7E0AF /* combobox.cpp in Sources */,
19B14238AD52EC06ED2094F1 /* tab_container.cpp in Sources */,
4E4A4DBC9F44444A5867EC2B /* migrate_version_selection.cpp in Sources */,
DC764C9F94D8B634B47A92B0 /* rich_label.cpp in Sources */,
DDA14069BCE29DE0FE71B970 /* gui_test_dialog.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -16,6 +16,7 @@ gui/widgets/multimenu_button.cpp
gui/widgets/panel.cpp
gui/widgets/password_box.cpp
gui/widgets/progress_bar.cpp
gui/widgets/rich_label.cpp
gui/widgets/repeating_button.cpp
gui/widgets/scroll_label.cpp
gui/widgets/scroll_text.cpp

View File

@ -211,6 +211,7 @@ gui/dialogs/game_save.cpp
gui/dialogs/game_stats.cpp
gui/dialogs/game_version_dialog.cpp
gui/dialogs/gamestate_inspector.cpp
gui/dialogs/gui_test_dialog.cpp
gui/dialogs/help_browser.cpp
gui/dialogs/hotkey_bind.cpp
gui/dialogs/label_settings.cpp
@ -273,7 +274,6 @@ gui/widgets/generator.cpp
gui/widgets/grid.cpp
gui/widgets/helper.cpp
gui/widgets/pane.cpp
gui/widgets/scroll_text.cpp
gui/widgets/scrollbar.cpp
gui/widgets/scrollbar_container.cpp
gui/widgets/settings.cpp

View File

@ -445,7 +445,7 @@ public:
optional_config_impl<const config> get_deprecated_child(config_key_type old_key, const std::string& in_tag, DEP_LEVEL level, const std::string& message) const;
/**
* Get a deprecated child rangw and log a deprecation message
* Get a deprecated child range and log a deprecation message
* @param old_key The deprecated child to return if present
* @param in_tag The name of the tag this child appears in
* @param level The deprecation level

View File

@ -29,7 +29,8 @@ const color_t
PETRIFIED_COLOR {160, 160, 160},
TITLE_COLOR {186, 172, 125},
LABEL_COLOR {107, 140, 255},
BIGMAP_COLOR {255, 255, 255};
BIGMAP_COLOR {255, 255, 255},
BLUE_COLOR {0 , 0 , 255};
const color_t DISABLED_COLOR = PETRIFIED_COLOR.inverse();
@ -42,4 +43,38 @@ const color_t
inactive_ability_color {146, 146, 146},
unit_type_color {245, 230, 193},
race_color {166, 146, 117};
color_t string_to_color(const std::string &cmp_str)
{
// TODO needs a more generic mechanism so that more common color names are recognized
if (cmp_str == "green") {
return font::GOOD_COLOR;
}
if (cmp_str == "red") {
return font::BAD_COLOR;
}
if (cmp_str == "black") {
return font::BLACK_COLOR;
}
if (cmp_str == "yellow") {
return font::YELLOW_COLOR;
}
if (cmp_str == "white") {
return font::BIGMAP_COLOR;
}
if (cmp_str == "blue") {
return font::BLUE_COLOR;
}
if (cmp_str.at(0) == '#' && cmp_str.size() == 7) {
// #rrggbb color, pango format.
return color_t::from_hex_string(cmp_str.substr(1));
} else if (cmp_str.size() == 6) {
// rrggbb color, wesnoth format
return color_t::from_hex_string(cmp_str);
}
return font::NORMAL_COLOR;
}
} // namespace font

View File

@ -48,4 +48,11 @@ extern const color_t
inactive_ability_color,
unit_type_color,
race_color;
/**
* Return the color the string represents. Return font::NORMAL_COLOR if
* the string is empty or can't be matched against any other color.
*/
color_t string_to_color(const std::string &s);
}

View File

@ -84,9 +84,16 @@ pango_text::pango_text()
, maximum_length_(std::string::npos)
, calculation_dirty_(true)
, length_(0)
, attribute_start_offset_(0)
, attribute_end_offset_(0)
, highlight_color_()
, attrib_hash_(0)
, pixel_scale_(1)
, surface_buffer_()
{
// Initialize global list
global_attribute_list_ = pango_attr_list_new();
// With 72 dpi the sizes are the same as with SDL_TTF so hardcoded.
pango_cairo_context_set_resolution(context_.get(), 72.0);
@ -342,6 +349,184 @@ point pango_text::get_column_line(const point& position) const
}
}
void pango_text::add_attribute_size(const unsigned start_offset, const unsigned end_offset, int size)
{
attribute_start_offset_ = start_offset;
attribute_end_offset_ = end_offset;
if (attribute_start_offset_ != attribute_end_offset_) {
PangoAttribute *attr = pango_attr_size_new_absolute(PANGO_SCALE * size);
attr->start_index = attribute_start_offset_;
attr->end_index = attribute_end_offset_;
DBG_GUI_D << "attribute: size";
DBG_GUI_D << "attribute start: " << start_offset << " end : " << end_offset;
// Update hash
boost::hash_combine(attrib_hash_, attribute_start_offset_);
boost::hash_combine(attrib_hash_, attribute_end_offset_);
boost::hash_combine(attrib_hash_, size);
// Insert all attributes
pango_attr_list_insert(global_attribute_list_, attr);
}
}
void pango_text::add_attribute_weight(const unsigned start_offset, const unsigned end_offset, PangoWeight weight)
{
attribute_start_offset_ = start_offset;
attribute_end_offset_ = end_offset;
if (attribute_start_offset_ != attribute_end_offset_) {
PangoAttribute *attr = pango_attr_weight_new(weight);
attr->start_index = attribute_start_offset_;
attr->end_index = attribute_end_offset_;
DBG_GUI_D << "attribute: weight";
DBG_GUI_D << "attribute start: " << start_offset << " end : " << end_offset;
// Update hash
boost::hash_combine(attrib_hash_, attribute_start_offset_);
boost::hash_combine(attrib_hash_, attribute_end_offset_);
boost::hash_combine(attrib_hash_, weight);
// Insert all attributes
pango_attr_list_insert(global_attribute_list_, attr);
}
}
void pango_text::add_attribute_style(const unsigned start_offset, const unsigned end_offset, PangoStyle style)
{
attribute_start_offset_ = start_offset;
attribute_end_offset_ = end_offset;
if (attribute_start_offset_ != attribute_end_offset_) {
PangoAttribute *attr = pango_attr_style_new(style);
attr->start_index = attribute_start_offset_;
attr->end_index = attribute_end_offset_;
DBG_GUI_D << "attribute: style";
DBG_GUI_D << "attribute start: " << attribute_start_offset_ << " end : " << attribute_end_offset_;
// Update hash
boost::hash_combine(attrib_hash_, attribute_start_offset_);
boost::hash_combine(attrib_hash_, attribute_end_offset_);
// Insert all attributes
pango_attr_list_insert(global_attribute_list_, attr);
}
}
void pango_text::add_attribute_underline(const unsigned start_offset, const unsigned end_offset, PangoUnderline underline)
{
attribute_start_offset_ = start_offset;
attribute_end_offset_ = end_offset;
if (attribute_start_offset_ != attribute_end_offset_) {
PangoAttribute *attr = pango_attr_underline_new(underline);
attr->start_index = attribute_start_offset_;
attr->end_index = attribute_end_offset_;
DBG_GUI_D << "attribute: underline";
DBG_GUI_D << "attribute start: " << start_offset << " end : " << end_offset;
// Update hash
boost::hash_combine(attrib_hash_, attribute_start_offset_);
boost::hash_combine(attrib_hash_, attribute_end_offset_);
boost::hash_combine(attrib_hash_, underline);
// Insert all attributes
pango_attr_list_insert(global_attribute_list_, attr);
}
}
void pango_text::add_attribute_fg_color(const unsigned start_offset, const unsigned end_offset, const color_t& color)
{
attribute_start_offset_ = start_offset;
attribute_end_offset_ = end_offset;
if (attribute_start_offset_ != attribute_end_offset_) {
int col_r = color.r / 255.0 * 65535.0;
int col_g = color.g / 255.0 * 65535.0;
int col_b = color.b / 255.0 * 65535.0;
PangoAttribute *attr = pango_attr_foreground_new(col_r, col_g, col_b);
attr->start_index = start_offset;
attr->end_index = end_offset;
DBG_GUI_D << "attribute: fg color";
DBG_GUI_D << "attribute start: " << attribute_start_offset_ << " end : " << attribute_end_offset_;
DBG_GUI_D << "color: " << col_r << "," << col_g << "," << col_b;
// Update hash
boost::hash_combine(attrib_hash_, attribute_start_offset_);
boost::hash_combine(attrib_hash_, attribute_end_offset_);
boost::hash_combine(attrib_hash_, color.to_rgba_bytes());
// Insert all attributes
pango_attr_list_insert(global_attribute_list_, attr);
}
}
void pango_text::add_attribute_font_family(const unsigned start_offset, const unsigned end_offset, std::string family)
{
attribute_start_offset_ = start_offset;
attribute_end_offset_ = end_offset;
if (attribute_start_offset_ != attribute_end_offset_) {
PangoAttribute *attr = pango_attr_family_new(family.c_str());
attr->start_index = attribute_start_offset_;
attr->end_index = attribute_end_offset_;
DBG_GUI_D << "attribute: font family";
DBG_GUI_D << "attribute start: " << start_offset << " end : " << end_offset;
DBG_GUI_D << "font family: " << family;
// Update hash
boost::hash_combine(attrib_hash_, attribute_start_offset_);
boost::hash_combine(attrib_hash_, attribute_end_offset_);
boost::hash_combine(attrib_hash_, family);
// Insert all attributes
pango_attr_list_insert(global_attribute_list_, attr);
}
}
void pango_text::set_highlight_area(const unsigned start_offset, const unsigned end_offset, const color_t& color) {
attribute_start_offset_ = start_offset;
attribute_end_offset_ = end_offset;
highlight_color_ = color;
if (attribute_start_offset_ != attribute_end_offset_) {
// Highlight
int col_r = highlight_color_.r / 255.0 * 65535.0;
int col_g = highlight_color_.g / 255.0 * 65535.0;
int col_b = highlight_color_.b / 255.0 * 65535.0;
DBG_GUI_D << "highlight start: " << attribute_start_offset_ << "end : " << attribute_end_offset_;
DBG_GUI_D << "highlight color: " << col_r << "," << col_g << "," << col_b;
PangoAttribute *attr = pango_attr_background_new(col_r, col_g, col_b);
attr->start_index = attribute_start_offset_;
attr->end_index = attribute_end_offset_;
// Update hash
boost::hash_combine(attrib_hash_, attribute_start_offset_);
boost::hash_combine(attrib_hash_, attribute_end_offset_);
boost::hash_combine(attrib_hash_, highlight_color_.to_rgba_bytes());
// Insert all attributes
pango_attr_list_insert(global_attribute_list_, attr);
}
}
void pango_text::clear_attribute_list() {
global_attribute_list_ = pango_attr_list_new();
pango_layout_set_attributes(layout_.get(), global_attribute_list_);
}
bool pango_text::set_text(const std::string& text, const bool markedup)
{
if(markedup != markedup_text_ || text != text_) {
@ -357,28 +542,16 @@ bool pango_text::set_text(const std::string& text, const bool markedup)
<< "' contains invalid utf-8, trimmed the invalid parts.";
}
if (highlight_start_offset_ != highlight_end_offset_) {
/** Highlight */
PangoAttrList *attribute_list = pango_attr_list_new();
int col_r = highlight_color_.r / 255.0 * 65535.0;
int col_g = highlight_color_.g / 255.0 * 65535.0;
int col_b = highlight_color_.b / 255.0 * 65535.0;
DBG_GUI_D << "highlight start : " << highlight_start_offset_ << "end : " << highlight_end_offset_;
DBG_GUI_D << "highlight rgb : " << col_r << "," << col_g << "," << col_b;
PangoAttribute *attr = pango_attr_background_new(col_r, col_g, col_b);
attr->start_index = highlight_start_offset_;
attr->end_index = highlight_end_offset_;
pango_attr_list_insert(attribute_list, attr);
pango_layout_set_attributes(layout_.get(), attribute_list);
}
pango_layout_set_attributes(layout_.get(), global_attribute_list_);
// Clear list. Using pango_attr_list_unref() causes segfault
global_attribute_list_ = pango_attr_list_new();
if(markedup) {
if(!this->set_markup(narrow, *layout_)) {
return false;
}
} else {
if (highlight_start_offset_ == highlight_end_offset_) {
if (attribute_start_offset_ == attribute_end_offset_) {
/*
* pango_layout_set_text after pango_layout_set_markup might
* leave the layout in an undefined state regarding markup so
@ -1034,9 +1207,9 @@ std::size_t hash<font::pango_text>::operator()(const font::pango_text& t) const
boost::hash_combine(hash, t.alignment_);
boost::hash_combine(hash, t.ellipse_mode_);
boost::hash_combine(hash, t.add_outline_);
boost::hash_combine(hash, t.highlight_start_offset_);
boost::hash_combine(hash, t.highlight_end_offset_);
boost::hash_combine(hash, t.highlight_color_.to_rgba_bytes());
// Hash for the global attribute list
boost::hash_combine(hash, t.attrib_hash_);
return hash;
}

View File

@ -305,12 +305,17 @@ public:
* @param end_offset Column offset of the cursor where selection/highlight ends
* @param color Highlight color
*/
void set_highlight_area(const unsigned start_offset, const unsigned end_offset, const color_t& color)
{
highlight_start_offset_ = start_offset;
highlight_end_offset_ = end_offset;
highlight_color_ = color;
}
void set_highlight_area(const unsigned start_offset, const unsigned end_offset, const color_t& color);
void add_attribute_weight(const unsigned start_offset, const unsigned end_offset, PangoWeight weight);
void add_attribute_style(const unsigned start_offset, const unsigned end_offset, PangoStyle style);
void add_attribute_underline(const unsigned start_offset, const unsigned end_offset, PangoUnderline underline);
void add_attribute_fg_color(const unsigned start_offset, const unsigned end_offset, const color_t& color);
void add_attribute_size(const unsigned start_offset, const unsigned end_offset, int size);
void add_attribute_font_family(const unsigned start_offset, const unsigned end_offset, std::string family);
/** Clear all attributes */
void clear_attribute_list();
private:
@ -409,10 +414,19 @@ private:
/** Length of the text. */
mutable std::size_t length_;
unsigned highlight_start_offset_;
unsigned highlight_end_offset_;
unsigned attribute_start_offset_;
unsigned attribute_end_offset_;
color_t highlight_color_;
/**
* Global pango attribute list. All attributes in this list
* will be applied one by one to the text
*/
PangoAttrList* global_attribute_list_;
/** Hash for the global_attribute_list_ */
std::size_t attrib_hash_;
/** The pixel scale, used to render high-DPI text. */
int pixel_scale_;

View File

@ -31,6 +31,7 @@
#include "gui/auxiliary/typed_formula.hpp"
#include "gui/core/log.hpp"
#include "gui/widgets/helper.hpp"
#include "font/standard_colors.hpp"
#include "picture.hpp"
#include "sdl/point.hpp"
#include "sdl/rect.hpp"
@ -39,6 +40,8 @@
#include "video.hpp" // read_pixels_low_res, only used for blurring
#include "wml_exception.hpp"
#include <iostream>
namespace gui2
{
@ -326,9 +329,23 @@ void image_shape::draw(wfl::map_formula_callable& variables)
local_variables.add("clip_x", wfl::variant(x));
local_variables.add("clip_y", wfl::variant(y));
if (variables.has_key("fake_draw") && variables.query_value("fake_draw").as_bool()) {
variables.add("image_original_width", wfl::variant(tex.w()));
variables.add("image_original_height", wfl::variant(tex.h()));
variables.add("image_width", wfl::variant(w ? w : tex.w()));
variables.add("image_height", wfl::variant(h ? h : tex.h()));
return;
}
// Execute the provided actions for this context.
wfl::variant(variables.fake_ptr()).execute_variant(actions_formula_.evaluate(local_variables));
// Useful for relative, grid like positioning
variables.add("item_x", wfl::variant(x));
variables.add("item_y", wfl::variant(y));
variables.add("item_width", wfl::variant(w ? w : tex.w()));
variables.add("item_height", wfl::variant(h ? h : tex.h()));
// If w or h is 0, assume it means the whole image.
if (!w) { w = tex.w(); }
if (!h) { h = tex.h(); }
@ -409,12 +426,17 @@ text_shape::text_shape(const config& cfg, wfl::action_function_symbol_table& fun
, maximum_width_(cfg["maximum_width"], -1)
, characters_per_line_(cfg["text_characters_per_line"])
, maximum_height_(cfg["maximum_height"], -1)
, highlight_start_(cfg["highlight_start"], 0)
, highlight_end_(cfg["highlight_end"], 0)
, highlight_start_(cfg["highlight_start"])
, highlight_end_(cfg["highlight_end"])
, highlight_color_(cfg["highlight_color"], color_t::from_hex_string("215380"))
, attr_start_(cfg["attr_start"])
, attr_end_(cfg["attr_end"])
, attr_name_(cfg["attr_name"])
, attr_data_(cfg["attr_data"])
, outline_(cfg["outline"], false)
, actions_formula_(cfg["actions"], &functions)
{
if(!font_size_.has_formula()) {
VALIDATE(font_size_(), _("Text has a font size of 0."));
}
@ -440,8 +462,72 @@ void text_shape::draw(wfl::map_formula_callable& variables)
}
font::pango_text& text_renderer = font::get_text_renderer();
text_renderer.clear_attribute_list();
text_renderer.set_highlight_area(highlight_start_(variables), highlight_end_(variables), highlight_color_(variables));
std::vector<std::string> starts = utils::split(highlight_start_, ',');
std::vector<std::string> stops = utils::split(highlight_end_, ',');
for(size_t i = 0; i < std::min(starts.size(), stops.size()); i++) {
typed_formula<int> hstart(starts.at(i));
typed_formula<int> hstop(stops.at(i));
text_renderer.set_highlight_area(hstart(variables), hstop(variables), highlight_color_(variables));
}
// TODO check the strings before parsing them
starts = utils::split(attr_start_, ',');
stops = utils::split(attr_end_, ',');
std::vector<std::string> styles = utils::split(attr_name_, ',');
std::vector<std::string> data = utils::split(attr_data_, ',');
for(size_t i = 0, data_index = 0; i < std::min(starts.size(), stops.size()); i++) {
if (styles.at(i).empty()) {
continue;
}
typed_formula<int> attr_start(starts.at(i));
typed_formula<int> attr_stop(stops.at(i));
// Note that the value corresponding to the attribute is not the i-th item
// but rather the data_index-th one. Using data_index so that we can get rid of excess commas
// that is, things like 'attr_data=value1,,,valu2,value3'
if (styles.at(i) == "color"||styles.at(i) == "fgcolor"||styles.at(i) == "foreground") {
text_renderer.add_attribute_fg_color(attr_start(variables), attr_stop(variables), font::string_to_color(data.at(data_index)));
data_index++;
} else if (styles.at(i) == "bgcolor"||styles.at(i) == "background") {
text_renderer.set_highlight_area(attr_start(variables), attr_stop(variables), font::string_to_color(data.at(data_index)));
data_index++;
} else if (styles.at(i) == "font_size"||styles.at(i) == "size") {
text_renderer.add_attribute_size(attr_start(variables), attr_stop(variables), std::stoi(data.at(data_index)));
data_index++;
} else if (styles.at(i) == "font_family"||styles.at(i) == "face") {
text_renderer.add_attribute_font_family(attr_start(variables), attr_stop(variables), data.at(data_index));
data_index++;
} else if (styles.at(i) == "weight") {
text_renderer.add_attribute_weight(attr_start(variables), attr_stop(variables), decode_text_weight(data.at(data_index)));
data_index++;
} else if (styles.at(i) == "style") {
text_renderer.add_attribute_style(attr_start(variables), attr_stop(variables), decode_text_style(data.at(data_index)));
data_index++;
} else {
font::pango_text::FONT_STYLE attr_style = decode_font_style(styles.at(i));
switch(attr_style)
{
case font::pango_text::STYLE_BOLD:
text_renderer.add_attribute_weight(attr_start(variables), attr_stop(variables), PANGO_WEIGHT_BOLD);
break;
case font::pango_text::STYLE_ITALIC:
text_renderer.add_attribute_style(attr_start(variables), attr_stop(variables), PANGO_STYLE_ITALIC);
break;
case font::pango_text::STYLE_UNDERLINE:
text_renderer.add_attribute_underline(attr_start(variables), attr_stop(variables), PANGO_UNDERLINE_SINGLE);
break;
default:
// Unsupported formatting or normal text
text_renderer.add_attribute_weight(attr_start(variables), attr_stop(variables), PANGO_WEIGHT_NORMAL);
text_renderer.add_attribute_style(attr_start(variables), attr_stop(variables), PANGO_STYLE_NORMAL);
}
}
}
text_renderer
.set_link_aware(link_aware_(variables))
@ -468,6 +554,12 @@ void text_shape::draw(wfl::map_formula_callable& variables)
local_variables.add("text_width", wfl::variant(tw));
local_variables.add("text_height", wfl::variant(th));
if (variables.has_key("fake_draw") && variables.query_value("fake_draw").as_bool()) {
variables.add("text_width", wfl::variant(tw));
variables.add("text_height", wfl::variant(th));
return;
}
const int x = x_(local_variables);
const int y = y_(local_variables);
const int w = w_(local_variables);
@ -477,6 +569,12 @@ void text_shape::draw(wfl::map_formula_callable& variables)
// Execute the provided actions for this context.
wfl::variant(variables.fake_ptr()).execute_variant(actions_formula_.evaluate(local_variables));
// Useful for relative, grid like positioning
variables.add("item_x", wfl::variant(x));
variables.add("item_y", wfl::variant(y));
variables.add("item_width", wfl::variant(tw));
variables.add("item_height", wfl::variant(th));
texture tex = text_renderer.render_and_get_texture();
if(!tex) {
DBG_GUI_D << "Text: Rendering '" << text << "' resulted in an empty canvas, leave.";

View File

@ -27,6 +27,7 @@
#include "sdl/texture.hpp"
#include "sdl/rect.hpp"
namespace wfl { class variant; }
struct point;
namespace gui2
@ -155,6 +156,11 @@ public:
variables_.add(key, std::move(value));
}
wfl::variant get_variable(const std::string& key)
{
return variables_.query_value(key);
}
private:
/** Vector with the shapes to draw. */
std::vector<std::unique_ptr<shape>> shapes_;

View File

@ -283,12 +283,29 @@ private:
typed_formula<int> maximum_height_;
/** Start and end offsets for highlight */
typed_formula<int> highlight_start_;
typed_formula<int> highlight_end_;
std::string highlight_start_;
std::string highlight_end_;
/** The color to be used for highlighting */
typed_formula<color_t> highlight_color_;
/** Generic start and end offsets for various attributes */
std::string attr_start_;
std::string attr_end_;
/**
* The attribute type
* Possible values :
* color/foreground, bgcolor/background, font_size/size,
* bold, italic, underline
* The first three require extra data
* the color for the first two, and font size for the last
*/
std::string attr_name_;
/** extra data for the attribute, if any */
std::string attr_data_;
/** Whether to apply a text outline. */
typed_formula<bool> outline_;

View File

@ -0,0 +1,40 @@
/*
Copyright (C) 2023 - 2024
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "gui/dialogs/gui_test_dialog.hpp"
#include "gui/auxiliary/find_widget.hpp"
#include "gui/widgets/text_box.hpp"
namespace gui2::dialogs
{
REGISTER_DIALOG(gui_test_dialog)
gui_test_dialog::gui_test_dialog()
: modal_dialog(window_id())
{
}
void gui_test_dialog::pre_show(window& /*win*/)
{
}
void gui_test_dialog::post_show(window& /*win*/)
{
}
} // namespace gui2::dialogs

View File

@ -0,0 +1,46 @@
/*
Copyright (C) 2023 - 2024
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "gui/dialogs/modal_dialog.hpp"
namespace gui2
{
namespace dialogs
{
/**
* @ingroup GUIWindowDefinitionWML
*
* A test dialog for testing various gui2 features
*/
class gui_test_dialog : public modal_dialog
{
public:
gui_test_dialog();
/** The execute function. See @ref modal_dialog for more information. */
DEFINE_SIMPLE_EXECUTE_WRAPPER(gui_test_dialog)
private:
virtual void pre_show(window& /*window*/) override;
virtual void post_show(window& /*window*/) override;
virtual const std::string& window_id() const override;
};
} // namespace dialogs
} // namespace gui2

View File

@ -37,6 +37,7 @@
#include "gui/dialogs/preferences_dialog.hpp"
#include "gui/dialogs/screenshot_notification.hpp"
#include "gui/dialogs/simple_item_selector.hpp"
#include "gui/dialogs/gui_test_dialog.hpp"
#include "language.hpp"
#include "log.hpp"
#include "preferences/game.hpp"
@ -362,6 +363,17 @@ void title_screen::init_callbacks()
clock->set_visible(show_debug_clock_button ? widget::visibility::visible : widget::visibility::invisible);
}
//
// GUI Test and Debug Window
//
register_button(*this, "test_dialog", hotkey::HOTKEY_NULL,
std::bind(&title_screen::show_gui_test_dialog, this));
auto test_dialog = find_widget<button>(this, "test_dialog", false, false);
if(test_dialog) {
test_dialog->set_visible(show_debug_clock_button ? widget::visibility::visible : widget::visibility::invisible);
}
//
// Static labels (version and language)
//
@ -453,6 +465,11 @@ void title_screen::show_debug_clock_window()
}
}
void title_screen::show_gui_test_dialog()
{
gui2::dialogs::gui_test_dialog::execute();
}
void title_screen::hotkey_callback_select_tests()
{
game_config_manager::get()->load_game_config_for_create(false, true);

View File

@ -85,6 +85,9 @@ private:
/** Shows the debug clock. */
void show_debug_clock_window();
/** Shows the gui test window. */
void show_gui_test_dialog();
void hotkey_callback_select_tests();
void show_achievements();

View File

@ -55,6 +55,42 @@ color_t decode_color(const std::string& color)
return color_t::from_rgba_string(color);
}
PangoWeight decode_text_weight(const std::string& weight)
{
if(weight == "thin") {
return PANGO_WEIGHT_THIN;
} else if (weight == "light") {
return PANGO_WEIGHT_LIGHT;
} else if (weight == "semibold") {
return PANGO_WEIGHT_SEMIBOLD;
} else if (weight == "bold") {
return PANGO_WEIGHT_BOLD;
} else if (weight == "heavy") {
return PANGO_WEIGHT_HEAVY;
}
if(!weight.empty() && weight != "normal") {
ERR_GUI_E << "Invalid text weight '" << weight << "', falling back to 'normal'.";
}
return PANGO_WEIGHT_NORMAL;
}
PangoStyle decode_text_style(const std::string& style)
{
if(style == "italic") {
return PANGO_STYLE_ITALIC;
} else if(style == "oblique") {
return PANGO_STYLE_OBLIQUE;
}
if(!style.empty() && style != "normal") {
ERR_GUI_E << "Invalid text style '" << style << "', falling back to 'normal'.";
}
return PANGO_STYLE_NORMAL;
}
PangoAlignment decode_text_alignment(const std::string& alignment)
{
if(alignment == "center") {

View File

@ -51,6 +51,24 @@ color_t decode_color(const std::string& color);
*/
PangoAlignment decode_text_alignment(const std::string& alignment);
/**
* Converts a text weight string to a PangoWeight.
*
* @param weight A weight string, possible values: "thin", "light", "normal", "semibold", "bold", "heavy"
*
* @returns The corresponding PangoWeight.
*/
PangoWeight decode_text_weight(const std::string& weight);
/**
* Converts a text style string to a PangoStyle.
*
* @param style A style string, possible values: "normal", "italic", "oblique
*
* @returns The corresponding PangoStyle.
*/
PangoStyle decode_text_style(const std::string& style);
/**
* Converts a text alignment to its string representation.
*

View File

@ -0,0 +1,839 @@
/*
Copyright (C) 2024
by Subhraman Sarkar (babaissarkar) <suvrax@gmail.com>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#define GETTEXT_DOMAIN "wesnoth-lib"
#include "gui/widgets/rich_label.hpp"
#include "gui/core/log.hpp"
#include "gui/core/widget_definition.hpp"
#include "gui/core/register_widget.hpp"
#include "gui/dialogs/message.hpp"
#include "cursor.hpp"
#include "desktop/clipboard.hpp"
#include "desktop/open.hpp"
#include "help/help_impl.hpp"
#include "gettext.hpp"
#include "log.hpp"
#include "serialization/unicode.hpp"
#include "serialization/string_utils.hpp"
#include "wml_exception.hpp"
#include <functional>
#include <string>
#include <boost/format.hpp>
static lg::log_domain log_rich_label("gui/widget/rich_label");
#define DBG_GUI_RL LOG_STREAM(debug, log_rich_label)
namespace gui2
{
// ------------ WIDGET -----------{
REGISTER_WIDGET(rich_label)
rich_label::rich_label(const implementation::builder_rich_label& builder)
: styled_widget(builder, type())
, state_(ENABLED)
, can_wrap_(true)
, link_aware_(true)
, link_color_(font::YELLOW_COLOR)
, can_shrink_(true)
, text_alpha_(ALPHA_OPAQUE)
, unparsed_text_()
, w_(0)
, h_(0)
, x_(0)
, padding_(5)
, txt_height_(0)
, prev_blk_height_(0)
{
connect_signal<event::LEFT_BUTTON_CLICK>(
std::bind(&rich_label::signal_handler_left_button_click, this, std::placeholders::_3));
connect_signal<event::MOUSE_MOTION>(
std::bind(&rich_label::signal_handler_mouse_motion, this, std::placeholders::_3, std::placeholders::_5));
connect_signal<event::MOUSE_LEAVE>(
std::bind(&rich_label::signal_handler_mouse_leave, this, std::placeholders::_3));
}
wfl::map_formula_callable rich_label::setup_text_renderer(config text_cfg, unsigned width) {
// Set up fake render to calculate text position
wfl::action_function_symbol_table functions;
wfl::map_formula_callable variables;
variables.add("text", wfl::variant(text_cfg["text"].str()));
variables.add("width", wfl::variant(width > 0 ? width : w_));
variables.add("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
variables.add("fake_draw", wfl::variant(true));
tshape_ = std::make_unique<gui2::text_shape>(text_cfg, functions);
tshape_->draw(variables);
return variables;
}
point rich_label::get_text_size(config text_cfg, unsigned width) {
wfl::map_formula_callable variables = setup_text_renderer(text_cfg, width);
return point(variables.query_value("text_width").as_int(), variables.query_value("text_height").as_int());
}
point rich_label::get_image_size(config img_cfg) {
wfl::action_function_symbol_table functions;
wfl::map_formula_callable variables;
variables.add("fake_draw", wfl::variant(true));
ishape_ = std::make_unique<gui2::image_shape>(img_cfg, functions);
ishape_->draw(variables);
return point(variables.query_value("image_width").as_int(), variables.query_value("image_height").as_int());
}
void rich_label::add_text_with_attribute(config& curr_item, std::string text, std::string attr_name, std::string extra_data) {
size_t start = curr_item["text"].str().size();
curr_item["text"] = curr_item["text"].str() + text;
if (!attr_name.empty()) {
append_if_not_empty(&curr_item["attr_name"], ",");
curr_item["attr_name"] = curr_item["attr_name"].str() + attr_name;
append_if_not_empty(&curr_item["attr_start"], ",");
curr_item["attr_start"] = curr_item["attr_start"].str() + std::to_string(start);
append_if_not_empty(&curr_item["attr_end"], ",");
curr_item["attr_end"] = curr_item["attr_end"].str() + std::to_string(curr_item["text"].str().size());
if (!extra_data.empty()) {
append_if_not_empty(&curr_item["attr_data"], ",");
curr_item["attr_data"] = curr_item["attr_data"].str() + extra_data;
}
}
}
void rich_label::add_text_with_attributes(config& curr_item, std::string text, std::vector<std::string> attr_names, std::vector<std::string> extra_data) {
size_t start = curr_item["text"].str().size();
curr_item["text"] = curr_item["text"].str() + text;
if (!attr_names.empty()) {
append_if_not_empty(&curr_item["attr_name"], ",");
curr_item["attr_name"] = curr_item["attr_name"].str() + utils::join(attr_names);
for (size_t i = 0; i < attr_names.size(); i++) {
append_if_not_empty(&curr_item["attr_start"], ",");
curr_item["attr_start"] = curr_item["attr_start"].str() + std::to_string(start);
append_if_not_empty(&curr_item["attr_end"], ",");
curr_item["attr_end"] = curr_item["attr_end"].str() + std::to_string(curr_item["text"].str().size());
}
if (!extra_data.empty()) {
append_if_not_empty(&curr_item["attr_data"], ",");
curr_item["attr_data"] = curr_item["attr_data"].str() + utils::join(extra_data);
}
}
}
void rich_label::add_image(config& curr_item, std::string name, std::string align, bool floating, point& img_size) {
curr_item["name"] = name;
if (align.empty()) {
align = "left";
}
if (align == "right") {
curr_item["x"] = floating ? "(width - image_width - img_x)" : "(width - image_width - pos_x)";
} else if (align == "middle" || align == "center") {
// works for single image only
curr_item["x"] = floating ? "(img_x + (width - image_width)/2.0)" : "(pos_x + (width - image_width)/2.0)";
} else {
// left aligned images are default for now
curr_item["x"] = floating ? "(img_x)" : "(pos_x)";
}
curr_item["y"] = floating ? "(img_y + pos_y)" : "(pos_y)";
curr_item["h"] = "(image_height)";
curr_item["w"] = "(image_width)";
// Sizing
if (floating) {
img_size.x = get_image_size(curr_item).x;
img_size.y += get_image_size(curr_item).y;
} else {
img_size.x += get_image_size(curr_item).x + padding_;
img_size.y = get_image_size(curr_item).y;
}
std::stringstream actions;
actions << "([";
if (floating) {
if (align == "left") {
x_ = img_size.x + padding_;
actions << "set_var('pos_x', image_width + padding)";
} else if (align == "right") {
x_ = 0;
actions << "set_var('pos_x', 0)";
actions << ",";
actions << "set_var('ww', image_width)";
}
img_size.y += padding_;
actions << "," << "set_var('img_y', img_y + image_height + padding)";
} else {
x_ = img_size.x;
actions << "set_var('pos_x', pos_x + image_width + padding)";
}
actions << "])";
curr_item["actions"] = actions.str();
actions.str("");
}
void rich_label::add_link(config& curr_item, std::string name, std::string dest, int img_width) {
// TODO algorithm needs to be text_alignment independent
DBG_GUI_RL << "add_link, x=" << x_ << " width=" << img_width;
setup_text_renderer(curr_item, w_ - x_ - img_width);
point t_start = get_xy_from_offset(utf8::size(curr_item["text"].str()));
DBG_GUI_RL << "link text start:" << t_start;
std::string link_text = name.empty() ? dest : name;
add_text_with_attribute(curr_item, link_text, "color", link_color_.to_hex_string().substr(1));
setup_text_renderer(curr_item, w_ - x_ - img_width);
point t_end = get_xy_from_offset(utf8::size(curr_item["text"].str()));
DBG_GUI_RL << "link text end:" << t_end;
point link_start(x_ + t_start.x, prev_blk_height_ + t_start.y);
t_end.y += font::get_max_height(font::SIZE_NORMAL);
// TODO link after right aligned images
// Add link
if (t_end.x > t_start.x) {
point link_size = t_end - t_start;
rect link_rect = {
link_start.x,
link_start.y,
link_size.x,
link_size.y,
};
links_.push_back(std::pair(link_rect, dest));
DBG_GUI_RL << "added link at rect: " << link_rect;
} else {
//link straddles two lines, break into two rects
point t_size(w_ - link_start.x - (x_ == 0 ? img_width : 0), t_end.y - t_start.y);
point link_start2(x_, link_start.y + font::get_max_height(font::SIZE_NORMAL));
point t_size2(t_end.x, t_end.y - t_start.y);
rect link_rect = {
link_start.x,
link_start.y,
t_size.x,
t_size.y,
};
rect link_rect2 = {
link_start2.x,
link_start2.y,
t_size2.x,
t_size2.y,
};
links_.push_back(std::pair(link_rect, dest));
links_.push_back(std::pair(link_rect2, dest));
DBG_GUI_RL << "added link at rect 1: " << link_rect;
DBG_GUI_RL << "added link at rect 2: " << link_rect2;
}
}
size_t rich_label::get_split_location(std::string text, int img_height) {
point wrap_position = get_column_line(point(w_, img_height));
size_t len = 0;
for (int i = 0; i < wrap_position.y; i++) {
len += utf8::size(font::get_text_renderer().get_lines()[i]);
}
len += wrap_position.x;
// break only at word boundary
char c;
while((c = text.at(len)) != ' ') {
len--;
}
return len;
}
void rich_label::set_label(const t_string& text)
{
// Initialization
w_ = (w_ == 0) ? styled_widget::calculate_best_size().x : w_;
DBG_GUI_RL << "Width: " << w_;
h_ = 0;
unparsed_text_ = text;
text_dom_.clear();
links_.clear();
help::topic_text marked_up_text(text);
std::vector<std::string> parsed_text = marked_up_text.parsed_text();
config* curr_item = nullptr;
optional_config_impl<config> child;
bool is_image = false;
bool floating = false;
bool new_text_block = false;
bool needs_size_update = true;
bool in_table = false;
point img_size;
unsigned col_width = 0;
unsigned max_col_height = 0;
prev_blk_height_ = 0;
txt_height_ = 0;
for (size_t i = 0; i < parsed_text.size(); i++) {
bool last_entry = (i == parsed_text.size() - 1);
std::string line = parsed_text.at(i);
if (!line.empty() && line.at(0) == '[') {
config cfg;
::read(cfg, line);
if ((child = cfg.optional_child("img"))) {
std::string name = child["src"];
floating = child["float"].to_bool();
std::string align = child["align"];
curr_item = &(text_dom_.add_child("image"));
add_image(*curr_item, name, align, floating, img_size);
is_image = true;
new_text_block = true;
DBG_GUI_RL << "image: src=" << name << ", size=" << get_image_size(*curr_item);
} else {
if (is_image && (!floating)) {
x_ = 0;
(*curr_item)["actions"] = "([set_var('pos_x', 0), set_var('pos_y', pos_y + image_height + padding)])";
}
if (curr_item == nullptr || new_text_block) {
prev_blk_height_ += txt_height_ + padding_;
txt_height_ = 0;
curr_item = &(text_dom_.add_child("text"));
default_text_config(curr_item);
new_text_block = false;
}
// }---------- TEXT TAGS -----------{
int tmp_h = get_text_size(*curr_item, w_ - x_).y;
if ((child = cfg.optional_child("ref"))) {
add_link(*curr_item, child["text"], child["dst"], img_size.x);
is_image = false;
DBG_GUI_RL << "ref: dst=" << child["dst"];
} else if ((child = cfg.optional_child("bold")) || (child = cfg.optional_child("b"))) {
add_text_with_attribute(*curr_item, child["text"], "bold");
is_image = false;
DBG_GUI_RL << "bold: text=" << child["text"];
} else if ((child = cfg.optional_child("italic")) || (child = cfg.optional_child("i"))) {
add_text_with_attribute(*curr_item, child["text"], "italic");
is_image = false;
DBG_GUI_RL << "italic: text=" << child["text"];
} else if ((child = cfg.optional_child("underline")) || (child = cfg.optional_child("u"))) {
add_text_with_attribute(*curr_item, child["text"], "underline");
is_image = false;
DBG_GUI_RL << "u: text=" << child["text"];
} else if ((child = cfg.optional_child("header")) || (child = cfg.optional_child("h"))) {
// Header starts in a new line
append_if_not_empty(&((*curr_item)["text"]), "\n");
append_if_not_empty(&((*curr_item)["attr_name"]), ",");
append_if_not_empty(&((*curr_item)["attr_start"]), ",");
append_if_not_empty(&((*curr_item)["attr_end"]), ",");
append_if_not_empty(&((*curr_item)["attr_data"]), ",");
std::stringstream header_text;
header_text << child["text"].str() + "\n";
std::vector<std::string> attrs = {"color", "size"};
std::vector<std::string> attr_data;
attr_data.push_back(font::TITLE_COLOR.to_hex_string().substr(1));
attr_data.push_back(std::to_string(font::SIZE_TITLE));
add_text_with_attributes((*curr_item), header_text.str(), attrs, attr_data);
is_image = false;
DBG_GUI_RL << "h: text=" << child["text"];
} else if ((child = cfg.optional_child("span")) || (child = cfg.optional_child("format"))) {
std::vector<std::string> attrs;
std::vector<std::string> attr_data;
DBG_GUI_RL << "span/format: text=" << child["text"];
DBG_GUI_RL << "attributes:";
for (const auto& attr : child.value().attribute_range()) {
if (attr.first != "text") {
attrs.push_back(attr.first);
attr_data.push_back(attr.second);
DBG_GUI_RL << attr.first << "=" << attr.second;
}
}
add_text_with_attributes((*curr_item), child["text"], attrs, attr_data);
is_image = false;
// }---------- TABLE TAGS -----------{
} else if ((child = cfg.optional_child("table"))) {
in_table = true;
// setup column width
unsigned columns = child["col"].to_int();
unsigned width = child["width"].to_int();
width = width > 0 ? width : w_;
col_width = width/columns;
// start on a new line
(*curr_item)["actions"] = boost::str(boost::format("([set_var('pos_x', 0), set_var('pos_y', pos_y + if(ih > text_height, ih, text_height)), set_var('tw', width - pos_x - %d), set_var('ih', 0)])") % col_width);
x_ = 0;
prev_blk_height_ += std::max(img_size.y, get_text_size(*curr_item, w_ - img_size.x).y);
txt_height_ = 0;
new_text_block = true;
DBG_GUI_RL << "start table : " << "col=" << columns;
DBG_GUI_RL << "col_width : " << col_width;
} else if (cfg.optional_child("jump")) {
if (col_width > 0) {
max_col_height = std::max(max_col_height, txt_height_);
max_col_height = std::max(max_col_height, static_cast<unsigned>(img_size.y));
txt_height_ = 0;
x_ += col_width;
DBG_GUI_RL << "jump to next column";
(*curr_item)["actions"] = boost::str(boost::format("([set_var('pos_x', pos_x + %d), set_var('tw', width - pos_x - %d)])") % col_width % col_width);
if (!is_image) {
new_text_block = true;
}
}
} else if (cfg.optional_child("break") || cfg.optional_child("br")) {
if (in_table) {
max_col_height = std::max(max_col_height, txt_height_);
max_col_height = std::max(max_col_height, static_cast<unsigned>(img_size.y));
//linebreak
x_ = 0;
prev_blk_height_ += max_col_height;
max_col_height = 0;
txt_height_ = 0;
(*curr_item)["actions"] = boost::str(boost::format("([set_var('pos_x', 0), set_var('pos_y', pos_y + %d + %d), set_var('tw', width - pos_x - %d)])") % max_col_height % padding_ % col_width);
}
DBG_GUI_RL << "linebreak: " << (in_table ? max_col_height : w_);
if (!is_image) {
new_text_block = true;
}
} else if (cfg.optional_child("endtable")) {
DBG_GUI_RL << "end table: " << max_col_height;
max_col_height = std::max(max_col_height, txt_height_);
max_col_height = std::max(max_col_height, static_cast<unsigned>(img_size.y));
(*curr_item)["actions"] = boost::str(boost::format("([set_var('pos_x', 0), set_var('pos_y', pos_y + %d), set_var('tw', 0)])") % max_col_height);
//linebreak and reset col_width
col_width = 0;
x_ = 0;
prev_blk_height_ += max_col_height;
max_col_height = 0;
txt_height_ = 0;
if (!last_entry) {
new_text_block = true;
}
in_table = false;
}
if (needs_size_update) {
int ah = get_text_size(*curr_item, w_ - x_).y;
// update text size and widget height
if (tmp_h > ah) {
tmp_h = 0;
}
txt_height_ += ah - tmp_h;
}
}
} else if (!line.empty()) {
DBG_GUI_RL << "text: text=" << line.substr(1, 20) << "...";
// Start the text in a new paragraph if a newline follows after an image
if (is_image && (!floating)) {
if ((line.at(0) == '\n')) {
x_ = 0;
(*curr_item)["actions"] = "([set_var('pos_x', 0), set_var('pos_y', pos_y + image_height + padding)])";
line = line.substr(1, line.size());
needs_size_update = true;
} else {
needs_size_update = false;
}
}
if (curr_item == nullptr || new_text_block) {
prev_blk_height_ += txt_height_ + padding_;
txt_height_ = 0;
curr_item = &(text_dom_.add_child("text"));
default_text_config(curr_item);
new_text_block = false;
}
(*curr_item)["font_size"] = font::SIZE_NORMAL;
int tmp_h = get_text_size(*curr_item, w_ - x_).y;
(*curr_item)["text"] = (*curr_item)["text"].str() + line;
point text_size;
text_size.x = get_text_size(*curr_item, w_ - (x_ == 0 ? img_size.x : x_)).x - x_;
text_size.y = get_text_size(*curr_item, w_ - (x_ == 0 ? img_size.x : x_)).y;
if ( floating && (img_size.y > 0) && (text_size.y > img_size.y) ) {
DBG_GUI_RL << "wrap start";
size_t len = get_split_location((*curr_item)["text"].str(), img_size.y);
t_string* removed_part = new t_string((*curr_item)["text"].str().substr(len+1));
(*curr_item)["text"] = (*curr_item)["text"].str().substr(0, len);
(*curr_item)["actions"] = "([set_var('pos_x', 0), set_var('ww', 0), set_var('pos_y', pos_y + text_height)])";
// New text block
x_ = 0;
prev_blk_height_ += img_size.y + padding_;
// TODO excess line gets added, so that needs to be compensated
txt_height_ = 0;
img_size = point(0,0);
floating = false;
curr_item = &(text_dom_.add_child("text"));
default_text_config(curr_item);
add_text_with_attribute(*curr_item, *removed_part);
} else if ((img_size.y > 0) && (text_size.y < img_size.y)) {
DBG_GUI_RL << "no wrap";
if (is_image) {
(*curr_item)["actions"] = "([set_var('pos_y', pos_y + image_height)])";
} else {
(*curr_item)["actions"] = "([set_var('pos_y', pos_y + text_height)])";
}
}
int ah = get_text_size(*curr_item, w_ - x_).y;
// update text size and widget height
if (tmp_h > ah) {
tmp_h = 0;
}
txt_height_ += ah - tmp_h;
is_image = false;
}
// Height Update
if (!is_image && !floating && img_size.y > 0) {
if (needs_size_update) {
prev_blk_height_ += img_size.y;
}
img_size = point(0,0);
}
DBG_GUI_RL << "Item :" << curr_item->debug();
DBG_GUI_RL << "X: " << x_;
DBG_GUI_RL << "Prev block height: " << prev_blk_height_ << " Current text block height: " << txt_height_;
DBG_GUI_RL << "Height: " << h_;
h_ = txt_height_ + prev_blk_height_;
// reset all variables to zero, otherwise they grow infinitely
if (last_entry) {
if (static_cast<unsigned>(img_size.y) > h_) {
h_ = img_size.y;
}
h_ += font::get_line_spacing_factor() * font::get_max_height(font::SIZE_NORMAL);
config& break_cfg = text_dom_.add_child("text");
default_text_config(&break_cfg);
break_cfg["text"] = " ";
break_cfg["actions"] = "([set_var('pos_x', 0), set_var('pos_y', 0), set_var('img_x', 0), set_var('img_y', 0)])";
}
DBG_GUI_RL << "-----------";
} // for loop ends
} // function ends
void rich_label::default_text_config(config* txt_ptr, t_string text) {
if (txt_ptr != nullptr) {
(*txt_ptr)["text"] = text;
(*txt_ptr)["font_size"] = font::SIZE_NORMAL;
(*txt_ptr)["text_alignment"] = encode_text_alignment(get_text_alignment());
(*txt_ptr)["x"] = "(pos_x)";
(*txt_ptr)["y"] = "(pos_y)";
(*txt_ptr)["w"] = "(text_width)";
(*txt_ptr)["h"] = "(text_height)";
// tw -> table width, used for wrapping text inside table cols
// ww -> wrap width, used for wrapping around floating image
(*txt_ptr)["maximum_width"] = "(width - pos_x - ww - tw)";
(*txt_ptr)["actions"] = "([set_var('pos_y', pos_y+text_height)])";
}
}
void rich_label::update_canvas()
{
for(canvas& tmp : get_canvases()) {
tmp.set_variable("pos_x", wfl::variant(0));
tmp.set_variable("pos_y", wfl::variant(0));
tmp.set_variable("img_x", wfl::variant(0));
tmp.set_variable("img_y", wfl::variant(0));
tmp.set_variable("tw", wfl::variant(0));
tmp.set_variable("ww", wfl::variant(0));
tmp.set_variable("padding", wfl::variant(padding_));
// Disable ellipsization so that text wrapping can work
tmp.set_variable("text_wrap_mode", wfl::variant(PANGO_ELLIPSIZE_NONE));
tmp.set_cfg(text_dom_, true);
tmp.set_variable("text_alpha", wfl::variant(text_alpha_));
}
}
void rich_label::set_text_alpha(unsigned short alpha)
{
if(alpha != text_alpha_) {
text_alpha_ = alpha;
update_canvas();
queue_redraw();
}
}
void rich_label::set_active(const bool active)
{
if(get_active() != active) {
set_state(active ? ENABLED : DISABLED);
}
}
void rich_label::set_link_aware(bool link_aware)
{
if(link_aware != link_aware_) {
link_aware_ = link_aware;
update_canvas();
queue_redraw();
}
}
void rich_label::set_link_color(const color_t& color)
{
if(color != link_color_) {
link_color_ = color;
update_canvas();
queue_redraw();
}
}
void rich_label::set_state(const state_t state)
{
if(state != state_) {
state_ = state;
queue_redraw();
}
}
void rich_label::signal_handler_left_button_click(bool& handled)
{
DBG_GUI_E << "rich_label click";
if(!get_link_aware()) {
return; // without marking event as "handled"
}
point mouse = get_mouse_position();
mouse.x -= get_x();
mouse.y -= get_y();
DBG_GUI_RL << "(mouse)" << mouse.x << "," << mouse.y;
DBG_GUI_RL << "link count :" << links_.size();
for (const auto& entry : links_) {
DBG_GUI_RL << "link [" << entry.first.x << "," << entry.first.y << ","
<< entry.first.x + entry.first.w << "," << entry.first.y + entry.first.h << "]";
if (entry.first.contains(mouse)) {
DBG_GUI_RL << "Clicked link! dst = " << entry.second;
if (link_handler_) {
link_handler_(entry.second);
} else {
DBG_GUI_RL << "No registered link handler found";
}
}
}
handled = true;
}
void rich_label::signal_handler_mouse_motion(bool& handled, const point& coordinate)
{
DBG_GUI_E << "rich_label mouse motion";
if(!get_link_aware()) {
return; // without marking event as "handled"
}
point mouse = coordinate;
mouse.x -= get_x();
mouse.y -= get_y();
for (const auto& entry : links_) {
if (entry.first.contains(mouse)) {
update_mouse_cursor(true);
handled = true;
return;
}
}
update_mouse_cursor(false);
}
void rich_label::signal_handler_mouse_leave(bool& handled)
{
DBG_GUI_E << "rich_label mouse leave";
if(!get_link_aware()) {
return; // without marking event as "handled"
}
// We left the widget, so just unconditionally reset the cursor
update_mouse_cursor(false);
handled = true;
}
void rich_label::update_mouse_cursor(bool enable)
{
// Someone else may set the mouse cursor for us to something unusual (e.g.
// the WAIT cursor) so we ought to mess with that only if it's set to
// NORMAL or HYPERLINK.
if(enable && cursor::get() == cursor::NORMAL) {
cursor::set(cursor::HYPERLINK);
} else if(!enable && cursor::get() == cursor::HYPERLINK) {
cursor::set(cursor::NORMAL);
}
}
// }---------- DEFINITION ---------{
rich_label_definition::rich_label_definition(const config& cfg)
: styled_widget_definition(cfg)
{
DBG_GUI_P << "Parsing rich_label " << id;
load_resolutions<resolution>(cfg);
}
rich_label_definition::resolution::resolution(const config& cfg)
: resolution_definition(cfg)
, link_color(cfg["link_color"].empty() ? font::YELLOW_COLOR : color_t::from_rgba_string(cfg["link_color"].str()))
{
// Note the order should be the same as the enum state_t is rich_label.hpp.
state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_enabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_enabled")));
state.emplace_back(VALIDATE_WML_CHILD(cfg, "state_disabled", missing_mandatory_wml_tag("rich_label_definition][resolution", "state_disabled")));
}
// }---------- BUILDER -----------{
namespace implementation
{
builder_rich_label::builder_rich_label(const config& cfg)
: builder_styled_widget(cfg)
, text_alignment(decode_text_alignment(cfg["text_alignment"]))
, link_aware(cfg["link_aware"].to_bool(true))
, width(cfg["width"].to_int(500))
{
}
std::unique_ptr<widget> builder_rich_label::build() const
{
auto lbl = std::make_unique<rich_label>(*this);
const auto conf = lbl->cast_config_to<rich_label_definition>();
assert(conf);
lbl->set_text_alignment(text_alignment);
lbl->set_link_aware(link_aware);
lbl->set_link_color(conf->link_color);
lbl->set_width(width);
lbl->set_label(lbl->get_label());
DBG_GUI_G << "Window builder: placed rich_label '" << id << "' with definition '"
<< definition << "'.";
return lbl;
}
} // namespace implementation
// }------------ END --------------
} // namespace gui2

View File

@ -0,0 +1,314 @@
/*
Copyright (C) 2024
by Subhraman Sarkar (babaissarkar) <suvrax@gmail.com>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#pragma once
#include "gui/widgets/styled_widget.hpp"
#include "font/standard_colors.hpp"
#include "gui/core/canvas_private.hpp"
#include "gui/core/widget_definition.hpp"
#include "help/help_impl.hpp"
#include "serialization/parser.hpp"
namespace gui2
{
namespace implementation
{
struct builder_rich_label;
}
// ------------ WIDGET -----------{
/**
*
* A rich_label takes marked up text and shows it correctly formatted and wrapped but no scrollbars are provided.
*/
class rich_label : public styled_widget
{
friend struct implementation::builder_rich_label;
public:
explicit rich_label(const implementation::builder_rich_label& builder);
virtual bool can_wrap() const override
{
return can_wrap_ || characters_per_line_ != 0;
}
virtual unsigned get_characters_per_line() const override
{
return characters_per_line_;
}
virtual bool get_link_aware() const override
{
return link_aware_;
}
virtual color_t get_link_color() const override
{
return link_color_;
}
virtual void set_active(const bool active) override;
virtual bool get_active() const override
{
return state_ != DISABLED;
}
virtual unsigned get_state() const override
{
return state_;
}
bool disable_click_dismiss() const override
{
return false;
}
virtual bool can_mouse_focus() const override
{
return !tooltip().empty() || get_link_aware();
}
virtual void update_canvas() override;
/* **** ***** ***** setters / getters for members ***** ****** **** */
void set_can_wrap(const bool wrap)
{
can_wrap_ = wrap;
}
void set_characters_per_line(const unsigned characters_per_line)
{
characters_per_line_ = characters_per_line;
}
void set_link_aware(bool l);
void set_link_color(const color_t& color);
void set_can_shrink(bool can_shrink)
{
can_shrink_ = can_shrink;
}
void set_width(unsigned width)
{
w_ = width;
}
void set_text_alpha(unsigned short alpha);
const t_string& get_label() const
{
return unparsed_text_.empty() ? styled_widget::get_label() : unparsed_text_;
}
void set_label(const t_string& text) override;
void register_link_callback(std::function<void(std::string)> link_handler)
{
link_handler_ = link_handler;
}
private:
/**
* Possible states of the widget.
*
* Note the order of the states must be the same as defined in settings.hpp.
*/
enum state_t {
ENABLED,
DISABLED,
};
void set_state(const state_t state);
/**
* Current state of the widget.
*
* The state of the widget determines what to render and how the widget
* reacts to certain 'events'.
*/
state_t state_;
/** Holds the rich_label can wrap or not. */
bool can_wrap_;
/**
* The maximum number of characters per line.
*
* The maximum is not an exact maximum, it uses the average character width.
*/
unsigned characters_per_line_;
/**
* Whether the rich_label is link aware, rendering links with special formatting
* and handling click events.
*/
bool link_aware_;
/**
* What color links will be rendered in.
*/
color_t link_color_;
bool can_shrink_;
unsigned short text_alpha_;
/** Inherited from styled_widget. */
virtual bool text_can_shrink() override
{
return can_shrink_;
}
/** structure tree of the marked up text after parsing */
config text_dom_;
/** The unparsed/raw text */
t_string unparsed_text_;
/** shapes used for size calculation */
std::unique_ptr<text_shape> tshape_;
std::unique_ptr<image_shape> ishape_;
/** Width and height of the canvas */
unsigned w_, h_, x_;
/** Padding */
unsigned padding_;
/** Height of current text block */
unsigned txt_height_;
/** Height of all previous blocks, combined */
unsigned prev_blk_height_;
/** template for canvas text config */
void default_text_config(config* txt_ptr, t_string text = "");
void add_text_with_attribute(config& curr_item, std::string text, std::string attr_name = "", std::string extra_data = "");
void add_text_with_attributes(config& curr_item, std::string text, std::vector<std::string> attr_names, std::vector<std::string> extra_data);
void add_image(config& curr_item, std::string name, std::string align, bool floating, point& img_size);
void add_link(config& curr_item, std::string name, std::string dest, int img_width);
void append_if_not_empty(config_attribute_value* key, std::string suffix) {
if (!key->str().empty()) {
*key = key->str() + suffix;
}
}
/** size calculation functions */
point get_text_size(config text_cfg, unsigned width = 0);
point get_image_size(config img_cfg);
wfl::map_formula_callable setup_text_renderer(config text_cfg, unsigned width = 0);
size_t get_split_location(std::string text, int img_height);
/** link variables and functions */
std::vector<std::pair<rect, std::string>> links_;
std::function<void(std::string)> link_handler_;
point get_column_line(const point& position) const
{
return font::get_text_renderer().get_column_line(position);
}
point get_xy_from_offset(const unsigned offset) const
{
return font::get_text_renderer().get_cursor_position(offset);
}
point calculate_best_size() const override
{
return point(w_, h_);
}
public:
/** Static type getter that does not rely on the widget being constructed. */
static const std::string& type();
private:
/** Inherited from styled_widget, implemented by REGISTER_WIDGET. */
virtual const std::string& get_control_type() const override;
/* **** ***** ***** signal handlers ***** ****** **** */
/**
* Left click signal handler: checks if we clicked on a hyperlink
*/
void signal_handler_left_button_click(bool& handled);
/**
* Mouse motion signal handler: checks if the cursor is on a hyperlink
*/
void signal_handler_mouse_motion(bool& handled, const point& coordinate);
/**
* Mouse leave signal handler: checks if the cursor left a hyperlink
*/
void signal_handler_mouse_leave(bool& handled);
/**
* Implementation detail for (re)setting the hyperlink cursor.
*/
void update_mouse_cursor(bool enable);
};
// }---------- DEFINITION ---------{
struct rich_label_definition : public styled_widget_definition
{
explicit rich_label_definition(const config& cfg);
struct resolution : public resolution_definition
{
explicit resolution(const config& cfg);
color_t link_color;
};
};
// }---------- BUILDER -----------{
namespace implementation
{
struct builder_rich_label : public builder_styled_widget
{
builder_rich_label(const config& cfg);
using builder_styled_widget::build;
virtual std::unique_ptr<widget> build() const override;
PangoAlignment text_alignment;
bool link_aware;
unsigned width;
};
} // namespace implementation
// }------------ END --------------
} // namespace gui2

View File

@ -47,6 +47,7 @@
#include "serialization/unicode.hpp" // for iterator
#include "color.hpp"
#include <boost/algorithm/string.hpp>
#include <cassert> // for assert
#include <algorithm> // for sort, find, transform, etc
#include <iterator> // for back_insert_iterator, etc
@ -1335,6 +1336,8 @@ std::vector<std::string> parse_text(const std::string &text)
{
std::vector<std::string> res;
bool last_char_escape = false;
bool found_slash = false;
bool in_quotes = false;
const char escape_char = '\\';
std::stringstream ss;
std::size_t pos;
@ -1343,50 +1346,71 @@ std::vector<std::string> parse_text(const std::string &text)
const char c = text[pos];
if (c == escape_char && !last_char_escape) {
last_char_escape = true;
}
else {
} else {
if (state == OTHER) {
if (c == '<') {
if (last_char_escape) {
ss << c;
}
else {
} else {
res.push_back(ss.str());
ss.str("");
state = ELEMENT_NAME;
}
}
else {
} else {
ss << c;
}
}
else if (state == ELEMENT_NAME) {
if (c == '/') {
std::string msg = "Erroneous / in element name.";
throw parse_error(msg);
}
else if (c == '>') {
// End of this name.
} else if (state == ELEMENT_NAME) {
if ((c == '/') && (!in_quotes)) {
found_slash = true;
} else if (c == '\'') {
// toggle quoting
in_quotes = !in_quotes;
} else if (c == '>') {
in_quotes = false;
// end of this tag.
std::stringstream s;
const std::string element_name = ss.str();
std::string element_name = ss.str();
ss.str("");
s << "</" << element_name << ">";
const std::string end_element_name = s.str();
std::size_t end_pos = text.find(end_element_name, pos);
if (end_pos == std::string::npos) {
std::stringstream msg;
msg << "Unterminated element: " << element_name;
throw parse_error(msg.str());
// process any attributes in the start tag
std::size_t attr_pos = element_name.find(" ");
std::string attrs = "";
if (attr_pos != std::string::npos) {
attrs = element_name.substr(attr_pos+1);
element_name = element_name.substr(0, attr_pos);
}
if (found_slash) {
// empty tag
res.push_back(convert_to_wml(element_name, attrs));
found_slash = false;
pos = text.find(">", pos);
} else {
// non-empty tag
s << "</" << element_name << ">";
const std::string end_element_name = s.str();
std::size_t end_pos = text.find(end_element_name, pos);
if (end_pos == std::string::npos) {
std::stringstream msg;
msg << "Unterminated element: " << element_name;
throw parse_error(msg.str());
}
s.str("");
const std::string contents = attrs + " " + text.substr(pos + 1, end_pos - pos - 1);
const std::string element = convert_to_wml(element_name, contents);
res.push_back(element);
pos = end_pos + end_element_name.size() - 1;
}
s.str("");
const std::string contents = text.substr(pos + 1, end_pos - pos - 1);
const std::string element = convert_to_wml(element_name, contents);
res.push_back(element);
pos = end_pos + end_element_name.size() - 1;
state = OTHER;
}
else {
ss << c;
} else {
if (found_slash) {
found_slash = false;
std::string msg = "Erroneous / in element name.";
throw parse_error(msg);
} else {
ss << c;
}
}
}
last_char_escape = false;
@ -1404,17 +1428,23 @@ std::vector<std::string> parse_text(const std::string &text)
return res;
}
std::string convert_to_wml(const std::string &element_name, const std::string &contents)
std::string convert_to_wml(std::string& element_name, const std::string& contents)
{
std::stringstream ss;
bool in_quotes = false;
bool last_char_escape = false;
const char escape_char = '\\';
std::vector<std::string> attributes;
std::stringstream buff;
// Remove any leading and trailing space from element name
boost::algorithm::trim(element_name);
// Find the different attributes.
// No checks are made for the equal sign or something like that.
// Attributes are just separated by spaces or newlines.
// Attributes that contain spaces must be in single quotes.
// No equal key forces that token to be considered as plain text
// and it gets attached to the default 'text' key.
for (std::size_t pos = 0; pos < contents.size(); ++pos) {
const char c = contents[pos];
if (c == escape_char && !last_char_escape) {
@ -1427,7 +1457,13 @@ std::string convert_to_wml(const std::string &element_name, const std::string &c
}
else if ((c == ' ' || c == '\n') && !last_char_escape && !in_quotes) {
// Space or newline, end of attribute.
attributes.push_back(ss.str());
std::size_t eq_pos = ss.str().find("=");
if (eq_pos == std::string::npos) {
// no = sign found, assuming plain text
buff << " " << ss.str();
} else {
attributes.push_back(ss.str());
}
ss.str("");
}
else {
@ -1436,47 +1472,41 @@ std::string convert_to_wml(const std::string &element_name, const std::string &c
last_char_escape = false;
}
}
if (in_quotes) {
std::stringstream msg;
msg << "Unterminated single quote after: '" << ss.str() << "'";
throw parse_error(msg.str());
}
if (!ss.str().empty()) {
attributes.push_back(ss.str());
std::size_t eq_pos = ss.str().find("=");
if (eq_pos == std::string::npos) {
// no = sign found, assuming plain text
buff << " " << ss.str();
} else {
attributes.push_back(ss.str());
}
}
ss.str("");
// Create the WML.
ss << "[" << element_name << "]\n";
for (std::vector<std::string>::const_iterator it = attributes.begin();
it != attributes.end(); ++it) {
ss << *it << "\n";
//for (std::vector<std::string>::const_iterator it = attributes.begin();
// it != attributes.end(); ++it) {
for (auto& elem : attributes) {
boost::algorithm::trim(elem);
ss << elem << "\n";
}
ss << "[/" << element_name << "]\n";
return ss.str();
}
color_t string_to_color(const std::string &cmp_str)
{
if (cmp_str == "green") {
return font::GOOD_COLOR;
std::string text = buff.str();
boost::algorithm::trim(text);
if (!text.empty()) {
ss << "text=\"" << text << "\"\n";
}
if (cmp_str == "red") {
return font::BAD_COLOR;
}
if (cmp_str == "black") {
return font::BLACK_COLOR;
}
if (cmp_str == "yellow") {
return font::YELLOW_COLOR;
}
if (cmp_str == "white") {
return font::BIGMAP_COLOR;
}
// a #rrggbb color in pango format.
if (*cmp_str.c_str() == '#' && cmp_str.size() == 7) {
return color_t::from_hex_string(cmp_str.substr(1));
}
return font::NORMAL_COLOR;
ss << "[/" << element_name << "]";
buff.str("");
return ss.str();
}
std::vector<std::string> split_in_width(const std::string &s, const int font_size,

View File

@ -35,6 +35,7 @@
#include "color.hpp"
#include "exceptions.hpp" // for error
#include "font/constants.hpp"
#include "font/standard_colors.hpp"
#include "gettext.hpp"
#include <optional>
#include <list> // for list
@ -304,23 +305,20 @@ const section *find_section(const section &sec, const std::string &id);
section *find_section(section &sec, const std::string &id);
/**
* Parse a text string. Return a vector with the different parts of the
* text. Each markup item is a separate part while the text between
* markups are separate parts.
* Parse a xml style marked up text string. Return a vector with
* the different parts of the text. Each markup item and the text
* between markups are separate parts. Each line of returned vector
* is valid WML.
*/
std::vector<std::string> parse_text(const std::string &text);
/**
* Convert the contents to wml attributes, surrounded within
* [element_name]...[/element_name]. Return the resulting WML.
* Convert the the text between start and end xml tags for element_name to
* valid wml attributes, surrounded between [element_name]...[/element_name].
* The attributes in the start tag are also used.
* @return the resulting WML.
*/
std::string convert_to_wml(const std::string &element_name, const std::string &contents);
/**
* Return the color the string represents. Return font::NORMAL_COLOR if
* the string is empty or can't be matched against any other color.
*/
color_t string_to_color(const std::string &s);
std::string convert_to_wml(std::string &element_name, const std::string &contents);
/** Make a best effort to word wrap s. All parts are less than width. */
std::vector<std::string> split_in_width(const std::string &s, const int font_size, const unsigned width);

View File

@ -18,6 +18,7 @@
#include "config.hpp" // for config, etc
#include "draw.hpp" // for blit, fill
#include "font/sdl_ttf_compat.hpp"
#include "font/standard_colors.hpp" // for string_to_color
#include "game_config.hpp" // for debug
#include "help/help_impl.hpp" // for parse_error, box_width, etc
#include "lexical_cast.hpp"
@ -294,7 +295,7 @@ void help_text_area::handle_format_cfg(const config &cfg)
bool bold = cfg["bold"].to_bool();
bool italic = cfg["italic"].to_bool();
int font_size = cfg["font_size"].to_int(normal_font_size);
color_t color = help::string_to_color(cfg["color"]);
color_t color = font::string_to_color(cfg["color"]);
add_text_item(text, "", false, font_size, bold, italic, color);
}

View File

@ -73,6 +73,7 @@
#include "gui/dialogs/game_stats.hpp"
#include "gui/dialogs/game_version_dialog.hpp"
#include "gui/dialogs/gamestate_inspector.hpp"
#include "gui/dialogs/gui_test_dialog.hpp"
#include "gui/dialogs/help_browser.hpp"
#include "gui/dialogs/hotkey_bind.hpp"
#include "gui/dialogs/label_settings.hpp"
@ -520,6 +521,10 @@ BOOST_AUTO_TEST_CASE(modal_dialog_test_generator_settings)
{
test<generator_settings>();
}
BOOST_AUTO_TEST_CASE(modal_dialog_test_gui_test_dialog)
{
test<gui_test_dialog>();
}
BOOST_AUTO_TEST_CASE(modal_dialog_test_hotkey_bind)
{
test<hotkey_bind>();
@ -1458,4 +1463,14 @@ struct dialog_tester<editor_edit_unit>
}
};
template<>
struct dialog_tester<gui_test_dialog>
{
dialog_tester() {}
gui_test_dialog* create()
{
return new gui_test_dialog();
}
};
} // namespace