WindowServer: Add the ability to animate cursors

This adds the ability to specify cursor attributes as part of their
file names, which allows us to remove hard coded values like the hot
spot from the code. The attributes can be specified between the last
two dots of the file name. Each attribute begins with a character,
followed by one or more digits that specify a uint value.

Supported attributes:
x: The x-coordinate of the cursor hotspot
y: The y-coordinate of the cursor hotspot
f: The number of animated frames horizontally in the image
t: The number of milliseconds per frame

For example, the filename wait.f14t100.png specifies that the image
contains 14 frames that should be cycled through at a rate of 100ms.
The hotspot is not specified, so it defaults to the center.
This commit is contained in:
Tom 2020-12-16 22:15:14 -07:00 committed by Andreas Kling
parent 853664bd3c
commit 07badd9530
12 changed files with 188 additions and 39 deletions

View file

@ -7,7 +7,7 @@ Name=Default
[Cursor]
Hidden=/res/cursors/hidden.png
Arrow=/res/cursors/arrow.png
Arrow=/res/cursors/arrow.x2y2.png
ResizeH=/res/cursors/resize-horizontal.png
ResizeV=/res/cursors/resize-vertical.png
ResizeDTLBR=/res/cursors/resize-diagonal-tlbr.png
@ -17,10 +17,10 @@ ResizeRow=/res/cursors/resize-row.png
IBeam=/res/cursors/i-beam.png
Disallowed=/res/cursors/disallowed.png
Move=/res/cursors/move.png
Hand=/res/cursors/hand.png
Help=/res/cursors/help.png
Hand=/res/cursors/hand.x8y4.png
Help=/res/cursors/help.x1y1.png
Drag=/res/cursors/drag.png
Wait=/res/cursors/wait.png
Wait=/res/cursors/wait.f14t100.png
Crosshair=/res/cursors/crosshair.png
[Input]

View file

Before

Width:  |  Height:  |  Size: 415 B

After

Width:  |  Height:  |  Size: 415 B

View file

Before

Width:  |  Height:  |  Size: 241 B

After

Width:  |  Height:  |  Size: 241 B

View file

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -118,6 +118,12 @@ void Compositor::compose()
m_wallpaper_mode = mode_to_enum(wm.config()->read_entry("Background", "Mode", "simple"));
auto& ws = Screen::the();
{
auto& current_cursor = wm.active_cursor();
if (m_current_cursor != &current_cursor)
change_cursor(&current_cursor);
}
if (!m_invalidated_any) {
// nothing dirtied since the last compose pass.
return;
@ -705,17 +711,21 @@ bool Compositor::set_resolution(int desired_width, int desired_height)
Gfx::IntRect Compositor::current_cursor_rect() const
{
auto& wm = WindowManager::the();
return { Screen::the().cursor_location().translated(-wm.active_cursor().hotspot()), wm.active_cursor().size() };
auto& current_cursor = m_current_cursor ? *m_current_cursor : wm.active_cursor();
return { Screen::the().cursor_location().translated(-current_cursor.params().hotspot()), current_cursor.size() };
}
void Compositor::invalidate_cursor()
void Compositor::invalidate_cursor(bool compose_immediately)
{
if (m_invalidated_cursor)
return;
m_invalidated_cursor = true;
m_invalidated_any = true;
start_compose_async_timer();
if (compose_immediately)
compose();
else
start_compose_async_timer();
}
bool Compositor::draw_geometry_label(Gfx::IntRect& geometry_label_rect)
@ -742,6 +752,29 @@ bool Compositor::draw_geometry_label(Gfx::IntRect& geometry_label_rect)
return true;
}
void Compositor::change_cursor(const Cursor* cursor)
{
if (m_current_cursor == cursor)
return;
m_current_cursor = cursor;
m_current_cursor_frame = 0;
if (m_cursor_timer) {
m_cursor_timer->stop();
m_cursor_timer = nullptr;
}
if (cursor && cursor->params().frames() > 1 && cursor->params().frame_ms() != 0) {
m_cursor_timer = add<Core::Timer>(
cursor->params().frame_ms(), [this, cursor] {
if (m_current_cursor != cursor)
return;
auto frames = cursor->params().frames();
if (++m_current_cursor_frame >= frames)
m_current_cursor_frame = 0;
invalidate_cursor(true);
});
}
}
void Compositor::draw_cursor(const Gfx::IntRect& cursor_rect)
{
auto& wm = WindowManager::the();
@ -751,9 +784,10 @@ void Compositor::draw_cursor(const Gfx::IntRect& cursor_rect)
m_cursor_back_painter = make<Gfx::Painter>(*m_cursor_back_bitmap);
}
m_cursor_back_painter->blit({ 0, 0 }, *m_back_bitmap, wm.active_cursor().rect().translated(cursor_rect.location()).intersected(Screen::the().rect()));
auto& current_cursor = m_current_cursor ? *m_current_cursor : wm.active_cursor();
m_cursor_back_painter->blit({ 0, 0 }, *m_back_bitmap, current_cursor.rect().translated(cursor_rect.location()).intersected(Screen::the().rect()));
auto& back_painter = *m_back_painter;
back_painter.blit(cursor_rect.location(), wm.active_cursor().bitmap(), wm.active_cursor().rect());
back_painter.blit(cursor_rect.location(), current_cursor.bitmap(), current_cursor.source_rect(m_current_cursor_frame));
m_last_cursor_rect = cursor_rect;
}

