Files
raven/native/desktop/scrollbar.cpp
T

566 lines
21 KiB
C++

#include "windows.h"
#include "canvas.h"
#include "shapes.h"
#include <algorithm>
#include <cstring>
namespace raven {
namespace {
constexpr uint32_t ws_hscroll = 0x100000, ws_vscroll = 0x200000;
constexpr int32_t arrow_size = 17, minimum_thumb = 8;
// Parts of a scroll bar a pointer can hit.
enum Part { none = 0, line_up = 1, line_down = 2, page_up = 3, page_down = 4, thumb = 5 };
int32_t maximum_position(const DesktopScroll &scroll)
{
return scroll.max - (scroll.page ? int32_t(scroll.page) - 1 : 0);
}
bool scrollable(const DesktopScroll &scroll)
{
return scroll.page ? int64_t(scroll.max) - scroll.min + 1 > int64_t(scroll.page) : scroll.max > scroll.min;
}
}
NativeWindows::ScrollGeometry NativeWindows::scroll_geometry(const DesktopWindow &window, int bar) const
{
ScrollGeometry geometry{};
const uint32_t flag = bar == 1 ? ws_vscroll : ws_hscroll;
if (!(window.style & flag))
return geometry;
geometry.present = true;
geometry.vertical = bar == 1;
const int32_t left = window.x, top = window.y, right = window.x + window.width, bottom = window.y + window.height;
if (geometry.vertical)
geometry.bar = {right, top, right + arrow_size, bottom};
else
geometry.bar = {left, bottom, right, bottom + arrow_size};
const auto &box = geometry.bar;
const int32_t length = geometry.vertical ? box.bottom - box.top : box.right - box.left;
const int32_t track_length = std::max(0, length - 2 * arrow_size);
if (geometry.vertical) {
geometry.up = {box.left, box.top, box.right, box.top + std::min(arrow_size, length / 2)};
geometry.down = {box.left, box.bottom - std::min(arrow_size, length / 2), box.right, box.bottom};
geometry.track = {box.left, geometry.up.bottom, box.right, geometry.down.top};
} else {
geometry.up = {box.left, box.top, box.left + std::min(arrow_size, length / 2), box.bottom};
geometry.down = {box.right - std::min(arrow_size, length / 2), box.top, box.right, box.bottom};
geometry.track = {geometry.up.right, box.top, geometry.down.left, box.bottom};
}
const auto &scroll = window.scroll[size_t(bar)];
if (track_length > 0 && scrollable(scroll)) {
const int64_t range = int64_t(scroll.max) - scroll.min + 1;
int32_t thumb_length = scroll.page ? int32_t(int64_t(track_length) * scroll.page / range) : arrow_size;
thumb_length = std::clamp(thumb_length, minimum_thumb, track_length);
const int64_t positions = int64_t(maximum_position(scroll)) - scroll.min;
const int32_t shown = scroll.track != scroll.pos || scroll_drag.window ? scroll.track : scroll.pos;
const int64_t offset = positions > 0 ? (int64_t(std::clamp(shown, scroll.min, maximum_position(scroll))) - scroll.min) * (track_length - thumb_length) / positions : 0;
if (geometry.vertical)
geometry.thumb = {geometry.track.left, geometry.track.top + int32_t(offset), geometry.track.right, geometry.track.top + int32_t(offset) + thumb_length};
else
geometry.thumb = {geometry.track.left + int32_t(offset), geometry.track.top, geometry.track.left + int32_t(offset) + thumb_length, geometry.track.bottom};
geometry.has_thumb = true;
}
return geometry;
}
// Re-lays out a window after its scroll bars or frame changed: recompute the client area and repaint.
void NativeWindows::refresh_frame(uint64_t handle)
{
auto *window = objects.window(handle);
if (!window)
return;
const auto old_width = window->width, old_height = window->height;
recalculate_client(handle);
window = objects.window(handle);
if (!window)
return;
if (window->parent)
objects.update_window_dcs(handle);
if (window->width != old_width || window->height != old_height)
send(handle, 5, 0, uint16_t(window->width) | (uint64_t(uint16_t(window->height)) << 16));
if (objects.window_visible(handle)) {
invalidate_tree(handle, true);
drawing(handle);
}
}
bool NativeWindows::scroll_sync_visibility(uint64_t handle, int bar, bool allow_disable)
{
auto *window = objects.window(handle);
const uint32_t flag = bar == 1 ? ws_vscroll : ws_hscroll;
const bool shown = (window->style & flag) != 0;
const bool want = scrollable(window->scroll[size_t(bar)]) || allow_disable;
if (shown == want)
return false;
window->style = want ? window->style | flag : window->style & ~flag;
return true;
}
void NativeWindows::scroll_notify(uint64_t handle, int bar, uint32_t code, int32_t position)
{
if (!objects.window(handle))
return;
send(handle, bar == 1 ? 0x115 : 0x114, uint64_t(code) | (uint64_t(uint16_t(position)) << 16), 0);
}
void NativeWindows::register_scroll_api(ApiRegistry &registry)
{
const auto bar_of = [](int32_t bar) { return bar == 0 ? 0 : bar == 1 ? 1 : -1; };
registry.add("SetScrollInfo", 4, [this, bar_of](ApiCall &call) {
auto *window = objects.window(call[0]);
if (!window) {
call.fail(1400);
return;
}
const int32_t kind = call.i32(1);
if (kind == 2) {
call.ret(send(call[0], 0xe9, call.u32(3), call[2]));
return;
}
const int bar = bar_of(kind);
if (bar < 0 || !call[2] || call.load<uint32_t>(call[2]) < 28) {
call.fail(87);
return;
}
const uint32_t mask = call.load<uint32_t>(call[2] + 4);
auto &scroll = window->scroll[size_t(bar)];
bool range_changed = false;
if (mask & 1) {
scroll.min = call.load<int32_t>(call[2] + 8);
scroll.max = call.load<int32_t>(call[2] + 12);
if (scroll.max < scroll.min)
scroll.max = scroll.min;
range_changed = true;
}
if (mask & 2) {
scroll.page = call.load<uint32_t>(call[2] + 16);
range_changed = true;
}
if (mask & 4)
scroll.pos = call.load<int32_t>(call[2] + 20);
scroll.pos = std::clamp(scroll.pos, scroll.min, std::max(scroll.min, maximum_position(scroll)));
scroll.track = scroll.pos;
bool relayout = false;
if (range_changed || (mask & 8)) {
const bool keep_disabled = (mask & 8) != 0;
relayout = scroll_sync_visibility(call[0], bar, keep_disabled && (window->style & (bar == 1 ? ws_vscroll : ws_hscroll)));
scroll.disabled = !scrollable(scroll) && keep_disabled ? 3 : 0;
}
if (relayout)
refresh_frame(call[0]);
else if (call.u32(3) && objects.window_visible(call[0]))
drawing(call[0]);
call.ret(uint32_t(scroll.pos));
});
registry.add("GetScrollInfo", 3, [this, bar_of](ApiCall &call) {
const auto *window = objects.window(call[0]);
if (!window) {
call.fail(1400);
return;
}
const int32_t kind = call.i32(1);
if (kind == 2) {
call.ret(send(call[0], 0xea, 0, call[2]) != 0);
return;
}
const int bar = bar_of(kind);
if (bar < 0 || !call[2] || call.load<uint32_t>(call[2]) < 28) {
call.fail(87);
return;
}
const auto &scroll = window->scroll[size_t(bar)];
const uint32_t mask = call.load<uint32_t>(call[2] + 4);
if (mask & 1) {
call.store<int32_t>(call[2] + 8, scroll.min);
call.store<int32_t>(call[2] + 12, scroll.max);
}
if (mask & 2)
call.store<uint32_t>(call[2] + 16, scroll.page);
if (mask & 4)
call.store<int32_t>(call[2] + 20, scroll.pos);
if (mask & 0x10)
call.store<int32_t>(call[2] + 24, scroll.track);
call.ret(1);
});
registry.add("SetScrollPos", 4, [this, bar_of](ApiCall &call) {
auto *window = objects.window(call[0]);
if (!window) {
call.fail(1400);
return;
}
if (call.i32(1) == 2) {
call.ret(send(call[0], 0xe0, uint64_t(call.i32(2)), call.u32(3)));
return;
}
const int bar = bar_of(call.i32(1));
if (bar < 0) {
call.fail(87);
return;
}
auto &scroll = window->scroll[size_t(bar)];
const auto previous = scroll.pos;
scroll.pos = std::clamp(call.i32(2), scroll.min, std::max(scroll.min, maximum_position(scroll)));
scroll.track = scroll.pos;
if (call.u32(3) && objects.window_visible(call[0]))
drawing(call[0]);
call.ret(uint32_t(previous));
});
registry.add("GetScrollPos", 2, [this, bar_of](ApiCall &call) {
const auto *window = objects.window(call[0]);
if (!window) {
call.fail(1400);
return;
}
if (call.i32(1) == 2) {
call.ret(send(call[0], 0xe1, 0, 0));
return;
}
const int bar = bar_of(call.i32(1));
if (bar < 0) {
call.fail(87);
return;
}
call.ret(uint32_t(window->scroll[size_t(bar)].pos));
});
registry.add("SetScrollRange", 5, [this, bar_of](ApiCall &call) {
auto *window = objects.window(call[0]);
if (!window) {
call.fail(1400);
return;
}
if (call.i32(1) == 2) {
call.ret(send(call[0], 0xe2, uint64_t(call.i32(2)), uint64_t(call.i32(3))));
return;
}
const int bar = bar_of(call.i32(1));
if (bar < 0 || call.i32(2) > call.i32(3)) {
call.fail(87);
return;
}
auto &scroll = window->scroll[size_t(bar)];
scroll.min = call.i32(2);
scroll.max = call.i32(3);
scroll.pos = std::clamp(scroll.pos, scroll.min, std::max(scroll.min, maximum_position(scroll)));
scroll.track = scroll.pos;
if (scroll_sync_visibility(call[0], bar, false))
refresh_frame(call[0]);
else if (call.u32(4) && objects.window_visible(call[0]))
drawing(call[0]);
call.ret(1);
});
registry.add("GetScrollRange", 4, [this, bar_of](ApiCall &call) {
const auto *window = objects.window(call[0]);
if (!window) {
call.fail(1400);
return;
}
const int bar = bar_of(call.i32(1));
if (bar < 0 || !call[2] || !call[3]) {
call.fail(87);
return;
}
call.store<int32_t>(call[2], window->scroll[size_t(bar)].min);
call.store<int32_t>(call[3], window->scroll[size_t(bar)].max);
call.ret(1);
});
registry.add("ShowScrollBar", 3, [this, bar_of](ApiCall &call) {
auto *window = objects.window(call[0]);
if (!window) {
call.fail(1400);
return;
}
const int32_t kind = call.i32(1);
if (kind == 2) {
show_window(call[0], call.u32(2) ? 5 : 0);
call.ret(1);
return;
}
bool changed = false;
for (int bar = 0; bar < 2; ++bar) {
if (kind != 3 && bar != bar_of(kind))
continue;
const uint32_t flag = bar == 1 ? ws_vscroll : ws_hscroll;
const bool shown = (window->style & flag) != 0;
if (shown != (call.u32(2) != 0)) {
window->style = call.u32(2) ? window->style | flag : window->style & ~flag;
changed = true;
}
}
if (changed)
refresh_frame(call[0]);
call.ret(1);
});
registry.add("EnableScrollBar", 3, [this, bar_of](ApiCall &call) {
auto *window = objects.window(call[0]);
if (!window) {
call.fail(1400);
return;
}
const int32_t kind = call.i32(1);
if (kind == 2) {
call.ret(send(call[0], 0xe4, call.u32(2), 0));
return;
}
bool changed = false;
for (int bar = 0; bar < 2; ++bar) {
if (kind != 3 && bar != bar_of(kind))
continue;
auto &scroll = window->scroll[size_t(bar)];
changed = changed || scroll.disabled != (call.u32(2) & 3);
scroll.disabled = call.u32(2) & 3;
}
if (changed && objects.window_visible(call[0]))
drawing(call[0]);
call.ret(changed);
});
registry.add("GetScrollBarInfo", 3, [this](ApiCall &call) {
const auto *window = objects.window(call[0]);
if (!window) {
call.fail(1400);
return;
}
const int32_t object = call.i32(1);
if (object != -5 && object != -6) {
call.fail(87);
return;
}
const int bar = object == -5 ? 1 : 0;
const auto geometry = scroll_geometry(*window, bar);
std::vector<uint8_t> bytes(60, 0);
const uint32_t size = 60;
std::memcpy(bytes.data(), &size, 4);
const int32_t box[4] = {geometry.bar.left, geometry.bar.top, geometry.bar.right, geometry.bar.bottom};
std::memcpy(bytes.data() + 4, box, 16);
const int32_t line_button = arrow_size;
std::memcpy(bytes.data() + 20, &line_button, 4);
const int32_t thumb_start = geometry.has_thumb ? (geometry.vertical ? geometry.thumb.top - geometry.bar.top : geometry.thumb.left - geometry.bar.left) : 0;
const int32_t thumb_end = geometry.has_thumb ? (geometry.vertical ? geometry.thumb.bottom - geometry.bar.top : geometry.thumb.right - geometry.bar.left) : 0;
std::memcpy(bytes.data() + 24, &thumb_start, 4);
std::memcpy(bytes.data() + 28, &thumb_end, 4);
// rgstate: the bar itself, then up, page up, thumb, page down, down; STATE_SYSTEM_INVISIBLE is 0x8000.
const uint32_t invisible = geometry.present ? 0 : 0x8000;
for (size_t i = 0; i < 6; ++i)
std::memcpy(bytes.data() + 36 + i * 4, &invisible, 4);
call.write(call[2], bytes);
call.ret(1);
});
}
// Draws the scroll bars of every window of one top-level surface into its bitmap.
void NativeWindows::paint_scrollbars(uint64_t root)
{
const auto *top = objects.window(root);
if (!top || !top->bitmap)
return;
auto *bitmap = objects.bitmap(top->bitmap);
if (!bitmap || bitmap->depth != 32)
return;
using namespace paint;
Canvas canvas{bitmap->pixels().data(), int32_t(bitmap->width), int32_t(bitmap->height)};
const auto color = [&](int index) { return rgb(objects.system_color(index)); };
const int32_t origin_x = top->outer.left, origin_y = top->outer.top;
const auto shifted = [&](DesktopRect rect) { return DesktopRect{rect.left - origin_x, rect.top - origin_y, rect.right - origin_x, rect.bottom - origin_y}; };
for (const auto handle : objects.window_handles()) {
const auto *window = objects.window(handle);
if (!window || !(window->style & (ws_hscroll | ws_vscroll)) || objects.window_root(handle) != root || !objects.window_visible(handle))
continue;
// Drawing is clipped to the window's own outer rectangle and the ancestors' client areas.
for (int bar = 0; bar < 2; ++bar) {
const auto geometry = scroll_geometry(*window, bar);
if (!geometry.present)
continue;
const auto &scroll = window->scroll[size_t(bar)];
const bool dead = !scrollable(scroll) || scroll.disabled == 3;
if (themed()) {
// Windows 11: a light bar, small solid arrows and a rounded thumb narrower than the bar.
struct Surface {
Canvas &canvas;
void blend(int64_t x, int64_t y, uint32_t ink, double alpha)
{
if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height)
return;
const auto *pixel = canvas.pixels + (size_t(y) * size_t(canvas.width) + size_t(x)) * 4;
const uint32_t below = uint32_t(pixel[2]) | uint32_t(pixel[1]) << 8 | uint32_t(pixel[0]) << 16;
canvas.set(int32_t(x), int32_t(y), alpha >= 0.998 ? ink : shapes::mix(below, ink, alpha));
}
} surface{canvas};
shapes::Shapes<Surface> shapes{surface};
const uint32_t ground = 0xf9f9f9, ink = 0x868686, faint = 0xc5c5c5;
canvas.fill(shifted(geometry.bar), ground);
const auto arrow = [&](DesktopRect rect, char direction, bool disabled) {
const auto r = shifted(rect);
if (r.right > r.left && r.bottom > r.top)
shapes.triangle((r.left + r.right) / 2.0, (r.top + r.bottom) / 2.0, 7, direction, disabled ? faint : ink);
};
arrow(geometry.up, geometry.vertical ? 'u' : 'l', dead || (scroll.disabled & 1));
arrow(geometry.down, geometry.vertical ? 'd' : 'r', dead || (scroll.disabled & 2));
if (geometry.has_thumb && !dead) {
const auto r = shifted(geometry.thumb);
const bool held = scroll_drag.window == handle && scroll_drag.bar == bar && scroll_drag.part == Part::thumb;
const uint32_t pill = held ? 0x5a5a5a : 0x868686;
if (geometry.vertical)
shapes.rounded((r.left + r.right) / 2.0 - 3, r.top + 2, (r.left + r.right) / 2.0 + 3, r.bottom - 2, 3, pill, std::nullopt);
else
shapes.rounded(r.left + 2, (r.top + r.bottom) / 2.0 - 3, r.right - 2, (r.top + r.bottom) / 2.0 + 3, 3, pill, std::nullopt);
}
continue;
}
// Track: checkerboard of face and highlight, like the classic dithered scroll bar background.
const auto track = shifted(geometry.track);
for (int32_t y = track.top; y < track.bottom; ++y)
for (int32_t x = track.left; x < track.right; ++x)
canvas.set(x, y, (x + y) % 2 ? color(color_button_highlight) : color(color_button_face));
const auto button = [&](DesktopRect rect, int direction, bool disabled) {
const auto r = shifted(rect);
if (r.right <= r.left || r.bottom <= r.top)
return;
canvas.fill(r, color(color_button_face));
canvas.edge(r, color(color_button_highlight), color(color_dark_shadow), color(color_light), color(color_button_shadow));
const int32_t cx = (r.left + r.right) / 2, cy = (r.top + r.bottom) / 2;
const uint32_t ink = disabled ? color(color_button_shadow) : color(18);
for (int32_t d = 0; d < 4; ++d)
for (int32_t w = -(3 - d); w <= 3 - d; ++w)
switch (direction) {
case 0: canvas.set(cx + w, cy + 1 - d, ink); break;
case 1: canvas.set(cx + w, cy - 2 + d, ink); break;
case 2: canvas.set(cx + 1 - d, cy + w, ink); break;
default: canvas.set(cx - 2 + d, cy + w, ink); break;
}
};
button(geometry.up, geometry.vertical ? 0 : 2, dead || (scroll.disabled & 1));
button(geometry.down, geometry.vertical ? 1 : 3, dead || (scroll.disabled & 2));
if (geometry.has_thumb && !dead) {
const auto r = shifted(geometry.thumb);
canvas.fill(r, color(color_button_face));
canvas.edge(r, color(color_button_highlight), color(color_dark_shadow), color(color_light), color(color_button_shadow));
}
}
// Where both bars meet, the corner box takes the face color.
if ((window->style & ws_hscroll) && (window->style & ws_vscroll))
canvas.fill(shifted({window->x + window->width, window->y + window->height, window->x + window->width + arrow_size, window->y + window->height + arrow_size}),
themed() ? 0xf9f9f9 : color(color_button_face));
}
}
int NativeWindows::scroll_part_at(const DesktopWindow &window, int bar, int32_t x, int32_t y) const
{
const auto geometry = scroll_geometry(window, bar);
const auto inside = [&](const DesktopRect &r) { return x >= r.left && x < r.right && y >= r.top && y < r.bottom; };
if (!geometry.present || !inside(geometry.bar))
return none;
if (inside(geometry.up))
return line_up;
if (inside(geometry.down))
return line_down;
if (geometry.has_thumb && inside(geometry.thumb))
return thumb;
if (!inside(geometry.track) || !scrollable(window.scroll[size_t(bar)]))
return none;
const bool before = geometry.vertical ? y < geometry.thumb.top : x < geometry.thumb.left;
return before ? page_up : page_down;
}
// Hit test for the non-client scroll bars: HTHSCROLL (6) or HTVSCROLL (7), zero elsewhere.
uint64_t NativeWindows::scroll_hit(const DesktopWindow &window, int32_t x, int32_t y) const
{
for (int bar = 0; bar < 2; ++bar) {
const auto geometry = scroll_geometry(window, bar);
if (geometry.present && x >= geometry.bar.left && x < geometry.bar.right && y >= geometry.bar.top && y < geometry.bar.bottom)
return bar == 1 ? 7 : 6;
}
return 0;
}
bool NativeWindows::scroll_pointer(uint64_t handle, uint64_t hit, uint32_t message, int32_t x, int32_t y)
{
auto *window = objects.window(handle);
if (!window || (hit != 6 && hit != 7))
return false;
if (message != 0x201)
return true;
const int bar = hit == 7 ? 1 : 0;
const int part = scroll_part_at(*window, bar, x, y);
if (part == none)
return true;
auto &scroll = window->scroll[size_t(bar)];
if (!scrollable(scroll) || scroll.disabled == 3)
return true;
const auto geometry = scroll_geometry(*window, bar);
scroll_drag = {handle, bar, part, 0, std::chrono::steady_clock::now() + std::chrono::milliseconds(400)};
if (part == thumb) {
scroll_drag.grab = geometry.vertical ? y - geometry.thumb.top : x - geometry.thumb.left;
scroll.track = scroll.pos;
} else {
static constexpr uint32_t codes[] = {0, 0, 1, 2, 3};
scroll_notify(handle, bar, codes[part] , scroll.pos);
}
return true;
}
// Pointer motion and release while a scroll bar is being operated; returns true when the event was consumed.
bool NativeWindows::scroll_motion(uint32_t message, int32_t x, int32_t y)
{
if (!scroll_drag.window)
return false;
auto *window = objects.window(scroll_drag.window);
if (!window) {
scroll_drag = {};
return true;
}
const auto handle = scroll_drag.window;
const int bar = scroll_drag.bar;
auto &scroll = window->scroll[size_t(bar)];
if (message == 0x202) {
const bool was_thumb = scroll_drag.part == thumb;
scroll_drag = {};
if (was_thumb)
scroll_notify(handle, bar, 4, scroll.track);
scroll_notify(handle, bar, 8, scroll.pos);
scroll.track = scroll.pos;
if (objects.window_visible(handle))
drawing(handle);
return true;
}
if (scroll_drag.part == thumb) {
const auto geometry = scroll_geometry(*window, bar);
const int32_t thumb_length = geometry.vertical ? geometry.thumb.bottom - geometry.thumb.top : geometry.thumb.right - geometry.thumb.left;
const int32_t track_length = geometry.vertical ? geometry.track.bottom - geometry.track.top : geometry.track.right - geometry.track.left;
const int32_t start = geometry.vertical ? geometry.track.top : geometry.track.left;
const int32_t cursor = geometry.vertical ? y : x;
const int64_t slack = std::max(1, track_length - thumb_length);
const int64_t offset = std::clamp<int64_t>(cursor - scroll_drag.grab - start, 0, slack);
const int64_t positions = int64_t(maximum_position(scroll)) - scroll.min;
scroll.track = scroll.min + int32_t(offset * positions / slack);
scroll_notify(handle, bar, 5, scroll.track);
if (objects.window_visible(handle))
drawing(handle);
}
return true;
}
// Holding an arrow or the page area repeats its scroll code.
void NativeWindows::scroll_idle()
{
if (!scroll_drag.window || scroll_drag.part == thumb)
return;
const auto now = std::chrono::steady_clock::now();
if (now < scroll_drag.next)
return;
scroll_drag.next = now + std::chrono::milliseconds(50);
const auto *window = objects.window(scroll_drag.window);
if (!window) {
scroll_drag = {};
return;
}
static constexpr uint32_t codes[] = {0, 0, 1, 2, 3};
scroll_notify(scroll_drag.window, scroll_drag.bar, codes[scroll_drag.part], window->scroll[size_t(scroll_drag.bar)].pos);
}
}