diff --git a/Documentation/HighDPI.md b/Documentation/HighDPI.md index 4a7e8d0ef2087f..778fc89ab24b58 100644 --- a/Documentation/HighDPI.md +++ b/Documentation/HighDPI.md @@ -32,7 +32,7 @@ Resources such as icons, cursors, bitmap fonts are scale-dependent: In HighDPI m A 2x resource should look like a 1x resource, just with less jagged edges. A horizontal or vertical line that's 1 pixel wide in 1x should be 2 pixels wide in 2x. -A good guideline for black-and-white images: start with a 1x bitmap, resize it to 200% using nearest-neighbor filtering, and then move black pixels around to smooth diagonal edges -- but the number of black pixels shouldn't change relative to the 200% nearest-neighbor-resampled image. +A good guideline for black-and-white images: start with a 1x bitmap, resize it to 200% using nearest-neighbor filtering, and then move black pixels around to smooth diagonal edges -- but the number of black pixels shouldn't change relative to the 200% nearest-neighbor-resampled image. If that's not possible, err towards making the icon smaller instead of larger. A good technique is to use the Ctrl-Shift-Super-I shortcut in HighDPI mode to toggle between low-res and high-res icons in HighDPI mode. While a 1x 32x32 bitmap and a 2x 16x16 bitmap both have 32x32 pixels, they don't have to look the same: The 2x 16x16 should look exactly like the corresonding 1x 16x16, just with smoother edges. The 1x 32x32 pixel resource could instead pick a different crop. As a concrete example, the 1x 7x10 ladybug emoji image currently just has the ladybug's shell (for space reasons), and so should the 2x version of that emoji. On the other hand, if we add a higher-res 1x 14x20 ladybug emoji at some point, we might want show the ladybug's legs on it, instead of just a smoother rendition of just the shell. (The 2x version of that 14x20 emoji would then have legs and shell in less jagged.) @@ -135,6 +135,8 @@ Android has additional modifiers in additon to scale factors in its resource sys In the end probably doesn't matter all that much which version to pick. +For now, we're going with a "-2x" suffix on the file name. + ### Resource loading strategy tradeoffs - eagerly load one scale, reload at new scale on scale factor change events @@ -156,6 +158,8 @@ In the end probably doesn't matter all that much which version to pick. + puts (less) complexity in framework, app doesn't have to care + can transparently paint UI at both 1x and 2x into different backbuffers (eg for multiple screens that have different scale factors) +This isn't figured out yet, for now we're doing the first approach in select places in the window server. + ### Resource loading API Currently: @@ -186,4 +190,4 @@ The plan is to have all applications use highdpi backbuffers eventually. It'll t 4. Let apps opt in to high-res window framebuffers, and convert all apps one-by-one 5. Remove high-res window framebuffer opt-in since all apps have it now. -We're currently before point 3. +We're currently in the middle of point 3. Some window server icons are high-resolution, but fonts aren't yet, and in-window-server things with their own backing store (eg menus) aren't yet either. diff --git a/Userland/Libraries/LibGfx/Bitmap.cpp b/Userland/Libraries/LibGfx/Bitmap.cpp index 291d30a9b3762d..879fec987f6b04 100644 --- a/Userland/Libraries/LibGfx/Bitmap.cpp +++ b/Userland/Libraries/LibGfx/Bitmap.cpp @@ -25,6 +25,7 @@ */ #include +#include #include #include #include @@ -141,8 +142,33 @@ RefPtr Bitmap::create_wrapper(BitmapFormat format, const IntSize& size, return adopt(*new Bitmap(format, size, scale_factor, pitch, data)); } -RefPtr Bitmap::load_from_file(const StringView& path) +RefPtr Bitmap::load_from_file(const StringView& path, int scale_factor) { + if (scale_factor > 1 && path.starts_with("/res/")) { + LexicalPath lexical_path { path }; + StringBuilder highdpi_icon_path; + highdpi_icon_path.append(lexical_path.dirname()); + highdpi_icon_path.append("/"); + highdpi_icon_path.append(lexical_path.title()); + highdpi_icon_path.appendf("-%dx.", scale_factor); + highdpi_icon_path.append(lexical_path.extension()); + + RefPtr bmp; +#define __ENUMERATE_IMAGE_FORMAT(Name, Ext) \ + if (path.ends_with(Ext, CaseSensitivity::CaseInsensitive)) \ + bmp = load_##Name(highdpi_icon_path.to_string()); + ENUMERATE_IMAGE_FORMATS +#undef __ENUMERATE_IMAGE_FORMAT + if (bmp) { + ASSERT(bmp->width() % scale_factor == 0); + ASSERT(bmp->height() % scale_factor == 0); + bmp->m_size.set_width(bmp->width() / scale_factor); + bmp->m_size.set_height(bmp->height() / scale_factor); + bmp->m_scale = scale_factor; + return bmp; + } + } + #define __ENUMERATE_IMAGE_FORMAT(Name, Ext) \ if (path.ends_with(Ext, CaseSensitivity::CaseInsensitive)) \ return load_##Name(path); diff --git a/Userland/Libraries/LibGfx/Bitmap.h b/Userland/Libraries/LibGfx/Bitmap.h index 92b0f3b144a0fb..0dfbe82bcc1a00 100644 --- a/Userland/Libraries/LibGfx/Bitmap.h +++ b/Userland/Libraries/LibGfx/Bitmap.h @@ -112,7 +112,7 @@ class Bitmap : public RefCounted { static RefPtr create_shareable(BitmapFormat, const IntSize&, int intrinsic_scale = 1); static RefPtr create_purgeable(BitmapFormat, const IntSize&, int intrinsic_scale = 1); static RefPtr create_wrapper(BitmapFormat, const IntSize&, int intrinsic_scale, size_t pitch, void*); - static RefPtr load_from_file(const StringView& path); // FIXME: scale factor + static RefPtr load_from_file(const StringView& path, int scale_factor = 1); static RefPtr create_with_anon_fd(BitmapFormat, int anon_fd, const IntSize&, int intrinsic_scale, const Vector& palette, ShouldCloseAnonymousFile); static RefPtr create_from_serialized_byte_buffer(ByteBuffer&& buffer); static bool is_path_a_supported_image_format(const StringView& path) diff --git a/Userland/Services/WindowServer/Compositor.cpp b/Userland/Services/WindowServer/Compositor.cpp index a5d69df51339e1..703ea77a191c0c 100644 --- a/Userland/Services/WindowServer/Compositor.cpp +++ b/Userland/Services/WindowServer/Compositor.cpp @@ -713,7 +713,10 @@ bool Compositor::set_resolution(int desired_width, int desired_height, int scale return false; } + int old_scale_factor = Screen::the().scale_factor(); bool success = Screen::the().set_resolution(desired_width, desired_height, scale_factor); + if (success && old_scale_factor != scale_factor) + WindowManager::the().reload_icon_bitmaps_after_scale_change(); init_bitmaps(); invalidate_occlusions(); compose(); diff --git a/Userland/Services/WindowServer/Cursor.cpp b/Userland/Services/WindowServer/Cursor.cpp index db18aa42818373..dcc10d8e6b5ecb 100644 --- a/Userland/Services/WindowServer/Cursor.cpp +++ b/Userland/Services/WindowServer/Cursor.cpp @@ -46,7 +46,8 @@ CursorParams CursorParams::parse_from_file_name(const StringView& cursor_path, c auto params_str = file_title.substring_view(last_dot_in_title.value() + 1); CursorParams params(default_hotspot); - for (size_t i = 0; i + 1 < params_str.length();) { + bool in_display_scale_part = false; + for (size_t i = 0; i + 1 < params_str.length() && !in_display_scale_part;) { auto property = params_str[i++]; auto value = [&]() -> Optional { @@ -88,6 +89,9 @@ CursorParams CursorParams::parse_from_file_name(const StringView& cursor_path, c else dbgln("Cursor frame rate outside of valid range (100-1000ms)"); break; + case '-': + in_display_scale_part = true; + break; default: dbg() << "Ignore unknown property '" << property << "' with value " << value.value() << " parsed from cursor path: " << cursor_path; return { default_hotspot }; diff --git a/Userland/Services/WindowServer/WindowFrame.cpp b/Userland/Services/WindowServer/WindowFrame.cpp index fee0e2a939beaf..c4e4dd0f2a937c 100644 --- a/Userland/Services/WindowServer/WindowFrame.cpp +++ b/Userland/Services/WindowServer/WindowFrame.cpp @@ -57,6 +57,7 @@ static Gfx::Bitmap* s_restore_icon; static Gfx::Bitmap* s_close_icon; static String s_last_title_button_icons_path; +static int s_last_title_button_icons_scale; WindowFrame::WindowFrame(Window& window) : m_window(window) @@ -96,34 +97,35 @@ void WindowFrame::set_button_icons() return; String icons_path = WindowManager::the().palette().title_button_icons_path(); + int icons_scale = WindowManager::the().compositor_icon_scale(); StringBuilder full_path; - if (!s_minimize_icon || s_last_title_button_icons_path != icons_path) { + if (!s_minimize_icon || s_last_title_button_icons_path != icons_path || s_last_title_button_icons_scale != icons_scale) { full_path.append(icons_path); full_path.append("window-minimize.png"); - if (!(s_minimize_icon = Gfx::Bitmap::load_from_file(full_path.to_string()).leak_ref())) - s_minimize_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/downward-triangle.png").leak_ref(); + if (!(s_minimize_icon = Gfx::Bitmap::load_from_file(full_path.to_string(), icons_scale).leak_ref())) + s_minimize_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/downward-triangle.png", icons_scale).leak_ref(); full_path.clear(); } - if (!s_maximize_icon || s_last_title_button_icons_path != icons_path) { + if (!s_maximize_icon || s_last_title_button_icons_path != icons_path || s_last_title_button_icons_scale != icons_scale) { full_path.append(icons_path); full_path.append("window-maximize.png"); - if (!(s_maximize_icon = Gfx::Bitmap::load_from_file(full_path.to_string()).leak_ref())) - s_maximize_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/upward-triangle.png").leak_ref(); + if (!(s_maximize_icon = Gfx::Bitmap::load_from_file(full_path.to_string(), icons_scale).leak_ref())) + s_maximize_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/upward-triangle.png", icons_scale).leak_ref(); full_path.clear(); } - if (!s_restore_icon || s_last_title_button_icons_path != icons_path) { + if (!s_restore_icon || s_last_title_button_icons_path != icons_path || s_last_title_button_icons_scale != icons_scale) { full_path.append(icons_path); full_path.append("window-restore.png"); - if (!(s_restore_icon = Gfx::Bitmap::load_from_file(full_path.to_string()).leak_ref())) - s_restore_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-restore.png").leak_ref(); + if (!(s_restore_icon = Gfx::Bitmap::load_from_file(full_path.to_string(), icons_scale).leak_ref())) + s_restore_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-restore.png", icons_scale).leak_ref(); full_path.clear(); } - if (!s_close_icon || s_last_title_button_icons_path != icons_path) { + if (!s_close_icon || s_last_title_button_icons_path != icons_path || s_last_title_button_icons_scale != icons_scale) { full_path.append(icons_path); full_path.append("window-close.png"); - if (!(s_close_icon = Gfx::Bitmap::load_from_file(full_path.to_string()).leak_ref())) - s_close_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-close.png").leak_ref(); + if (!(s_close_icon = Gfx::Bitmap::load_from_file(full_path.to_string(), icons_scale).leak_ref())) + s_close_icon = Gfx::Bitmap::load_from_file("/res/icons/16x16/window-close.png", icons_scale).leak_ref(); full_path.clear(); } @@ -134,6 +136,7 @@ void WindowFrame::set_button_icons() m_maximize_button->set_icon(m_window.is_maximized() ? *s_restore_icon : *s_maximize_icon); s_last_title_button_icons_path = icons_path; + s_last_title_button_icons_scale = icons_scale; } void WindowFrame::did_set_maximized(Badge, bool maximized) diff --git a/Userland/Services/WindowServer/WindowManager.cpp b/Userland/Services/WindowServer/WindowManager.cpp index 5c34a19dae7df0..c0be674b957e4e 100644 --- a/Userland/Services/WindowServer/WindowManager.cpp +++ b/Userland/Services/WindowServer/WindowManager.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -84,7 +85,7 @@ NonnullRefPtr WindowManager::get_cursor(const String& name) { static const auto s_default_cursor_path = "/res/cursors/arrow.x2y2.png"; auto path = m_config->read_entry("Cursor", name, s_default_cursor_path); - auto gb = Gfx::Bitmap::load_from_file(path); + auto gb = Gfx::Bitmap::load_from_file(path, compositor_icon_scale()); if (gb) return Cursor::create(*gb, path); return Cursor::create(*Gfx::Bitmap::load_from_file(s_default_cursor_path), s_default_cursor_path); @@ -1092,6 +1093,12 @@ void WindowManager::event(Core::Event& event) return; } + if (key_event.type() == Event::KeyDown && (key_event.modifiers() == (Mod_Ctrl | Mod_Logo | Mod_Shift) && key_event.key() == Key_I)) { + reload_icon_bitmaps_after_scale_change(!m_allow_hidpi_icons); + Compositor::the().invalidate_screen(); + return; + } + if (MenuManager::the().current_menu()) { MenuManager::the().dispatch_event(event); return; @@ -1486,4 +1493,21 @@ Gfx::IntPoint WindowManager::get_recommended_window_position(const Gfx::IntPoint return point; } + +int WindowManager::compositor_icon_scale() const +{ + if (!m_allow_hidpi_icons) + return 1; + return scale_factor(); +} + +void WindowManager::reload_icon_bitmaps_after_scale_change(bool allow_hidpi_icons) +{ + m_allow_hidpi_icons = allow_hidpi_icons; + reload_config(); + for_each_window([&](Window& window) { + window.frame().set_button_icons(); + return IterationDecision::Continue; + }); +} } diff --git a/Userland/Services/WindowServer/WindowManager.h b/Userland/Services/WindowServer/WindowManager.h index 37687119cca7d1..5caa7a878dba40 100644 --- a/Userland/Services/WindowServer/WindowManager.h +++ b/Userland/Services/WindowServer/WindowManager.h @@ -230,6 +230,9 @@ class WindowManager : public Core::Object { Gfx::IntPoint get_recommended_window_position(const Gfx::IntPoint& desired); + int compositor_icon_scale() const; + void reload_icon_bitmaps_after_scale_change(bool allow_hidpi_icons = true); + private: NonnullRefPtr get_cursor(const String& name); @@ -263,6 +266,7 @@ class WindowManager : public Core::Object { void do_move_to_front(Window&, bool, bool); + bool m_allow_hidpi_icons { true }; RefPtr m_hidden_cursor; RefPtr m_arrow_cursor; RefPtr m_hand_cursor;