View file

@ -65,7 +65,7 @@ public:
bool set_wallpaper(const String& path, Function<void(bool)>&& callback);
String wallpaper_path() const { return m_wallpaper_path; }
void invalidate_cursor();
void invalidate_cursor(bool = false);
Gfx::IntRect current_cursor_rect() const;
void increment_display_link_count(Badge<ClientConnection>);
@ -84,6 +84,7 @@ private:
void start_compose_async_timer();
void recompute_occlusions();
bool any_opaque_window_above_this_one_contains_rect(const Window&, const Gfx::IntRect&);
void change_cursor(const Cursor*);
void draw_cursor(const Gfx::IntRect&);
void restore_cursor_back();
bool draw_geometry_label(Gfx::IntRect&);
@ -118,6 +119,10 @@ private:
WallpaperMode m_wallpaper_mode { WallpaperMode::Unchecked };
RefPtr<Gfx::Bitmap> m_wallpaper;
const Cursor* m_current_cursor { nullptr };
unsigned m_current_cursor_frame { 0 };
RefPtr<Core::Timer> m_cursor_timer;
RefPtr<Core::Timer> m_display_link_notify_timer;
size_t m_display_link_count { 0 };
};

View file

@ -24,15 +24,106 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <AK/LexicalPath.h>
#include <WindowServer/Cursor.h>
#include <WindowServer/WindowManager.h>
namespace WindowServer {
Cursor::Cursor(NonnullRefPtr<Gfx::Bitmap>&& bitmap, const Gfx::IntPoint& hotspot)
: m_bitmap(move(bitmap))
, m_hotspot(hotspot)
CursorParams CursorParams::parse_from_file_name(const StringView& cursor_path, const Gfx::IntPoint& default_hotspot)
{
LexicalPath path(cursor_path);
if (!path.is_valid()) {
dbg() << "Cannot parse invalid cursor path, use default cursor params";
return { default_hotspot };
}
auto file_title = path.title();
auto last_dot_in_title = StringView(file_title).find_last_of('.');
if (!last_dot_in_title.has_value() || last_dot_in_title.value() == 0) {
// No encoded params in filename. Not an error, we'll just use defaults
return { default_hotspot };
}
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();) {
auto property = params_str[i++];
auto value = [&]() -> Optional<size_t> {
size_t k = i;
while (k < params_str.length()) {
auto ch = params_str[k];
if (ch < '0' || ch > '9')
break;
k++;
}
if (k == i)
return {};
auto parsed_number = params_str.substring_view(i, k - i).to_uint();
if (!parsed_number.has_value())
return {};
i = k;
return parsed_number.value();
}();
if (!value.has_value()) {
dbg() << "Failed to parse value for property '" << property << "' from parsed cursor path: " << cursor_path;
return { default_hotspot };
}
switch (property) {
case 'x':
params.m_hotspot.set_x(value.value());
params.m_have_hotspot = true;
break;
case 'y':
params.m_hotspot.set_y(value.value());
params.m_have_hotspot = true;
break;
case 'f':
if (value.value() > 1)
params.m_frames = value.value();
break;
case 't':
if (value.value() >= 100 && value.value() <= 1000)
params.m_frame_ms = value.value();
else
dbg() << "Cursor frame rate outside of valid range (100-1000ms)";
break;
default:
dbg() << "Ignore unknown property '" << property << "' with value " << value.value() << " parsed from cursor path: " << cursor_path;
return { default_hotspot };
}
}
return params;
}
CursorParams CursorParams::constrained(const Gfx::Bitmap& bitmap) const
{
CursorParams params(*this);
auto rect = bitmap.rect();
if (params.m_frames > 1) {
if (rect.width() % params.m_frames == 0) {
rect.set_width(rect.width() / (int)params.m_frames);
} else {
dbg() << "Cannot divide cursor dimensions " << rect << " into " << params.m_frames << " frames";
params.m_frames = 1;
}
}
if (params.m_have_hotspot)
params.m_hotspot = params.m_hotspot.constrained(rect);
else
params.m_hotspot = rect.center();
return params;
}
Cursor::Cursor(NonnullRefPtr<Gfx::Bitmap>&& bitmap, const CursorParams& cursor_params)
: m_bitmap(move(bitmap))
, m_params(cursor_params.constrained(*m_bitmap))
, m_rect(m_bitmap->rect())
{
if (m_params.frames() > 1) {
ASSERT(m_rect.width() % m_params.frames() == 0);
m_rect.set_width(m_rect.width() / m_params.frames());
}
}
Cursor::~Cursor()
@ -41,12 +132,14 @@ Cursor::~Cursor()
NonnullRefPtr<Cursor> Cursor::create(NonnullRefPtr<Gfx::Bitmap>&& bitmap)
{
return adopt(*new Cursor(move(bitmap), bitmap->rect().center()));
auto hotspot = bitmap->rect().center();
return adopt(*new Cursor(move(bitmap), CursorParams(hotspot)));
}
NonnullRefPtr<Cursor> Cursor::create(NonnullRefPtr<Gfx::Bitmap>&& bitmap, const Gfx::IntPoint& hotspot)
NonnullRefPtr<Cursor> Cursor::create(NonnullRefPtr<Gfx::Bitmap>&& bitmap, const StringView& filename)
{
return adopt(*new Cursor(move(bitmap), hotspot));
auto default_hotspot = bitmap->rect().center();
return adopt(*new Cursor(move(bitmap), CursorParams::parse_from_file_name(filename, default_hotspot)));
}
RefPtr<Cursor> Cursor::create(Gfx::StandardCursor standard_cursor)

