The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
story_viewer.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2017 - 2025
3 by Charles Dang <exodia339@gmail.com>
4 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY.
12
13 See the COPYING file for more details.
14*/
15
16#define GETTEXT_DOMAIN "wesnoth-lib"
17
19
20#include "display.hpp"
21#include "formula/variant.hpp"
22#include "sdl/point.hpp"
23#include "gui/core/timer.hpp"
25#include "gui/widgets/grid.hpp"
26#include "gui/widgets/image.hpp"
27#include "gui/widgets/label.hpp"
32#include "sound.hpp"
33#include "variable.hpp"
34
35#include <SDL3/SDL_timer.h>
36
37namespace gui2::dialogs
38{
39
40// Helper function to get the canvas shape data for the shading under the title area until
41// I can figure out how to ensure it always stays on top of the canvas stack.
43{
44 static config cfg;
45 cfg["x"] = 0;
46 cfg["y"] = 0;
47 cfg["w"] = "(screen_width)";
48 cfg["h"] = "(image_original_height * 2)";
49 cfg["name"] = "dialogs/story_title_decor.png~O(75%)";
50
51 return cfg;
52}
53
54// Stacked widget layer constants for the text stack.
55static const unsigned int LAYER_BACKGROUND = 1;
56static const unsigned int LAYER_TEXT = 2;
57
59
60story_viewer::story_viewer(const std::string& scenario_name, const config& cfg_parsed)
61 : modal_dialog(window_id())
62 , controller_(vconfig(cfg_parsed, true), scenario_name)
63 , part_index_(0)
64 , current_part_(nullptr)
65 , timer_id_(0)
66 , next_draw_(0)
67 , fade_step_(0)
68 , fade_state_(NOT_FADING)
69{
70 update_current_part_ptr();
71}
72
74{
75 if(timer_id_ != 0) {
77 timer_id_ = 0;
78 }
79}
80
85
87{
89
90 // Special callback handle key presses
91 connect_signal_pre_key_press(*this, std::bind(&story_viewer::key_press_callback, this, std::placeholders::_5));
92
93 connect_signal_mouse_left_click(find_widget<button>("next"),
95 connect_signal_mouse_left_click(find_widget<button>("prev"),
97
98 connect_signal<event::BACK_BUTTON_CLICK>([this](auto&&...) {
101 connect_signal<event::FORWARD_BUTTON_CLICK>([this](auto&&...) {
104
105 find_widget<scroll_label>("part_text").connect_signal<event::LEFT_BUTTON_CLICK>([this](auto&&...) {
108
109 // Tell the game display not to draw
112
113 display_part();
114}
115
117{
118 // Bring the game display back again, if appropriate
120}
121
126
128{
129 static const int VOICE_SOUND_SOURCE_ID = 255;
130 // Update Back button state. Doing this here so it gets called in pre_show too.
131 find_widget<button>("prev").set_active(part_index_ != 0);
132
133 //
134 // Music and sound
135 //
136 if(!current_part_->music().empty()) {
138 "name", current_part_->music(),
139 "ms_after", 2000,
140 "immediate", true
141 });
142 }
143
144 if(!current_part_->sound().empty()) {
146 }
147
148 sound::stop_sound(VOICE_SOUND_SOURCE_ID);
149 if(!current_part_->voice().empty()) {
151 }
152
153 config cfg;
154
155 //
156 // Background images
157 //
158 bool has_background = false;
159 config* base_layer = nullptr;
160
161 for(const auto& layer : current_part_->get_background_layers()) {
162 has_background |= !layer.file().empty();
163
164 const bool preserve_ratio = layer.keep_aspect_ratio();
165 const bool tile_h = layer.tile_horizontally();
166 const bool tile_v = layer.tile_vertically();
167
168 // Background layers are almost always centered. In case of tiling, we want the full
169 // area in the horizontal or vertical direction, so set the origin to 0 for that axis.
170 // The resize mode will center the original image in the available area first/
171 std::string x_formula = tile_h ? "0" : "(max(pos, 0) where pos = (width / 2 - image_width / 2))";
172 std::string y_formula = tile_v ? "0" : "(max(pos, 0) where pos = (height / 2 - image_height / 2))";
173
174 // By default, no scaling will be applied.
175 std::string width_formula = "(image_original_width)";
176 std::string height_formula = "(image_original_height)";
177
178 if(layer.scale_horizontally() && preserve_ratio) {
179 height_formula = "(min((image_original_height * width / image_original_width), height))";
180 } else if(layer.scale_vertically() || tile_v) {
181 height_formula = "(height)";
182 }
183
184 if(layer.scale_vertically() && preserve_ratio) {
185 width_formula = "(min((image_original_width * height / image_original_height), width))";
186 } else if(layer.scale_horizontally() || tile_h) {
187 width_formula = "(width)";
188 }
189
190 config& image = cfg.add_child("image", config{
191 "x", x_formula,
192 "y", y_formula,
193 "w", width_formula,
194 "h", height_formula,
195 "name", layer.file(),
196 "resize_mode", (tile_h || tile_v) ? "tile_center" : "scale"
197 });
198
199 if(base_layer == nullptr || layer.is_base_layer()) {
200 base_layer = &image;
201 }
202 }
203
204 canvas& window_canvas = get_canvas(0);
205
206 /* In order to avoid manually loading the image and calculating the scaling factor, we instead
207 * delegate the task of setting the necessary variables to the canvas once the calculations
208 * have been made internally.
209 *
210 * This sets the necessary values with the data for "this" image when its drawn. If no base
211 * layer was found (which would be the case if no backgrounds were provided at all), simply set
212 * some sane defaults directly.
213 */
214 if(base_layer != nullptr) {
215 (*base_layer)["actions"] = R"((
216 [
217 set_var('base_scale_x', as_decimal(image_width) / as_decimal(image_original_width)),
218 set_var('base_scale_y', as_decimal(image_height) / as_decimal(image_original_height)),
219 set_var('base_origin_x', clip_x),
220 set_var('base_origin_y', clip_y)
221 ]
222 ))";
223 } else {
224 window_canvas.set_variable("base_scale_x", wfl::variant(1));
225 window_canvas.set_variable("base_scale_y", wfl::variant(1));
226 window_canvas.set_variable("base_origin_x", wfl::variant(0));
227 window_canvas.set_variable("base_origin_y", wfl::variant(0));
228 }
229
231
232 //
233 // Title
234 //
235 const std::string& title_text = current_part_->title();
236 if(current_part_->show_title() && !title_text.empty()) {
237 const point& title_position = current_part_->title_position();
238 cfg.add_child("text", config{
239 "x", "(max(" + std::to_string(title_position.x) + " * (width - 10 - text_width) / 100, 10))",
240 "y", "(max(" + std::to_string(title_position.y) + " * (height - 10 - text_height) / 100, 10))",
241 "w", "(text_width)",
242 "h", "(text_height)",
243 "maximum_width", "(text_width)",
244 "font_family", "script",
245 "font_size", 32,
246 "color", "([215, 215, 215, title_alpha])",
247 "text", title_text,
248 "text_markup", true,
249 "text_alignment", current_part_->title_text_alignment(),
250 "text_link_aware", "false"
251 });
252 }
253
254 window_canvas.set_shapes(cfg);
255
256 // Needed to make the background redraw correctly.
257 window_canvas.update_size_variables();
258 queue_redraw();
259
260 //
261 // Story text
262 //
263 stacked_widget& text_stack = find_widget<stacked_widget>("text_and_control_stack");
264
265 std::string new_panel_mode;
266
267 switch(current_part_->story_text_location()) {
269 new_panel_mode = "top";
270 break;
272 new_panel_mode = "center";
273 break;
275 new_panel_mode = "bottom";
276 break;
277 }
278
279 text_stack.set_vertical_alignment(new_panel_mode);
280
281 /* Set the panel mode control variables.
282 *
283 * We use get_layer_grid here to ensure the widget is always found regardless of
284 * whether the background is visible or not.
285 */
286 canvas& panel_canvas = text_stack.get_layer_grid(LAYER_BACKGROUND)->find_widget<panel>("text_panel").get_canvas(0);
287
288 panel_canvas.set_variable("panel_position", wfl::variant(new_panel_mode));
289
290 const std::string& part_text = current_part_->text();
291
292 if(part_text.empty() || !has_background) {
293 // No text or no background for this part, hide the background layer.
294 text_stack.select_layer(LAYER_TEXT);
295 } else if(text_stack.current_layer() != -1) {
296 // If the background layer was previously hidden, re-show it.
297 text_stack.select_layer(-1);
298 }
299
300 // Convert the story part text alignment types into the Pango equivalents
301 PangoAlignment story_text_alignment = decode_text_alignment(current_part_->story_text_alignment());
302
303 scroll_label& text_label = find_widget<scroll_label>("part_text");
304 text_label.set_text_alignment(story_text_alignment);
305 text_label.set_text_alpha(0);
306 text_label.set_label(part_text);
307
308 // Regenerate any background blur texture
309 panel_canvas.queue_reblur();
310
311 begin_fade_draw(true);
312 // if the previous page was skipped, it is possible that we already have a timer running.
314 //
315 // Floating images (handle this last)
316 //
317 const auto& floating_images = current_part_->get_floating_images();
318
319 // If we have images to draw, draw the first one now. A new non-repeating timer is added
320 // after every draw to schedule the next one after the specified interval.
321 //
322 // TODO: in the old GUI1 dialog, floating images delayed the appearance of the story panel until
323 // drawing was finished. Might be worth looking into restoring that.
324 if(!floating_images.empty()) {
325 draw_floating_image(floating_images.begin(), part_index_);
326 }
327}
328
329void story_viewer::draw_floating_image(floating_image_list::const_iterator image_iter, int this_part_index)
330{
331 const auto& images = current_part_->get_floating_images();
332 canvas& window_canvas = get_canvas(0);
333
334 // If the current part has changed or we're out of images to draw, exit the draw loop.
335 while((this_part_index == part_index_) && (image_iter != images.end())) {
336 const auto& floating_image = *image_iter;
337 ++image_iter;
338
339 std::ostringstream x_ss;
340 std::ostringstream y_ss;
341
342 // Floating images' locations are scaled by the same factor as the background.
343 x_ss << "(trunc(" << floating_image.ref_x() << " * base_scale_x) + base_origin_x";
344 y_ss << "(trunc(" << floating_image.ref_y() << " * base_scale_y) + base_origin_y";
345
346 if(floating_image.centered()) {
347 x_ss << " - (image_width / 2)";
348 y_ss << " - (image_height / 2)";
349 }
350
351 x_ss << ")";
352 y_ss << ")";
353
355 image["x"] = x_ss.str();
356 image["y"] = y_ss.str();
357
358 // Width and height don't need to be set unless the image needs to be scaled.
359 if(floating_image.resize_with_background()) {
360 image["w"] = "(image_original_width * base_scale_x)";
361 image["h"] = "(image_original_height * base_scale_y)";
362 }
363
364 image["name"] = floating_image.file();
365 config cfg{"image", std::move(image)};
366
367 window_canvas.append_shapes(cfg);
368
369 // Needed to make the background redraw correctly.
370 window_canvas.update_size_variables();
371 queue_redraw();
372
373 // If a delay is specified, schedule the next image draw and break out of the loop.
374 const auto& draw_delay = floating_image.display_delay();
375 if(draw_delay != std::chrono::milliseconds{0}) {
376 // This must be a non-repeating timer
377 timer_id_ = add_timer(draw_delay, std::bind(&story_viewer::draw_floating_image, this, image_iter, this_part_index), false);
378 return;
379 }
380 }
381
382 timer_id_ = 0;
383}
384
386{
387 // If a button is pressed while fading in, abort and set alpha to full opaque.
388 if(fade_state_ == FADING_IN) {
390
391 // Only set full alpha if Forward was pressed.
392 if(direction == DIR_FORWARD) {
393 find_widget<scroll_label>("part_text").set_text_alpha(ALPHA_OPAQUE);
395 return;
396 }
397 }
398
399 // If a button is pressed while fading out, skip and show next part.
400 if(fade_state_ == FADING_OUT) {
401 display_part();
402 return;
403 }
404
405 assert(fade_state_ == NOT_FADING);
406
407 part_index_ = (direction == DIR_FORWARD ? part_index_ + 1 : part_index_ -1);
408
409 // If we've viewed all the parts, close the dialog.
411 close();
412 return;
413 }
414
415 if(part_index_ < 0) {
416 part_index_ = 0;
417 }
418
420
421 begin_fade_draw(false);
422}
423
424void story_viewer::key_press_callback(const SDL_Keycode key)
425{
426 const bool next_keydown =
427 key == SDLK_SPACE
428 || key == SDLK_RETURN
429 || key == SDLK_KP_ENTER
430 || key == SDLK_RIGHT;
431
432 const bool back_keydown =
433 key == SDLK_BACKSPACE
434 || key == SDLK_LEFT;
435
436 if(next_keydown) {
438 } else if(back_keydown) {
440 }
441}
442
444{
445 next_draw_ = SDL_GetTicks() + 20;
446}
447
449{
451
452 fade_step_ = fade_in ? 0 : 10;
453 fade_state_ = fade_in ? FADING_IN : FADING_OUT;
454}
455
462
464{
466
467 if(next_draw_ && SDL_GetTicks() < next_draw_) {
468 return;
469 }
470
471 if(fade_state_ == NOT_FADING) {
472 return;
473 }
474
475 // If we've faded fully in...
476 if(fade_state_ == FADING_IN && fade_step_ > 10) {
478 return;
479 }
480
481 // If we've faded fully out...
482 if(fade_state_ == FADING_OUT && fade_step_ < 0) {
484
485 display_part();
486 return;
487 }
488
489 unsigned short new_alpha = std::clamp<short>(fade_step_ * 25.5, 0, ALPHA_OPAQUE);
490 find_widget<scroll_label>("part_text").set_text_alpha(new_alpha);
491 get_canvas(0).set_variable("title_alpha", wfl::variant(new_alpha));
492 queue_redraw();
493
494 // The text stack also needs to be marked dirty so the background panel redraws correctly.
496
497 if(fade_state_ == FADING_IN) {
498 fade_step_ ++;
499 } else if(fade_state_ == FADING_OUT) {
500 fade_step_ --;
501 }
502
504}
505
507{
508 find_widget<stacked_widget>("text_and_control_stack").queue_redraw();
509}
510
511} // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
config & add_child(std::string_view key)
Definition config.cpp:436
bool empty() const
Definition config.cpp:823
void set_prevent_draw(bool pd=true)
Prevent the game display from drawing.
Definition display.cpp:1979
bool get_prevent_draw()
Definition display.cpp:1988
static display * get_singleton()
Returns the display object if a display object exists.
Definition display.hpp:102
A simple canvas which can be drawn upon.
Definition canvas.hpp:45
void set_variable(const std::string &key, wfl::variant &&value)
Definition canvas.hpp:165
void queue_reblur()
Clear the cached blur texture, forcing it to regenerate.
Definition canvas.cpp:781
void append_shapes(const config &cfg)
Appends data to the config.
Definition canvas.hpp:143
void set_shapes(const config &cfg, const bool force=false)
Sets the config.
Definition canvas.hpp:126
void update_size_variables()
Update WFL size variables.
Definition canvas.cpp:855
Abstract base class for all modal dialogs.
Dialog to view the storyscreen.
storyscreen::controller controller_
void begin_fade_draw(bool fade_in)
storyscreen::controller::part_pointer_type current_part_
virtual void update() override
top_level_drawable hook to animate the view
void draw_floating_image(floating_image_list::const_iterator image_iter, int this_part_index)
virtual void pre_show() override
Actions to be taken before showing the window.
void nav_button_callback(NAV_DIRECTION direction)
virtual void post_show() override
Actions to be taken after the window has been shown.
void key_press_callback(const SDL_Keycode key)
virtual void set_label(const t_string &label) override
See styled_widget::set_label.
virtual void set_text_alignment(const PangoAlignment text_alignment) override
See styled_widget::set_text_alignment.
void set_text_alpha(unsigned short alpha)
int current_layer() const
Gets the current visible layer number.
grid * get_layer_grid(unsigned int i)
Gets the grid for a specified layer.
void select_layer(const int layer)
Selects and displays a particular layer.
canvas & get_canvas(const unsigned index)
virtual void update()
Update state and any parameters that may effect layout, or any of the later stages.
T * find_widget(const std::string_view id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition widget.hpp:747
void queue_redraw()
Indicates that this widget should be redrawn.
Definition widget.cpp:464
virtual void set_vertical_alignment(const std::string &alignment)
Sets the horizontal alignment of the widget within its parent grid.
Definition widget.cpp:293
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition window.hpp:313
void close()
Requests to close the window.
Definition window.hpp:216
part_pointer_type get_part(int index) const
@ BLOCK_BOTTOM
Bottom of the screen.
Definition part.hpp:239
@ BLOCK_MIDDLE
Center of the screen.
Definition part.hpp:238
@ BLOCK_TOP
Top of the screen.
Definition part.hpp:237
A variable-expanding proxy for the config class.
Definition variable.hpp:45
constexpr uint8_t ALPHA_OPAQUE
Definition color.hpp:37
map_display and display: classes which take care of displaying the map and game-data on the screen.
const config * cfg
This file contains the window object, this object is a top level container which has the event manage...
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
static config get_title_area_decor_config()
static const unsigned int LAYER_TEXT
static const unsigned int LAYER_BACKGROUND
@ LEFT_BUTTON_CLICK
Definition handler.hpp:122
std::size_t add_timer(const std::chrono::milliseconds &interval, const std::function< void(std::size_t id)> &callback, const bool repeat)
Adds a new timer.
Definition timer.cpp:123
PangoAlignment decode_text_alignment(const std::string &alignment)
Converts a text alignment string to a text alignment.
Definition helper.cpp:84
bool remove_timer(const std::size_t id)
Removes a timer.
Definition timer.cpp:164
Functions to load and save images from/to disk.
void play_music_config(const config &music_node, bool allow_interrupt_current_track, int i)
Definition sound.cpp:687
void play_sound(const std::string &files, sound_tracks::type group, unsigned int repeats)
Definition sound.cpp:978
void stop_sound()
Definition sound.cpp:543
STL namespace.
This file contains the settings handling of the widget library.
Holds a 2D point.
Definition point.hpp:25
Contains the gui2 timer routines.