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.
Author: https://github.com/tomuta Commit: https://github.com/SerenityOS/serenity/commit/07badd95303 Pull-request: https://github.com/SerenityOS/serenity/pull/4441
|
@ -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]
|
||||
|
|
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 415 B |
Before Width: | Height: | Size: 241 B After Width: | Height: | Size: 241 B |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
BIN
Base/res/cursors/wait.f14t100.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 4.1 KiB |
|
@ -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 != ¤t_cursor)
|
||||
change_cursor(¤t_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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|