View file

@ -31,24 +31,51 @@
namespace WindowServer {
class CursorParams {
public:
static CursorParams parse_from_file_name(const StringView&, const Gfx::IntPoint&);
CursorParams(const Gfx::IntPoint& hotspot)
: m_hotspot(hotspot)
{
}
CursorParams constrained(const Gfx::Bitmap&) const;
const Gfx::IntPoint& hotspot() const { return m_hotspot; }
unsigned frames() const { return m_frames; }
unsigned frame_ms() const { return m_frame_ms; }
private:
CursorParams() = default;
Gfx::IntPoint m_hotspot;
unsigned m_frames { 1 };
unsigned m_frame_ms { 0 };
bool m_have_hotspot { false };
};
class Cursor : public RefCounted<Cursor> {
public:
static NonnullRefPtr<Cursor> create(NonnullRefPtr<Gfx::Bitmap>&&, const Gfx::IntPoint& hotspot);
static NonnullRefPtr<Cursor> create(NonnullRefPtr<Gfx::Bitmap>&&, const StringView&);
static NonnullRefPtr<Cursor> create(NonnullRefPtr<Gfx::Bitmap>&&);
static RefPtr<Cursor> create(Gfx::StandardCursor);
~Cursor();
Gfx::IntPoint hotspot() const { return m_hotspot; }
const CursorParams& params() const { return m_params; }
const Gfx::Bitmap& bitmap() const { return *m_bitmap; }
Gfx::IntRect rect() const { return m_bitmap->rect(); }
Gfx::IntSize size() const { return m_bitmap->size(); }
Gfx::IntRect source_rect(unsigned frame) const
{
return m_rect.translated(frame * m_rect.width(), 0);
}
Gfx::IntRect rect() const { return m_rect; }
Gfx::IntSize size() const { return m_rect.size(); }
private:
Cursor(NonnullRefPtr<Gfx::Bitmap>&&, const Gfx::IntPoint&);
Cursor(NonnullRefPtr<Gfx::Bitmap>&&, const CursorParams&);
RefPtr<Gfx::Bitmap> m_bitmap;
Gfx::IntPoint m_hotspot;
CursorParams m_params;
Gfx::IntRect m_rect;
};
}

