From ee5834afb148c9f7b1f59799d32cf2056866b633 Mon Sep 17 00:00:00 2001 From: Qrox Date: Tue, 25 Feb 2020 02:24:32 +0800 Subject: [PATCH] Window manager (#37894) For properly refreshing and resizing multiple layers of windows --- src/ncurses_def.cpp | 2 + src/sdltiles.cpp | 107 ++++++++++++++++++++++++------------- src/sdltiles.h | 15 ++++++ src/ui_manager.cpp | 126 ++++++++++++++++++++++++++++++++++++++++++++ src/ui_manager.h | 54 +++++++++++++++++++ src/wincurse.cpp | 5 ++ 6 files changed, 272 insertions(+), 37 deletions(-) create mode 100644 src/ui_manager.cpp create mode 100644 src/ui_manager.h diff --git a/src/ncurses_def.cpp b/src/ncurses_def.cpp index 8550acb7b7ffd..ca8a4bee7857b 100644 --- a/src/ncurses_def.cpp +++ b/src/ncurses_def.cpp @@ -19,6 +19,7 @@ #include "catacharset.h" #include "color.h" #include "game_ui.h" +#include "ui_manager.h" extern int VIEW_OFFSET_X; // X position of terrain window extern int VIEW_OFFSET_Y; // Y position of terrain window @@ -204,6 +205,7 @@ void catacurses::resizeterm() const int new_y = ::getmaxy( stdscr.get<::WINDOW>() ); if( ::is_term_resized( new_x, new_y ) ) { game_ui::init_ui(); + ui_manager::screen_resized(); } } diff --git a/src/sdltiles.cpp b/src/sdltiles.cpp index 27140cc4fecbc..8c68095573f8c 100644 --- a/src/sdltiles.cpp +++ b/src/sdltiles.cpp @@ -57,6 +57,7 @@ #include "json.h" #include "optional.h" #include "point.h" +#include "ui_manager.h" #if defined(__linux__) # include // getenv()/setenv() @@ -1762,7 +1763,7 @@ bool handle_resize( int w, int h ) TERMINAL_HEIGHT = WindowHeight / fontheight / scaling_factor; SetupRenderTarget(); game_ui::init_ui(); - + ui_manager::screen_resized(); return true; } return false; @@ -2824,6 +2825,9 @@ static void CheckMessages() #endif last_input = input_event(); + + bool need_redraw = false; + while( SDL_PollEvent( &ev ) ) { switch( ev.type ) { case SDL_WINDOWEVENT: @@ -2854,17 +2858,14 @@ static void CheckMessages() break; #endif case SDL_WINDOWEVENT_SHOWN: - case SDL_WINDOWEVENT_EXPOSED: case SDL_WINDOWEVENT_MINIMIZED: - break; case SDL_WINDOWEVENT_FOCUS_GAINED: - // Main menu redraw - reinitialize_framebuffer(); - // TODO: redraw all game menus if they are open + break; + case SDL_WINDOWEVENT_EXPOSED: + need_redraw = true; needupdate = true; break; case SDL_WINDOWEVENT_RESTORED: - needupdate = true; #if defined(__ANDROID__) needs_sdl_surface_visibility_refresh = true; if( android_is_hardware_keyboard_available() ) { @@ -2880,6 +2881,10 @@ static void CheckMessages() break; } break; + case SDL_RENDER_TARGETS_RESET: + need_redraw = true; + needupdate = true; + break; case SDL_KEYDOWN: { #if defined(__ANDROID__) // Toggle virtual keyboard with Android back button. For some reason I get double inputs, so ignore everything once it's already down. @@ -3204,6 +3209,19 @@ static void CheckMessages() break; } } + if( need_redraw ) { + // FIXME: SDL_RENDER_TARGETS_RESET only seems to be fired after the first redraw + // when restoring the window after system sleep, rather than immediately + // on focus gain. This seems to mess up the first redraw and + // causes black screen that lasts ~0.5 seconds before the screen + // contents are redrawn in the following code. + + // Main menu redraw + reinitialize_framebuffer(); + window_dimensions dim = get_window_dimensions( catacurses::stdscr ); + ui_manager::invalidate( rectangle( point_zero, dim.window_size_pixel ) ); + ui_manager::redraw(); + } if( needupdate ) { try_sdl_update(); } @@ -3693,45 +3711,60 @@ void rescale_tileset( int size ) game_ui::init_ui(); } -cata::optional input_context::get_coordinates( const catacurses::window &capture_win_ ) +window_dimensions get_window_dimensions( const catacurses::window &win ) { - if( !coordinate_input_received ) { - return cata::nullopt; - } + cata_cursesport::WINDOW *const pwin = win.get(); - cata_cursesport::WINDOW *const capture_win = ( capture_win_.get() ? capture_win_ : - g->w_terrain ).get(); - - // this contains the font dimensions of the capture_win, - // not necessarily the global standard font dimensions. - int fw = fontwidth; - int fh = fontheight; - // tiles might have different dimensions than standard font - if( use_tiles && capture_win == g->w_terrain ) { - fw = tilecontext->get_tile_width(); - fh = tilecontext->get_tile_height(); - // add_msg( m_info, "tile map fw %d fh %d", fw, fh); - } else if( map_font && capture_win == g->w_terrain ) { + window_dimensions dim; + if( use_tiles && win == g->w_terrain ) { + // tiles might have different dimensions than standard font + dim.scaled_font_size.x = tilecontext->get_tile_width(); + dim.scaled_font_size.y = tilecontext->get_tile_height(); + } else if( map_font && win == g->w_terrain ) { // map font (if any) might differ from standard font - fw = map_font->fontwidth; - fh = map_font->fontheight; - } else if( overmap_font && capture_win == g->w_overmap ) { - fw = overmap_font->fontwidth; - fh = overmap_font->fontheight; + dim.scaled_font_size.x = map_font->fontwidth; + dim.scaled_font_size.y = map_font->fontheight; + } else if( overmap_font && win == g->w_overmap ) { + dim.scaled_font_size.x = overmap_font->fontwidth; + dim.scaled_font_size.y = overmap_font->fontheight; + } else { + dim.scaled_font_size.x = fontwidth; + dim.scaled_font_size.y = fontheight; } // multiplied by the user's specified scaling factor regardless of whether tiles are in use - fw = fw * get_scaling_factor(); - fh = fh * get_scaling_factor(); + dim.scaled_font_size *= get_scaling_factor(); + + dim.window_pos_cell = pwin->pos; + dim.window_size_cell.x = pwin->width; + dim.window_size_cell.y = pwin->height; - // Translate mouse coordinates to map coordinates based on tile size, // the window position is *always* in standard font dimensions! - const point win_min( capture_win->pos.x * fontwidth, capture_win->pos.y * fontheight ); + dim.window_pos_pixel = point( dim.window_pos_cell.x * fontwidth, + dim.window_pos_cell.y * fontheight ); // But the size of the window is in the font dimensions of the window. - const point win_size( capture_win->width * fw, capture_win->height * fh ); + dim.window_size_pixel.x = dim.window_size_cell.x * dim.scaled_font_size.x; + dim.window_size_pixel.y = dim.window_size_cell.y * dim.scaled_font_size.y; + + return dim; +} + +cata::optional input_context::get_coordinates( const catacurses::window &capture_win_ ) +{ + if( !coordinate_input_received ) { + return cata::nullopt; + } + + const catacurses::window &capture_win = capture_win_ ? capture_win_ : g->w_terrain; + const window_dimensions dim = get_window_dimensions( capture_win ); + + const int &fw = dim.scaled_font_size.x; + const int &fh = dim.scaled_font_size.y; + const point &win_min = dim.window_pos_pixel; + const point &win_size = dim.window_size_pixel; const point win_max = win_min + win_size; - // add_msg( m_info, "win_ left %d top %d right %d bottom %d", win_left,win_top,win_right,win_bottom); - // add_msg( m_info, "coordinate_ x %d y %d", coordinate_x, coordinate_y); + + // Translate mouse coordinates to map coordinates based on tile size // Check if click is within bounds of the window we care about const rectangle win_bounds( win_min, win_max ); if( !win_bounds.contains_inclusive( coordinate ) ) { @@ -3754,7 +3787,7 @@ cata::optional input_context::get_coordinates( const catacurses::windo p = view_offset + selected; } else { const point selected( screen_pos.x / fw, screen_pos.y / fh ); - p = view_offset + selected - point( capture_win->width / 2, capture_win->height / 2 ); + p = view_offset + selected - dim.window_size_cell / 2; } return tripoint( p, g->get_levz() ); diff --git a/src/sdltiles.h b/src/sdltiles.h index a7ab650c3ea4d..433561148e03d 100644 --- a/src/sdltiles.h +++ b/src/sdltiles.h @@ -9,10 +9,16 @@ #include #include "color_loader.h" +#include "point.h" #include "sdl_wrappers.h" class cata_tiles; +namespace catacurses +{ +class window; +} // namespace catacurses + extern SDL_Texture_Ptr alt_rect_tex; extern bool alt_rect_tex_enabled; extern std::unique_ptr tilecontext; @@ -25,6 +31,15 @@ void rescale_tileset( int size ); bool save_screenshot( const std::string &file_path ); void toggle_fullscreen_window(); +struct window_dimensions { + point scaled_font_size; + point window_pos_cell; + point window_size_cell; + point window_pos_pixel; + point window_size_pixel; +}; +window_dimensions get_window_dimensions( const catacurses::window &win ); + #endif // TILES #endif // CATA_SDLTILES_H diff --git a/src/ui_manager.cpp b/src/ui_manager.cpp new file mode 100644 index 0000000000000..9ee7387aa6aff --- /dev/null +++ b/src/ui_manager.cpp @@ -0,0 +1,126 @@ +#include "ui_manager.h" + +#include + +#include "cursesdef.h" +#include "point.h" +#include "sdltiles.h" + +static std::vector> ui_stack; + +ui_adaptor::ui_adaptor() : invalidated( false ) +{ + ui_stack.emplace_back( *this ); +} + +ui_adaptor::~ui_adaptor() +{ + for( auto it = ui_stack.rbegin(); it < ui_stack.rend(); ++it ) { + if( &it->get() == this ) { + ui_stack.erase( std::prev( it.base() ) ); + // TODO avoid invalidating portions that do not need to be redrawn + ui_manager::invalidate( dimensions ); + break; + } + } +} + +void ui_adaptor::position_from_window( const catacurses::window &win ) +{ + const rectangle old_dimensions = dimensions; + // ensure position is updated before calling invalidate +#ifdef TILES + const window_dimensions dim = get_window_dimensions( win ); + dimensions = rectangle( dim.window_pos_pixel, dim.window_pos_pixel + dim.window_size_pixel ); +#else + const point origin( getbegx( win ), getbegy( win ) ); + dimensions = rectangle( origin, origin + point( getmaxx( win ), getmaxy( win ) ) ); +#endif + invalidated = true; + ui_manager::invalidate( old_dimensions ); +} + +void ui_adaptor::on_redraw( const redraw_callback_t &fun ) +{ + redraw_cb = fun; +} + +void ui_adaptor::on_screen_resize( const screen_resize_callback_t &fun ) +{ + screen_resized_cb = fun; +} + +static bool contains( const rectangle &lhs, const rectangle &rhs ) +{ + return rhs.p_min.x >= lhs.p_min.x && rhs.p_max.x <= lhs.p_max.x && + rhs.p_min.y >= lhs.p_min.y && rhs.p_max.y <= lhs.p_max.y; +} + +static bool overlap( const rectangle &lhs, const rectangle &rhs ) +{ + return lhs.p_min.x < rhs.p_max.x && lhs.p_min.y < rhs.p_max.y && + rhs.p_min.x < lhs.p_max.x && rhs.p_min.y < lhs.p_max.y; +} + +void ui_adaptor::invalidate( const rectangle &rect ) +{ + if( rect.p_min.x >= rect.p_max.x || rect.p_min.y >= rect.p_max.y ) { + return; + } + // TODO avoid invalidating portions that do not need to be redrawn + for( auto it = ui_stack.crbegin(); it < ui_stack.crend(); ++it ) { + const ui_adaptor &ui = it->get(); + if( overlap( ui.dimensions, rect ) ) { + ui.invalidated = true; + if( contains( ui.dimensions, rect ) ) { + break; + } + } + } +} + +void ui_adaptor::redraw() +{ + // TODO refresh only when all stacked UIs are drawn + if( !ui_stack.empty() ) { + ui_stack.back().get().invalidated = true; + for( const ui_adaptor &ui : ui_stack ) { + if( ui.invalidated ) { + if( ui.redraw_cb ) { + ui.redraw_cb( ui ); + } + ui.invalidated = false; + } + } + } +} + +void ui_adaptor::screen_resized() +{ + for( ui_adaptor &ui : ui_stack ) { + if( ui.screen_resized_cb ) { + ui.screen_resized_cb( ui ); + } + } + redraw(); +} + +namespace ui_manager +{ + +void invalidate( const rectangle &rect ) +{ + ui_adaptor::invalidate( rect ); +} + +void redraw() +{ + ui_adaptor::redraw(); +} + +void screen_resized() +{ + ui_adaptor::screen_resized(); +} + +} // namespace ui_manager diff --git a/src/ui_manager.h b/src/ui_manager.h new file mode 100644 index 0000000000000..61c1f46915fd1 --- /dev/null +++ b/src/ui_manager.h @@ -0,0 +1,54 @@ +#pragma once +#ifndef UI_MANAGER_H +#define UI_MANAGER_H + +#include + +#include "point.h" + +namespace catacurses +{ +class window; +} // namespace catacurses + +class ui_adaptor +{ + public: + using redraw_callback_t = std::function; + using screen_resize_callback_t = std::function; + + ui_adaptor(); + ui_adaptor( const ui_adaptor &rhs ) = delete; + ui_adaptor( ui_adaptor &&rhs ) = delete; + ~ui_adaptor(); + + ui_adaptor &operator=( const ui_adaptor &rhs ) = delete; + ui_adaptor &operator=( ui_adaptor &&rhs ) = delete; + + void position_from_window( const catacurses::window &win ); + void on_redraw( const redraw_callback_t &fun ); + void on_screen_resize( const screen_resize_callback_t &fun ); + + static void invalidate( const rectangle &rect ); + static void redraw(); + static void screen_resized(); + private: + // pixel dimensions in tiles, console cell dimensions in curses + rectangle dimensions; + redraw_callback_t redraw_cb; + screen_resize_callback_t screen_resized_cb; + + mutable bool invalidated; +}; + +// export static funcs of ui_adaptor with a more coherent scope name +namespace ui_manager +{ +// rect is the pixel dimensions in tiles or console cell dimensions in curses +void invalidate( const rectangle &rect ); +// invalidate the top window and redraw all invalidated windows +void redraw(); +void screen_resized(); +} // namespace ui_manager + +#endif diff --git a/src/wincurse.cpp b/src/wincurse.cpp index 350759fc6516a..4337469a42080 100644 --- a/src/wincurse.cpp +++ b/src/wincurse.cpp @@ -24,6 +24,7 @@ #include "font_loader.h" #include "platform_win.h" #include "mmsystem.h" +#include "ui_manager.h" #include "wcwidth.h" //*********************************** @@ -189,6 +190,7 @@ bool handle_resize( int, int ) throw std::runtime_error( "SetDIBColorTable failed" ); } catacurses::refresh(); + ui_manager::screen_resized(); } return true; @@ -372,6 +374,9 @@ LRESULT CALLBACK ProcessMessages( HWND__ *hWnd, unsigned int Msg, case WM_PAINT: BitBlt( WindowDC, 0, 0, WindowWidth, WindowHeight, backbuffer, 0, 0, SRCCOPY ); + ui_manager::invalidate( rectangle( point_zero, point( getmaxx( catacurses::stdscr ), + getmaxy( catacurses::stdscr ) ) ) ); + ui_manager::redraw(); ValidateRect( WindowHandle, nullptr ); return 0;