View file

@ -83,23 +83,14 @@ WindowManager::~WindowManager()
{
}
NonnullRefPtr<Cursor> WindowManager::get_cursor(const String& name, const Gfx::IntPoint& hotspot)
{
auto path = m_config->read_entry("Cursor", name, "/res/cursors/arrow.png");
auto gb = Gfx::Bitmap::load_from_file(path);
if (gb)
return Cursor::create(*gb, hotspot);
return Cursor::create(*Gfx::Bitmap::load_from_file("/res/cursors/arrow.png"));
}
NonnullRefPtr<Cursor> WindowManager::get_cursor(const String& name)
{
auto path = m_config->read_entry("Cursor", name, "/res/cursors/arrow.png");
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);
if (gb)
return Cursor::create(*gb);
return Cursor::create(*Gfx::Bitmap::load_from_file("/res/cursors/arrow.png"));
return Cursor::create(*gb, path);
return Cursor::create(*Gfx::Bitmap::load_from_file(s_default_cursor_path), s_default_cursor_path);
}
void WindowManager::reload_config(bool set_screen)
@ -113,9 +104,9 @@ void WindowManager::reload_config(bool set_screen)
}
m_hidden_cursor = get_cursor("Hidden");
m_arrow_cursor = get_cursor("Arrow", { 2, 2 });
m_hand_cursor = get_cursor("Hand", { 8, 4 });
m_help_cursor = get_cursor("Help", { 1, 1 });
m_arrow_cursor = get_cursor("Arrow");
m_hand_cursor = get_cursor("Hand");
m_help_cursor = get_cursor("Help");
m_resize_horizontally_cursor = get_cursor("ResizeH");
m_resize_vertically_cursor = get_cursor("ResizeV");
m_resize_diagonally_tlbr_cursor = get_cursor("ResizeDTLBR");

View file

@ -215,7 +215,6 @@ public:
private:
NonnullRefPtr<Cursor> get_cursor(const String& name);
NonnullRefPtr<Cursor> get_cursor(const String& name, const Gfx::IntPoint& hotspot);
void process_mouse_event(MouseEvent&, Window*& hovered_window);
void process_event_for_doubleclick(Window& window, MouseEvent& event);