Compare commits

...

37 Commits

Author SHA1 Message Date
nullicorn 30d9c599e9
Merge e9328aa1e1 into 6d89a6bc15 2025-07-24 17:59:49 -04:00
Cerallin 6d89a6bc15 fix entry
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-24 12:50:32 -03:00
Cerallin d4e97b5a96 Add const method Entry::caretPosOnScreen()
This method is for Entry::setTextInput() and IME positioning.
2025-07-24 12:50:32 -03:00
Cerallin 205b18dc0f Make Entry::getCharBoxBounds() a const method 2025-07-24 12:50:32 -03:00
David Capello 2ba051b59b Fix crash deselecting moved pixels when using certain extensions (fix #5280)
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
Although the issue refers to deselecting MovingPixelsState, the same
crash could happen when canceling/finishing WritingTextState or
MovingSelectionState. This fixes the crash for all these states.
2025-07-23 19:01:37 -03:00
Christian Kaiser 3fcb000eb1 Fix slice transformations not updating editors
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-22 02:11:07 -03:00
David Capello af9dc3c817 Fix brush boundaries accumulation switching brush type only (fix #5281)
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-07-21 17:28:11 -03:00
David Capello 250dfdc86a Convert the brush generation counter into an atomic var 2025-07-21 17:27:23 -03:00
David Capello c904c41b39 Don't show stroke/fill option for theme fonts
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-18 18:48:35 -03:00
David Capello 9e941e9a8b [win32] Fix listing hidden files on Windows (related to #5269 / #3079) 2025-07-18 18:37:44 -03:00
Liebranca bbab4d5875 Add 'Show hidden' check to file selector 2025-07-18 18:26:35 -03:00
David Capello 5c4daff128 Add options to stroke/fill text (fix #5271)
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-14 23:17:25 -03:00
David Capello 11a7b061ff Remove unused var 2025-07-14 20:24:32 -03:00
David Capello 283bedf77e Add pinned/recent folders to export file popup menus
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
Related to https://community.aseprite.org/t/25920
2025-07-10 18:55:28 -03:00
David Capello 2eeb6f04a7 Fix "buttononly" bool attribute for <filename> widget 2025-07-10 18:13:10 -03:00
David Capello 706d0b8a7a Add possibility to select export file names w/"one click"
It's not with one click, it requires dragging the mouse but it's
better than two clicks.

Related to https://community.aseprite.org/t/25920
2025-07-10 18:12:38 -03:00
David Capello 2f3a7f5dec Update to new laf API: setTranslateDeadKeys -> setTextInput
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-07-10 16:06:03 -03:00
David Capello d5de74b715 Update laf module
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
2025-07-10 08:52:01 -03:00
David Capello 2d87a7b184 [ui] Fix drawing cursor at the end of the Entry/Combobox field
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
Regression introduced in 1a6a39700e
2025-07-07 22:21:58 -03:00
Gaspar Capello 2f22804fe8 Fix GIF animations with semi-transparent colors (fix #5226)
Prior to this fix, animations were incorrectly converted to GIF when
the animation contained semi-transparent colors or backgrounds
(specifically, colors with alpha <= 128). Essentially, semi-transparent
pixels didn't repaint the pixel of the processed frame, meaning they
retained the color from the previous frame.
2025-07-07 17:43:19 -03:00
Gaspar Capello bf1b4c6f50 Fix session 'isRunning' detection (fix #5252)
Before this fix, sometimes files available for recovery aren't
displayed due to the coincidence of the pid number of a crashed
session with the current pid number of the current session. This
coincidence caused Aseprite to falsely detect that it was
the current session.
2025-07-07 17:15:36 -03:00
David Capello 250244c777 [ui] Fix possible crash when there is no TextBlob to paint (fix #5255)
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-07-07 12:39:12 -03:00
David Capello b4555fc098 Add "Initial State" string for "Undo History" window to en.ini
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-03 11:18:21 -03:00
Gaspar Capello 8783135bf7 Fix convert_image_to_surface function (fix #5257)
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
Before this fix, 'convert_image_to_surface' would generate saturated
colors when converting semi-transparent colors. This happened because
 Skia Surfaces assumed colors with pre-multiplied alpha.
2025-07-02 20:44:33 -03:00
David Capello 1a6a39700e Fix suffix separation in Entry fields w/TTF fonts (fix #5261) 2025-07-02 20:43:17 -03:00
David Capello ce742bcbc1 Fix typo in GitHub profile URL
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-07-01 21:44:51 -03:00
David Capello 3c350c3e67 Update laf module
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-06-25 08:20:50 -03:00
David Capello 220d2d3a2d Update laf module
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-06-24 11:16:06 -03:00
Martín Capello 322040c4fb Recover Windows Vista compatibility (fix #5211) 2025-06-24 10:10:55 -03:00
David Capello ee69a2f2f6 Fix baseline alignment of file list items/names w/multiple fonts/emojis
build-auto / build-auto (Debug, macos-latest) (push) Waiting to run Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Waiting to run Details
build-auto / build-auto (Debug, windows-latest) (push) Waiting to run Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Waiting to run Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Waiting to run Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Waiting to run Details
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-06-23 23:03:16 -03:00
David Capello 91f8410749 Fix bug in news items when multiple fonts are needed (fix #5221) 2025-06-23 22:49:05 -03:00
Gaspar Capello 8d5534d4eb Fix do not move the pivot when dragging a selection with Alt key to snap to grid (fix #5238) 2025-06-23 18:54:32 -03:00
David Capello 2ee8c68d94 Update laf module (fix #5240) 2025-06-23 17:56:38 -03:00
David Capello f7040190cc Fix implicit conversion of nullptr constant to 'bool'
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-06-23 09:12:36 -03:00
David Capello 68bf0839aa Don't use userConfigPath to output automated test files 2025-06-23 09:08:02 -03:00
Necronomison e5917389cb
Export user & extension defined properties (fix #5186, #5187) 2025-06-23 09:06:05 -03:00
Nullicorn e9328aa1e1
Fix 'Bad substitution' in Linux thumbnailer
- Change shebang from `sh` to `bash` to support the script's existing syntax

Signed-off-by: Nullicorn <git@nullicorn.me>
2024-02-21 22:39:11 -05:00
50 changed files with 803 additions and 273 deletions

View File

@ -4,7 +4,7 @@ Aseprite is being developed and maintained currently by [Igara Studio](https://i
The active team of developers is:
* [David Capello](https://github.com/dacap)
* [Gaspar Capello](https://github.com/Gsparoken)
* [Gaspar Capello](https://github.com/Gasparoken)
* [Martín Capello](https://github.com/martincapello)
* [Christian Kaiser](https://github.com/ckaiser)
* [Dante Paola](https://github.com/Liebranca)

View File

@ -356,6 +356,7 @@
<section id="file_selector">
<option id="current_folder" type="std::string" default="&quot;&lt;empty&gt;&quot;" />
<option id="zoom" type="double" default="1.0" />
<option id="show_hidden" type="bool" default="false" />
</section>
<section id="text_tool">
<option id="font_face" type="std::string" />

View File

@ -765,6 +765,7 @@ pinned_folders = Pinned Folders
recent_folders = Recent Folders
all_formats = All formats
all_files = All files
show_hidden = Show hidden
[filters]
selected_cels = Selected
@ -1834,6 +1835,18 @@ pixel_scale = Pixel Scale
with_vars = Use CSS3 Variables
generate_html = Generate Sample HTML File
[shape]
fill = Fill
stroke = Stroke
stroke_width = Stroke Width
[text_tool]
font_family = Font Family
font_size = Font Size
bold = Bold
italic = Italic
more_options = More Options
[timeline_conf]
position = Position:
left = &Left
@ -1896,6 +1909,7 @@ timeline_show = Show Timeline
[undo_history]
title = Undo History
initial_state = Initial State
[user_data]
user_data = User Data:

View File

@ -23,6 +23,7 @@
<combobox id="location" expansive="true" />
<button text="" id="refresh_button" style="refresh_button"
tooltip="@.refresh_button_tooltip" tooltip_dir="bottom" />
<check id="show_hidden_check" text="@.show_hidden" />
</box>
<vbox id="file_view_placeholder" expansive="true" />
<grid columns="2">

2
laf

@ -1 +1 @@
Subproject commit 48785005692948ab44c87b8635741476cd346800
Subproject commit a2bb9ec7fb98354279a2c49870a4a47a67a8e86e

View File

@ -72,6 +72,10 @@ In Debug mode (`_DEBUG`):
* [`TRACEARGS`](https://github.com/aseprite/laf/blob/f3222bdee2d21556e9da55343e73803c730ecd97/base/debug.h#L40):
in debug mode, it prints in the terminal/console each given argument
In release mode you can use a similar function:
* `PRINTARGS` prints in the terminal/console each given argument
# Detect Platform
You can check the platform using some `laf` macros:

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -10,6 +10,7 @@
#endif
#include "app/app.h"
#include "app/color_utils.h"
#include "app/commands/command.h"
#include "app/console.h"
#include "app/context.h"
@ -88,10 +89,10 @@ void PasteTextCommand::onExecute(Context* ctx)
std::string text = window.userText()->text();
app::Color color = window.fontColor()->getColor();
doc::ImageRef image = render_text(
fontInfo,
text,
gfx::rgba(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha()));
ui::Paint paint = window.fontFace()->paint();
paint.color(color_utils::color_for_ui(color));
doc::ImageRef image = render_text(fontInfo, text, paint);
if (image) {
Sprite* sprite = editor->sprite();
if (image->pixelFormat() != sprite->pixelFormat()) {

View File

@ -21,6 +21,7 @@
#include "app/doc_undo.h"
#include "app/doc_undo_observer.h"
#include "app/docs_observer.h"
#include "app/i18n/strings.h"
#include "app/modules/gui.h"
#include "app/modules/palettes.h"
#include "app/site.h"
@ -292,7 +293,7 @@ public:
base::get_pretty_memory_size(static_cast<Cmd*>(state->cmd())->memSize())
#endif
:
std::string("Initial State"));
Strings::undo_history_initial_state());
if ((g->getClipBounds() & itemBounds).isEmpty())
return;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -139,6 +139,13 @@ DataRecovery::Sessions DataRecovery::sessions()
return copy;
}
bool DataRecovery::isRunningSession(const SessionPtr& session) const
{
ASSERT(session);
ASSERT(m_inProgress);
return session->path() == m_inProgress->path();
}
void DataRecovery::searchForSessions()
{
Sessions sessions;
@ -150,7 +157,7 @@ void DataRecovery::searchForSessions()
RECO_TRACE("RECO: Session '%s' ", itempath.c_str());
SessionPtr session(new Session(&m_config, itempath));
if (!session->isRunning()) {
if (!isRunningSession(session)) {
if ((session->isEmpty()) || (!session->isCrashedSession() && session->isOldSession())) {
RECO_TRACE("to be deleted (%s)\n",
session->isEmpty() ? "is empty" :

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -45,6 +45,8 @@ public:
// Returns a copy of the list of sessions that can be recovered.
Sessions sessions();
bool isRunningSession(const SessionPtr& session) const;
// Triggered in the UI-thread from the m_thread using an
// ui::execute_from_ui_thread() when the list of sessions is ready
// to be used.

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -19,12 +19,10 @@
#include "app/crash/write_document.h"
#include "app/doc.h"
#include "app/doc_access.h"
#include "app/file/file.h"
#include "app/ui_context.h"
#include "base/convert_to.h"
#include "base/fs.h"
#include "base/fstream_path.h"
#include "base/process.h"
#include "base/split_string.h"
#include "base/string.h"
#include "base/thread.h"
@ -128,12 +126,6 @@ const Session::Backups& Session::backups()
return m_backups;
}
bool Session::isRunning()
{
loadPid();
return base::get_process_name(m_pid) == base::get_process_name(base::get_current_process_id());
}
bool Session::isCrashedSession()
{
loadPid();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -47,9 +47,9 @@ public:
std::string name() const;
std::string version();
std::string& path() { return m_path; }
const Backups& backups();
bool isRunning();
bool isCrashedSession();
bool isOldSession();
bool isEmpty();

View File

@ -70,6 +70,104 @@ std::string escape_for_json(const std::string& path)
return res;
}
// Forward declaration
void serialize_properties(const doc::UserData::Properties& props, std::ostream& os);
// Helper for a single value
void serialize_variant(const doc::UserData::Variant& value, std::ostream& os)
{
using Properties = doc::UserData::Properties;
switch (value.index()) {
case USER_DATA_PROPERTY_TYPE_BOOL: os << (get_value<bool>(value) ? "true" : "false"); break;
case USER_DATA_PROPERTY_TYPE_INT8: os << static_cast<int64_t>(get_value<int8_t>(value)); break;
case USER_DATA_PROPERTY_TYPE_UINT8:
os << static_cast<int64_t>(get_value<uint8_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_INT16:
os << static_cast<int64_t>(get_value<int16_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_UINT16:
os << static_cast<int64_t>(get_value<uint16_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_INT32:
os << static_cast<int64_t>(get_value<int32_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_UINT32:
os << static_cast<int64_t>(get_value<uint32_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_INT64:
os << static_cast<int64_t>(get_value<int64_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_UINT64:
os << static_cast<int64_t>(get_value<uint64_t>(value));
break;
case USER_DATA_PROPERTY_TYPE_FLOAT: os << get_value<float>(value); break;
case USER_DATA_PROPERTY_TYPE_DOUBLE: os << get_value<double>(value); break;
case USER_DATA_PROPERTY_TYPE_STRING:
os << "\"" << escape_for_json(get_value<std::string>(value)) << "\"";
break;
case USER_DATA_PROPERTY_TYPE_PROPERTIES:
serialize_properties(get_value<Properties>(value), os);
break;
default: os << "\"[unsupported type]\""; break;
}
}
// Serializes a map of properties
void serialize_properties(const doc::UserData::Properties& props, std::ostream& os)
{
os << "{";
bool first = true;
for (const auto& [key, value] : props) {
if (!first)
os << ", ";
first = false;
os << "\"" << escape_for_json(key) << "\": ";
serialize_variant(value, os);
}
os << "}";
}
void serialize_userdata_properties(const doc::UserData& data, std::ostream& os)
{
const auto& propsMaps = data.propertiesMaps();
bool hasAnyProps = false;
for (const auto& [group, props] : propsMaps) {
if (!props.empty()) {
hasAnyProps = true;
break;
}
}
if (hasAnyProps) {
os << ", \"properties\": {";
bool firstProp = true;
for (const auto& [group, props] : propsMaps) {
if (!props.empty()) {
if (!firstProp)
os << ", ";
firstProp = false;
if (group.empty()) {
// Default group: flatten its keys at the top level
bool firstKey = true;
for (const auto& [key, value] : props) {
if (!firstKey)
os << ", ";
firstKey = false;
os << "\"" << escape_for_json(key) << "\": ";
serialize_variant(value, os);
}
}
else {
// Named group: nest under its group name
os << "\"" << escape_for_json(group) << "\": ";
serialize_properties(props, os);
}
}
}
os << "}";
}
}
std::ostream& operator<<(std::ostream& os, const doc::UserData& data)
{
doc::color_t color = data.color();
@ -81,6 +179,9 @@ std::ostream& operator<<(std::ostream& os, const doc::UserData& data)
}
if (!data.text().empty())
os << ", \"data\": \"" << escape_for_json(data.text()) << "\"";
serialize_userdata_properties(data, os);
return os;
}
@ -323,7 +424,7 @@ public:
dithering,
sprite ? sprite->rgbMap(0) : nullptr,
sprite ? sprite->palette(0) : nullptr,
sprite ? sprite->backgroundLayer() : nullptr,
(sprite && sprite->backgroundLayer()),
0,
0,
nullptr);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -1401,7 +1401,7 @@ private:
color_t color = *srcIt;
int i;
if (rgba_geta(color) >= 128) {
if (rgba_geta(color) > 0) {
i = framePalette.findExactMatch(rgba_getr(color),
rgba_getg(color),
rgba_getb(color),

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -82,6 +82,9 @@ public:
unsigned int m_version;
bool m_removed;
mutable bool m_is_folder;
#ifdef _WIN32
bool m_isHidden = false;
#endif
std::atomic<double> m_thumbnailProgress;
std::atomic<os::Surface*> m_thumbnail;
#ifdef _WIN32
@ -266,7 +269,7 @@ IFileItem* FileSystemModule::getRootFileItem()
fileitem->m_pidl = pidl;
fileitem->m_fullpidl = pidl;
SFGAOF attrib = SFGAO_FOLDER;
SFGAOF attrib = SFGAO_FOLDER | SFGAO_HIDDEN;
shl_idesktop->GetAttributesOf(1, (LPCITEMIDLIST*)&pidl, &attrib);
update_by_pidl(fileitem, attrib);
@ -357,7 +360,7 @@ bool FileItem::isHidden() const
ASSERT(m_displayname != NOTINITIALIZED);
#ifdef _WIN32
return false;
return m_isHidden;
#else
return m_displayname[0] == '.';
#endif
@ -462,7 +465,7 @@ const FileItemList& FileItem::children()
// Get the interface to enumerate subitems
hr = pFolder->EnumObjects(
reinterpret_cast<HWND>(os::System::instance()->defaultWindow()->nativeHandle()),
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS,
SHCONTF_FOLDERS | SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN,
&pEnum);
if (hr == S_OK && pEnum) {
@ -473,10 +476,9 @@ const FileItemList& FileItem::children()
while (pEnum->Next(256, itempidl, &fetched) == S_OK && fetched > 0) {
// Request the SFGAO_FOLDER attribute to know what of the
// item is file or a folder
for (c = 0; c < fetched; ++c) {
attribs[c] = SFGAO_FOLDER;
pFolder->GetAttributesOf(1, (LPCITEMIDLIST*)itempidl, attribs + c);
}
for (c = 0; c < fetched; ++c)
attribs[c] = SFGAO_FOLDER | SFGAO_HIDDEN;
pFolder->GetAttributesOf(fetched, (LPCITEMIDLIST*)itempidl, attribs);
// Generate the FileItems
for (c = 0; c < fetched; ++c) {
@ -755,6 +757,9 @@ static void update_by_pidl(FileItem* fileitem, SFGAOF attrib)
// Is it a folder?
fileitem->m_is_folder = calc_is_folder(fileitem->m_filename, attrib);
#if _WIN32
fileitem->m_isHidden = (attrib & SFGAO_HIDDEN ? true : false);
#endif
// Get the name to display

View File

@ -14,6 +14,7 @@
#include "text/font.h"
#include "text/font_mgr.h"
#include "text/sprite_sheet_font.h"
#include "text/typeface.h"
#include "ui/manager.h"
#include "ui/scale.h"

View File

@ -1857,7 +1857,7 @@ private:
class ContextBar::FontSelector : public FontEntry {
public:
FontSelector(ContextBar* contextBar)
FontSelector(ContextBar* contextBar) : FontEntry(true) // With stroke and fill options
{
// Load the font from the preferences
setInfo(FontInfo::getFromPreferences(), FontEntry::From::Init);
@ -2559,6 +2559,11 @@ FontInfo ContextBar::fontInfo() const
return m_fontSelector->info();
}
FontEntry* ContextBar::fontEntry()
{
return m_fontSelector;
}
render::DitheringMatrix ContextBar::ditheringMatrix()
{
return m_ditheringSelector->ditheringMatrix();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -90,6 +90,7 @@ public:
// For text tool
FontInfo fontInfo() const;
FontEntry* fontEntry();
// For gradients
render::DitheringMatrix ditheringMatrix();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -457,6 +457,8 @@ void BrushPreview::show(const gfx::Point& screenPos)
// Here we re-use the cached surface
if (!cached && m_uiLayer->surface()) {
m_uiLayer->surface()->clear();
gfx::Rect layerBounds = m_uiLayer->surface()->bounds();
ui::Graphics g(display, m_uiLayer->surface(), 0, 0);

View File

@ -58,6 +58,7 @@
#include "app/util/tile_flags_utils.h"
#include "base/chrono.h"
#include "base/convert_to.h"
#include "base/scoped_value.h"
#include "doc/doc.h"
#include "doc/mask_boundaries.h"
#include "doc/slice.h"
@ -266,6 +267,23 @@ void Editor::setStateInternal(const EditorStatePtr& newState)
{
m_brushPreview.hide();
// Some onLeaveState impls (like the ones from MovingPixelsState,
// WritingTextState, MovingSelectionState) might generate a
// Tx/Transaction::commit(), which will add a new undo state,
// triggering a sprite change scripting event
// (SpriteEvents::onAddUndoState). This event could be handled by an
// extension and that extension might want to save the current
// sprite (e.g. calling Sprite_saveCopyAs, the kind of extension
// that takes snapshots after each sprite change). That will be a
// new Context::executeCommand() for the save command, generating a
// BeforeCommandExecution signal, getting back to onLeaveState
// again. In that case, we just ignore the reentry as the first
// onLeaveState should handle everything (to avoid an stack
// overflow/infinite recursion).
if (m_leavingState)
return;
base::ScopedValue leaving(m_leavingState, true);
// Fire before change state event, set the state, and fire after
// change state event.
EditorState::LeaveAction leaveAction = m_state->onLeaveState(this, newState.get());

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -458,6 +458,13 @@ private:
DocView* m_docView;
// Special flag to avoid re-entering a new state when we are leaving
// the current one. This avoids an infinite onLeaveState() recursion
// in some special cases when an extension (third-party code)
// creates a new sprite change in the same sprite change scripting
// event.
bool m_leavingState = false;
// Last known mouse position received by this editor when the
// mouse button was pressed. Used for auto-scrolling. To get the
// current mouse position on the editor you can use

View File

@ -428,8 +428,8 @@ bool MovingSliceState::onMouseMove(Editor* editor, MouseMessage* msg)
if (editor->slicesTransforms())
drawExtraCel();
// Redraw the editor.
editor->invalidate();
// Notify changes
m_site.document()->notifyGeneralUpdate();
// Use StandbyState implementation
return StandbyState::onMouseMove(editor, msg);

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -420,10 +420,12 @@ void PixelsMovement::moveImage(const gfx::PointF& pos, MoveModifier moveModifier
// Now we calculate the difference from x1,y1 point and we can
// use it to adjust all coordinates (x1, y1, x2, y2).
bounds.setOrigin(gridOffset);
newTransformation.pivot(abs_initial_pivot - m_initialData.bounds().origin() + gridOffset);
}
else
newTransformation.pivot(abs_initial_pivot + gfx::PointF(dx, dy));
newTransformation.bounds(bounds);
newTransformation.pivot(abs_initial_pivot + gfx::PointF(dx, dy));
break;
}

View File

@ -15,6 +15,7 @@
#include "app/commands/command.h"
#include "app/extra_cel.h"
#include "app/fonts/font_info.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "app/site.h"
#include "app/tx.h"
@ -43,12 +44,42 @@
#include "os/skia/skia_surface.h"
#endif
#include <cmath>
namespace app {
using namespace ui;
// Get ui::Paint to render text from context bar options / preferences
static ui::Paint get_paint_for_text()
{
ui::Paint paint;
if (auto* app = App::instance()) {
if (auto* ctxBar = app->contextBar())
paint = ctxBar->fontEntry()->paint();
}
paint.color(color_utils::color_for_ui(Preferences::instance().colorBar.fgColor()));
return paint;
}
static gfx::RectF calc_blob_bounds(const text::TextBlobRef& blob)
{
gfx::RectF bounds = get_text_blob_required_bounds(blob);
ui::Paint paint = get_paint_for_text();
if (paint.style() == ui::Paint::Style::Stroke ||
paint.style() == ui::Paint::Style::StrokeAndFill) {
bounds.enlarge(std::ceil(paint.strokeWidth()));
}
return bounds;
}
class WritingTextState::TextEditor : public Entry {
public:
enum TextPreview {
Intermediate, // With selection preview / user interface
Final, // Final to be rendered in the cel
};
TextEditor(Editor* editor, const Site& site, const gfx::Rect& bounds)
: Entry(4096, "")
, m_editor(editor)
@ -61,7 +92,7 @@ public:
setPersistSelection(true);
createExtraCel(site, bounds);
renderExtraCelBase();
renderExtraCel(TextPreview::Intermediate);
FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
if (auto font = Fonts::instance()->fontFromInfo(fontInfo))
@ -76,36 +107,37 @@ public:
// Returns the extra cel with the text rendered (but without the
// selected text highlighted).
ExtraCelRef extraCel()
ExtraCelRef extraCel(const TextPreview textPreview)
{
renderExtraCelBase();
renderExtraCelText(false);
renderExtraCel(textPreview);
return m_extraCel;
}
void setExtraCelBounds(const gfx::Rect& bounds)
void setExtraCelBounds(const gfx::RectF& bounds)
{
doc::Image* extraImg = m_extraCel->image();
if (!extraImg || bounds.w != extraImg->width() || bounds.h != extraImg->height()) {
if (!extraImg || std::ceil(bounds.w) != extraImg->width() ||
std::ceil(bounds.h) != extraImg->height()) {
createExtraCel(m_editor->getSite(), bounds);
}
else {
m_baseBounds = bounds;
m_extraCel->cel()->setBounds(bounds);
}
renderExtraCelBase();
renderExtraCelText(true);
renderExtraCel(TextPreview::Intermediate);
}
obs::signal<void(const gfx::Size&)> NewRequiredBounds;
obs::signal<void(const gfx::RectF&)> NewRequiredBounds;
private:
void createExtraCel(const Site& site, const gfx::Rect& bounds)
{
m_baseBounds = bounds;
m_extraCel->create(ExtraCel::Purpose::TextPreview,
site.tilemapMode(),
site.sprite(),
bounds,
bounds.size(),
gfx::Size(std::ceil(bounds.w), std::ceil(bounds.h)),
site.frame(),
255);
@ -176,7 +208,7 @@ private:
// Notify that we could make the text editor bigger to show this
// text blob.
NewRequiredBounds(get_text_blob_required_size(blob));
NewRequiredBounds(calc_blob_bounds(blob));
}
void onPaint(PaintEvent& ev) override
@ -205,8 +237,7 @@ private:
}
// Render extra cel with text + selected text
renderExtraCelBase();
renderExtraCelText(true);
renderExtraCel(TextPreview::Intermediate);
m_doc->setExtraCel(m_extraCel);
// Paint caret
@ -227,76 +258,80 @@ private:
}
}
void renderExtraCelBase()
void renderExtraCel(const TextPreview textPreview)
{
doc::Image* extraImg = m_extraCel->image();
ASSERT(extraImg);
if (!extraImg)
return;
const doc::Cel* extraCel = m_extraCel->cel();
extraImg->clear(extraImg->maskColor());
text::TextBlobRef blob = textBlob();
doc::ImageRef blobImage;
gfx::RectF bounds;
if (blob) {
const ui::Paint paint = get_paint_for_text();
bounds = calc_blob_bounds(blob);
blobImage = render_text_blob(blob, bounds, get_paint_for_text());
if (!blobImage)
return;
// Invert selected range in the image
if (textPreview == TextPreview::Intermediate) {
Range range;
getEntryThemeInfo(nullptr, nullptr, nullptr, &range);
if (!range.isEmpty()) {
gfx::RectF selectedBounds = getCharBoxBounds(range.from) | getCharBoxBounds(range.to - 1);
if (!selectedBounds.isEmpty()) {
selectedBounds.offset(-bounds.origin());
#ifdef LAF_SKIA
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(blobImage.get());
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
os::Paint paint2 = paint;
paint2.blendMode(os::BlendMode::Xor);
paint2.style(os::Paint::Style::Fill);
surface->drawRect(selectedBounds, paint2);
#endif // LAF_SKIA
}
}
}
}
doc::Cel* extraCel = m_extraCel->cel();
ASSERT(extraCel);
if (!extraCel)
return;
extraImg->clear(extraImg->maskColor());
extraCel->setPosition(m_baseBounds.x + bounds.x, m_baseBounds.y + bounds.y);
render::Render().renderLayer(extraImg,
m_editor->layer(),
m_editor->frame(),
gfx::Clip(0, 0, extraCel->bounds()),
doc::BlendMode::SRC);
}
void renderExtraCelText(const bool withSelection)
{
const auto textColor = color_utils::color_for_image(Preferences::instance().colorBar.fgColor(),
IMAGE_RGB);
text::TextBlobRef blob = textBlob();
if (!blob)
return;
doc::ImageRef image = render_text_blob(blob, textColor);
if (!image)
return;
// Invert selected range in the image
if (withSelection) {
Range range;
getEntryThemeInfo(nullptr, nullptr, nullptr, &range);
if (!range.isEmpty()) {
gfx::RectF selectedBounds = getCharBoxBounds(range.from) | getCharBoxBounds(range.to - 1);
if (!selectedBounds.isEmpty()) {
#ifdef LAF_SKIA
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(image.get());
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
os::Paint paint;
paint.blendMode(os::BlendMode::Xor);
paint.color(textColor);
surface->drawRect(selectedBounds, paint);
#endif // LAF_SKIA
}
}
if (blobImage) {
doc::blend_image(extraImg,
blobImage.get(),
gfx::Clip(blobImage->bounds().size()),
m_doc->sprite()->palette(m_editor->frame()),
255,
doc::BlendMode::NORMAL);
}
doc::Image* extraImg = m_extraCel->image();
ASSERT(extraImg);
if (!extraImg)
return;
doc::blend_image(extraImg,
image.get(),
gfx::Clip(image->bounds().size()),
m_doc->sprite()->palette(m_editor->frame()),
255,
doc::BlendMode::NORMAL);
}
Editor* m_editor;
Doc* m_doc;
ExtraCelRef m_extraCel;
// Initial bounds for the entry field. This can be modified later to
// render the text in case some initial letter/glyph needs some
// extra room at the left side.
gfx::Rect m_baseBounds;
};
WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
@ -312,10 +347,10 @@ WritingTextState::WritingTextState(Editor* editor, const gfx::Rect& bounds)
m_fontChangeConn =
App::instance()->contextBar()->FontChange.connect(&WritingTextState::onFontChange, this);
m_entry->NewRequiredBounds.connect([this](const gfx::Size& blobSize) {
if (m_bounds.w < blobSize.w || m_bounds.h < blobSize.h) {
m_bounds.w = std::max(m_bounds.w, blobSize.w);
m_bounds.h = std::max(m_bounds.h, blobSize.h);
m_entry->NewRequiredBounds.connect([this](const gfx::RectF& blobBounds) {
if (m_bounds.w < blobBounds.w || m_bounds.h < blobBounds.h) {
m_bounds.w = std::max(m_bounds.w, blobBounds.w);
m_bounds.h = std::max(m_bounds.h, blobBounds.h);
m_entry->setExtraCelBounds(m_bounds);
m_entry->setBounds(calcEntryBounds());
}
@ -388,11 +423,11 @@ void WritingTextState::onCommitMouseMove(Editor* editor, const gfx::PointF& spri
if (!m_movingBounds)
return;
gfx::Point delta(spritePos - m_cursorStart);
gfx::PointF delta(spritePos - m_cursorStart);
if (delta.x == 0 && delta.y == 0)
return;
m_bounds.setOrigin(gfx::Point(delta + m_boundsOrigin));
m_bounds.setOrigin(delta + m_boundsOrigin);
m_entry->setExtraCelBounds(m_bounds);
m_entry->setBounds(calcEntryBounds());
}
@ -475,8 +510,8 @@ EditorState::LeaveAction WritingTextState::onLeaveState(Editor* editor, EditorSt
// Paints the text in the active layer/sprite creating an
// undoable transaction.
Site site = m_editor->getSite();
ExtraCelRef extraCel = m_entry->extraCel();
Tx tx(site.document(), "Text Tool");
ExtraCelRef extraCel = m_entry->extraCel(TextEditor::Final);
Tx tx(site.document(), Strings::tools_text());
ExpandCelCanvas expand(site, site.layer(), TiledMode::NONE, tx, ExpandCelCanvas::None);
expand.validateDestCanvas(gfx::Region(extraCel->cel()->bounds()));
@ -527,7 +562,7 @@ void WritingTextState::onFontChange(const FontInfo& fontInfo, FontEntry::From fr
// This is useful to show changes to the anti-alias option
// immediately.
auto dummy = m_entry->extraCel();
auto dummy = m_entry->extraCel(TextEditor::Intermediate);
if (fromField == FontEntry::From::Popup) {
if (m_entry)

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (c) 2022-2024 Igara Studio S.A.
// Copyright (c) 2022-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -59,7 +59,7 @@ private:
DelayedMouseMove m_delayedMouseMove;
Editor* m_editor;
gfx::Rect m_bounds;
gfx::RectF m_bounds;
std::unique_ptr<TextEditor> m_entry;
// True if the text was discarded.
@ -71,7 +71,7 @@ private:
bool m_mouseMoveReceived = false;
bool m_movingBounds = false;
gfx::PointF m_cursorStart;
gfx::Point m_boundsOrigin;
gfx::PointF m_boundsOrigin;
obs::scoped_connection m_beforeCmdConn;
obs::scoped_connection m_fontChangeConn;

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -18,6 +18,7 @@
#include "base/time.h"
#include "os/surface.h"
#include "text/font.h"
#include "text/font_metrics.h"
#include "ui/ui.h"
#include <algorithm>
@ -44,6 +45,7 @@ FileList::FileList()
, m_multiselect(false)
, m_zoom(1.0)
, m_itemsPerRow(0)
, m_showHidden(Preferences::instance().fileSelector.showHidden())
{
setFocusStop(true);
setDoubleBuffered(true);
@ -172,6 +174,14 @@ void FileList::animateToZoom(const double zoom)
startAnimation(ANI_ZOOM, 10);
}
void FileList::setShowHidden(const bool show)
{
m_showHidden = show;
m_req_valid = false;
m_selected = nullptr;
regenerateList();
}
bool FileList::onProcessMessage(Message* msg)
{
switch (msg->type()) {
@ -401,7 +411,6 @@ void FileList::onPaint(ui::PaintEvent& ev)
gfx::Rect bounds = clientBounds();
g->fillRect(theme->colors.background(), bounds);
// g->fillRect(bgcolor, gfx::Rect(bounds.x, y, bounds.w, itemSize.h));
int i = 0, selectedIndex = -1;
for (IFileItem* fi : m_list) {
@ -506,17 +515,28 @@ void FileList::paintItem(ui::Graphics* g, IFileItem* fi, const int i)
// item name
if (isIconView() && textBounds.w > info.bounds.w) {
g->drawAlignedUIText(fi->displayName().c_str(),
g->drawAlignedUIText(fi->displayName(),
fgcolor,
bgcolor,
(textBounds & gfx::Rect(info.bounds).shrink(2 * guiscale())),
ui::CENTER | ui::TOP | ui::CHARWRAP);
}
else {
g->drawText(fi->displayName().c_str(),
fgcolor,
bgcolor,
gfx::Point(textBounds.x + 2 * guiscale(), textBounds.y + 2 * guiscale()));
auto blob = text::TextBlob::MakeWithShaper(theme->fontMgr(), font(), fi->displayName());
if (blob) {
Paint paint;
paint.color(fgcolor);
paint.style(os::Paint::Fill);
text::FontMetrics metrics;
font()->metrics(&metrics);
const float baselineDelta = -metrics.ascent - blob->baseline();
g->drawTextBlob(
blob,
gfx::PointF(textBounds.x + 2 * guiscale(), textBounds.y + 2 * guiscale() + baselineDelta),
paint);
}
}
// Draw thumbnail progress bar
@ -814,7 +834,7 @@ void FileList::regenerateList()
for (FileItemList::iterator it = m_list.begin(); it != m_list.end();) {
IFileItem* fileitem = *it;
if (fileitem->isHidden())
if (fileitem->isHidden() && !m_showHidden)
it = m_list.erase(it);
else if (!fileitem->isFolder() && !fileitem->hasExtension(m_exts)) {
it = m_list.erase(it);

View File

@ -54,6 +54,7 @@ public:
double zoom() const { return m_zoom; }
void setZoom(const double zoom);
void animateToZoom(const double zoom);
void setShowHidden(const bool show);
obs::signal<void()> FileSelected;
obs::signal<void()> FileAccepted;
@ -137,6 +138,7 @@ private:
double m_toZoom;
int m_itemsPerRow;
bool m_showHidden;
};
} // namespace app

View File

@ -316,6 +316,7 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
for (auto child : viewType()->children())
child->setFocusStop(false);
showHiddenCheck()->setSelected(Preferences::instance().fileSelector.showHidden());
m_fileList = new FileList();
m_fileList->setId("fileview");
m_fileName->setAssociatedFileList(m_fileList);
@ -334,6 +335,10 @@ FileSelector::FileSelector(FileSelectorType type) : m_type(type), m_navigationLo
viewType()->ItemChange.connect([this] { onChangeViewType(); });
location()->CloseListBox.connect([this] { onLocationCloseListBox(); });
fileType()->Change.connect([this] { onFileTypeChange(); });
showHiddenCheck()->Click.connect([this] {
Preferences::instance().fileSelector.showHidden(showHiddenCheck()->isSelected());
m_fileList->setShowHidden(showHiddenCheck()->isSelected());
});
m_fileList->FileSelected.connect([this] { onFileListFileSelected(); });
m_fileList->FileAccepted.connect([this] { onFileListFileAccepted(); });
m_fileList->CurrentFolderChanged.connect([this] { onFileListCurrentFolderChanged(); });

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -10,8 +10,10 @@
#include "app/ui/filename_field.h"
#include "app/app.h"
#include "app/i18n/strings.h"
#include "app/pref/preferences.h"
#include "app/recent_files.h"
#include "app/ui/skin/skin_theme.h"
#include "base/fs.h"
#include "ui/box.h"
@ -24,6 +26,11 @@ namespace app {
using namespace ui;
FilenameField::FilenameButton::FilenameButton(const std::string& text) : ButtonSet(1)
{
addItem(text);
}
FilenameField::FilenameField(const Type type, const std::string& pathAndFilename)
: m_entry(type == EntryAndButton ? new ui::Entry(1024, "") : nullptr)
, m_button(type == EntryAndButton ? Strings::select_file_browse() : Strings::select_file_text())
@ -46,7 +53,10 @@ FilenameField::FilenameField(const Type type, const std::string& pathAndFilename
if (m_entry)
m_entry->Change.connect([this] { setFilename(updatedFilename()); });
m_button.Click.connect([this] { onBrowse(); });
m_button.ItemChange.connect([this](ButtonSet::Item* item) {
m_button.setSelectedItem(nullptr);
onBrowse();
});
initTheme();
m_editFullPathChangeConn = Preferences::instance().general.editFullPath.AfterChange.connect(
@ -94,7 +104,6 @@ void FilenameField::onSetEditFullPath()
void FilenameField::onBrowse()
{
const gfx::Rect bounds = m_button.bounds();
m_button.setSelected(false);
ui::Menu menu;
ui::MenuItem choose(Strings::select_file_choose());
@ -107,6 +116,11 @@ void FilenameField::onBrowse()
menu.addChild(&relative);
menu.addChild(&absolute);
if (auto* recent = App::instance()->recentFiles()) {
addFoldersToMenu(&menu, recent->pinnedFolders(), Strings::file_selector_pinned_folders());
addFoldersToMenu(&menu, recent->recentFolders(), Strings::file_selector_recent_folders());
}
choose.Click.connect([this] {
std::string fn = SelectOutputFile();
if (!fn.empty()) {
@ -120,6 +134,21 @@ void FilenameField::onBrowse()
menu.showPopup(gfx::Point(bounds.x, bounds.y2()), display());
}
void FilenameField::addFoldersToMenu(ui::Menu* menu,
const base::paths& folders,
const std::string& separatorTitle)
{
if (folders.empty())
return;
menu->addChild(new ui::Separator(separatorTitle, ui::HORIZONTAL));
for (const std::string& folder : folders) {
MenuItem* folderItem = new MenuItem(folder);
folderItem->Click.connect([this, folder] { setFilename(base::join_path(folder, m_file)); });
menu->addChild(folderItem);
}
}
void FilenameField::setFilename(const std::string& pathAndFilename)
{
const std::string spritePath = base::get_file_path(m_docFilename);
@ -164,11 +193,6 @@ void FilenameField::onInitTheme(ui::InitThemeEvent& ev)
{
HBox::onInitTheme(ev);
setChildSpacing(0);
auto theme = skin::SkinTheme::get(this);
ui::Style* style = theme->styles.miniButton();
if (style)
m_button.setStyle(style);
}
void FilenameField::onUpdateText()
@ -181,9 +205,9 @@ void FilenameField::updateWidgets()
if (m_entry)
m_entry->setText(displayedFilename());
else if (m_file.empty())
m_button.setText(Strings::select_file_text());
m_button.getItem(0)->setText(Strings::select_file_text());
else
m_button.setText(displayedFilename());
m_button.getItem(0)->setText(displayedFilename());
Change();
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -8,14 +8,19 @@
#define APP_UI_FILENAME_FIELD_H_INCLUDED
#pragma once
#include "app/ui/button_set.h"
#include "base/paths.h"
#include "obs/connection.h"
#include "obs/signal.h"
#include "ui/box.h"
#include "ui/button.h"
#include "ui/entry.h"
#include <string>
namespace ui {
class Menu;
}
namespace app {
class FilenameField : public ui::HBox {
@ -44,17 +49,25 @@ protected:
void onSetEditFullPath();
private:
class FilenameButton : public ButtonSet {
public:
FilenameButton(const std::string& text);
};
void setEditFullPath(const bool on);
void updateWidgets();
void onBrowse();
std::string updatedFilename() const;
void addFoldersToMenu(ui::Menu* menu,
const base::paths& folders,
const std::string& separatorTitle);
std::string m_path;
std::string m_pathBase;
std::string m_file;
std::string m_docFilename;
ui::Entry* m_entry;
ui::Button m_button;
FilenameButton m_button;
bool m_editFullPath;
bool m_askOverwrite;

View File

@ -12,10 +12,12 @@
#include "app/app.h"
#include "app/console.h"
#include "app/i18n/strings.h"
#include "app/recent_files.h"
#include "app/ui/font_popup.h"
#include "app/ui/skin/skin_theme.h"
#include "base/contains.h"
#include "base/convert_to.h"
#include "base/scoped_value.h"
#include "fmt/format.h"
#include "ui/display.h"
@ -262,22 +264,91 @@ void FontEntry::FontSize::onEntryChange()
Change();
}
FontEntry::FontStyle::FontStyle() : ButtonSet(3, true)
FontEntry::FontStyle::FontStyle(ui::TooltipManager* tooltips) : ButtonSet(3, true)
{
addItem("B");
addItem("I");
addItem("...");
setMultiMode(MultiMode::Set);
tooltips->addTooltipFor(getItem(0), Strings::text_tool_bold(), BOTTOM);
tooltips->addTooltipFor(getItem(1), Strings::text_tool_italic(), BOTTOM);
tooltips->addTooltipFor(getItem(2), Strings::text_tool_more_options(), BOTTOM);
}
FontEntry::FontEntry()
FontEntry::FontStroke::FontStroke(ui::TooltipManager* tooltips) : m_fill(2)
{
auto* theme = skin::SkinTheme::get(this);
m_fill.addItem(theme->parts.toolFilledRectangle(), theme->styles.contextBarButton());
m_fill.addItem(theme->parts.toolRectangle(), theme->styles.contextBarButton());
m_fill.setSelectedItem(0);
m_fill.ItemChange.connect([this] { Change(); });
m_stroke.setText("0");
m_stroke.setSuffix("pt");
m_stroke.ValueChange.connect([this] { Change(); });
addChild(&m_fill);
addChild(&m_stroke);
tooltips->addTooltipFor(m_fill.getItem(0), Strings::shape_fill(), BOTTOM);
tooltips->addTooltipFor(m_fill.getItem(1), Strings::shape_stroke(), BOTTOM);
tooltips->addTooltipFor(&m_stroke, Strings::shape_stroke_width(), BOTTOM);
}
bool FontEntry::FontStroke::fill() const
{
return const_cast<FontStroke*>(this)->m_fill.getItem(0)->isSelected();
}
float FontEntry::FontStroke::stroke() const
{
return m_stroke.textDouble();
}
FontEntry::FontStroke::WidthEntry::WidthEntry() : ui::IntEntry(0, 100, this)
{
}
void FontEntry::FontStroke::WidthEntry::onValueChange()
{
ui::IntEntry::onValueChange();
ValueChange();
}
bool FontEntry::FontStroke::WidthEntry::onAcceptUnicodeChar(int unicodeChar)
{
return (IntEntry::onAcceptUnicodeChar(unicodeChar) || unicodeChar == '.');
}
std::string FontEntry::FontStroke::WidthEntry::onGetTextFromValue(int value)
{
return fmt::format("{:.1f}", value / 10.0);
}
int FontEntry::FontStroke::WidthEntry::onGetValueFromText(const std::string& text)
{
return int(10.0 * base::convert_to<double>(text));
}
FontEntry::FontEntry(const bool withStrokeAndFill)
: m_style(&m_tooltips)
, m_stroke(withStrokeAndFill ? std::make_unique<FontStroke>(&m_tooltips) : nullptr)
{
m_face.setExpansive(true);
m_size.setExpansive(false);
m_style.setExpansive(false);
addChild(&m_tooltips);
addChild(&m_face);
addChild(&m_size);
addChild(&m_style);
if (m_stroke)
addChild(m_stroke.get());
m_tooltips.addTooltipFor(&m_face, Strings::text_tool_font_family(), BOTTOM);
m_tooltips.addTooltipFor(m_size.getEntryWidget(), Strings::text_tool_font_size(), BOTTOM);
m_face.setMinSize(gfx::Size(128 * guiscale(), 0));
@ -299,6 +370,8 @@ FontEntry::FontEntry()
});
m_style.ItemChange.connect(&FontEntry::onStyleItemClick, this);
if (m_stroke)
m_stroke->Change.connect(&FontEntry::onStrokeChange, this);
}
// Defined here as FontPopup type is not fully defined in the header
@ -327,6 +400,29 @@ void FontEntry::setInfo(const FontInfo& info, const From fromField)
FontChange(m_info, fromField);
}
ui::Paint FontEntry::paint()
{
ui::Paint paint;
ui::Paint::Style style = ui::Paint::Fill;
if (m_stroke) {
const float stroke = m_stroke->stroke();
if (m_stroke->fill()) {
if (stroke > 0.0f) {
style = ui::Paint::StrokeAndFill;
paint.strokeWidth(stroke);
}
}
else {
style = ui::Paint::Stroke;
paint.strokeWidth(stroke);
}
}
paint.style(style);
return paint;
}
void FontEntry::onStyleItemClick(ButtonSet::Item* item)
{
text::FontStyle style = m_info.style();
@ -404,4 +500,9 @@ void FontEntry::onStyleItemClick(ButtonSet::Item* item)
}
}
void FontEntry::onStrokeChange()
{
FontChange(m_info, From::Paint);
}
} // namespace app

View File

@ -14,7 +14,11 @@
#include "ui/box.h"
#include "ui/button.h"
#include "ui/combobox.h"
#include "ui/int_entry.h"
#include "ui/paint.h"
#include "ui/tooltips.h"
#include <memory>
#include <string>
namespace app {
@ -30,18 +34,22 @@ public:
Flags,
Hinting,
Popup,
Paint,
};
FontEntry();
FontEntry(bool withStrokeAndFill);
~FontEntry();
FontInfo info() { return m_info; }
void setInfo(const FontInfo& info, From from);
ui::Paint paint();
obs::signal<void(const FontInfo&, From)> FontChange;
private:
void onStyleItemClick(ButtonSet::Item* item);
void onStrokeChange();
class FontFace : public SearchEntry {
public:
@ -73,13 +81,40 @@ private:
class FontStyle : public ButtonSet {
public:
FontStyle();
FontStyle(ui::TooltipManager* tooltips);
};
class FontStroke : public HBox {
public:
FontStroke(ui::TooltipManager* tooltips);
bool fill() const;
float stroke() const;
obs::signal<void()> Change;
private:
class WidthEntry : public ui::IntEntry,
public ui::SliderDelegate {
public:
WidthEntry();
obs::signal<void()> ValueChange;
private:
void onValueChange() override;
bool onAcceptUnicodeChar(int unicodeChar) override;
// SliderDelegate impl
std::string onGetTextFromValue(int value) override;
int onGetValueFromText(const std::string& text) override;
};
ButtonSet m_fill;
WidthEntry m_stroke;
};
ui::TooltipManager m_tooltips;
FontInfo m_info;
FontFace m_face;
FontSize m_size;
FontStyle m_style;
std::unique_ptr<FontStroke> m_stroke;
bool m_lockFace = false;
};

View File

@ -194,7 +194,11 @@ private:
if (!blob)
return;
doc::ImageRef image = render_text_blob(blob, gfx::rgba(0, 0, 0));
ui::Paint paint;
paint.color(gfx::rgba(0, 0, 0));
paint.style(ui::Paint::Fill);
const gfx::RectF textBounds = get_text_blob_required_bounds(blob);
doc::ImageRef image = render_text_blob(blob, textBounds, paint);
if (!image)
return;

View File

@ -177,9 +177,6 @@ protected:
ui::Style* style = theme->styles.newsItem();
ui::Style* styleDetail = theme->styles.newsItemDetail();
text::FontMetrics metrics;
font()->metrics(&metrics);
gfx::Size textSize = theme->calcSizeHint(this, style);
gfx::Rect textBounds(bounds.x, bounds.y, bounds.w, textSize.h);
gfx::Rect detailsBounds(bounds.x, bounds.y + textSize.h, bounds.w, bounds.h - textSize.h);
@ -187,19 +184,28 @@ protected:
gfx::Border border = theme->calcBorder(this, style);
gfx::Border borderDetail = theme->calcBorder(this, styleDetail);
PaintWidgetPartInfo info(this);
info.text = &m_title;
info.baseline = border.top() - metrics.ascent;
theme->paintWidgetPart(g, style, bounds, info);
if (!m_titleBlob)
m_titleBlob = text::TextBlob::MakeWithShaper(theme->fontMgr(), font(), m_title);
info.text = &m_desc;
info.baseline += border.bottom() + borderDetail.top() + metrics.descent - metrics.ascent;
theme->paintWidgetPart(g, styleDetail, detailsBounds, info);
PaintWidgetPartInfo info(this);
info.baseline = border.top();
if (m_titleBlob) {
info.textBlob = m_titleBlob;
info.baseline += m_titleBlob->baseline();
theme->paintWidgetPart(g, style, bounds, info);
info.baseline += border.bottom();
info.text = &m_desc;
info.textBlob = nullptr;
info.baseline += borderDetail.top() + m_titleBlob->baseline();
theme->paintWidgetPart(g, styleDetail, detailsBounds, info);
}
}
private:
std::string m_title;
std::string m_desc;
text::TextBlobRef m_titleBlob;
};
class ProblemsItem : public NewsItem {

View File

@ -1350,6 +1350,7 @@ public:
, m_h(h)
{
m_widget->getEntryThemeInfo(&m_index, &m_caret, &m_state, &m_range);
m_suffixIndex = m_widget->text().size();
}
int index() const { return m_index; }
@ -1369,6 +1370,11 @@ public:
bg = ColorNone;
fg = colors.text();
// Suffix text
if (m_index >= m_suffixIndex) {
fg = colors.entrySuffix();
}
// Selected
if ((m_index >= m_range.from) && (m_index < m_range.to)) {
if (m_widget->hasFocus())
@ -1433,6 +1439,7 @@ private:
int m_lastX; // Last position used to fill the background
int m_y, m_h;
int m_charStartX;
int m_suffixIndex;
};
} // anonymous namespace
@ -1445,8 +1452,10 @@ void SkinTheme::drawEntryText(ui::Graphics* g, ui::Entry* widget)
DrawEntryTextDelegate delegate(widget, g, bounds.origin(), widget->textHeight());
int scroll = delegate.index();
if (!widget->text().empty()) {
const std::string& textString = widget->text();
// Full text to paint: widget text + suffix
const std::string textString = widget->text() + widget->getSuffix();
if (!textString.empty()) {
base::utf8_decode dec(textString);
auto pos = dec.pos();
for (int i = 0; i < scroll && dec.next(); ++i)
@ -1454,38 +1463,28 @@ void SkinTheme::drawEntryText(ui::Graphics* g, ui::Entry* widget)
IntersectClip clip(g, bounds);
if (clip) {
g->drawTextWithDelegate(
std::string(pos, textString.end()), // TODO use a string_view()
colors.text(),
ColorNone,
gfx::Point(bounds.x, widget->textBaseline() - widget->textBlob()->baseline()),
&delegate);
}
}
int baselineAdjustment = widget->textBaseline();
if (auto blob = widget->textBlob()) {
baselineAdjustment -= blob->baseline();
}
else {
text::FontMetrics metrics;
widget->font()->metrics(&metrics);
baselineAdjustment += metrics.ascent;
}
bounds.x += delegate.textBounds().w;
// Draw suffix if there is enough space
if (!widget->getSuffix().empty()) {
Rect sufBounds(bounds.x,
bounds.y,
bounds.x2() - widget->childSpacing() - bounds.x,
widget->textHeight());
IntersectClip clip(g, sufBounds & widget->clientChildrenBounds());
if (clip) {
drawText(g,
widget->getSuffix().c_str(),
colors.entrySuffix(),
ColorNone,
widget,
sufBounds,
widget->align(),
0);
g->drawTextWithDelegate(std::string(pos, textString.end()), // TODO use a string_view()
colors.text(),
ColorNone,
gfx::Point(bounds.x, baselineAdjustment),
&delegate);
}
}
// Draw caret at the end of the text
if (!delegate.caretDrawn()) {
bounds.x += delegate.textBounds().w;
gfx::Rect charBounds(bounds.x + widget->bounds().x,
bounds.y + widget->bounds().y,
0,

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (c) 2020-2024 Igara Studio S.A.
// Copyright (c) 2020-2025 Igara Studio S.A.
// Copyright (c) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -250,6 +250,11 @@ void convert_image_to_surface(const doc::Image* image,
os::SurfaceLock lockDst(surface);
os::SurfaceFormatData fd;
surface->getFormat(&fd);
#if LAF_SKIA
// Needed because Skia surfaces work with premultiplied alpha and
// here we need to copy unpremultiplied alpha RGB values to the Skia surface.
((os::SkiaSurface*)surface)->bitmap().setAlphaType(kUnpremul_SkAlphaType);
#endif
switch (image->pixelFormat()) {
case IMAGE_RGB:

View File

@ -60,7 +60,7 @@ private:
} // anonymous namespace
gfx::Size get_text_blob_required_size(const text::TextBlobRef& blob)
gfx::RectF get_text_blob_required_bounds(const text::TextBlobRef& blob)
{
ASSERT(blob != nullptr);
@ -74,32 +74,28 @@ gfx::Size get_text_blob_required_size(const text::TextBlobRef& blob)
bounds.w = 1;
if (bounds.h < 1)
bounds.h = 1;
return gfx::Size(std::ceil(bounds.w), std::ceil(bounds.h));
return bounds;
}
doc::ImageRef render_text_blob(const text::TextBlobRef& blob, gfx::Color color)
doc::ImageRef render_text_blob(const text::TextBlobRef& blob,
const gfx::RectF& textBounds,
const ui::Paint& paint)
{
ASSERT(blob != nullptr);
os::Paint paint;
// TODO offer Stroke, StrokeAndFill, and Fill styles
paint.style(os::Paint::Fill);
paint.color(color);
gfx::Size blobSize = get_text_blob_required_size(blob);
doc::ImageRef image(doc::Image::create(doc::IMAGE_RGB, blobSize.w, blobSize.h));
doc::ImageRef image(
doc::Image::create(doc::IMAGE_RGB, std::ceil(textBounds.w), std::ceil(textBounds.h)));
#ifdef LAF_SKIA
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(image.get());
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
text::draw_text(surface.get(), blob, gfx::PointF(0, 0), &paint);
text::draw_text(surface.get(), blob, -textBounds.origin(), &paint);
#endif // LAF_SKIA
return image;
}
doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, gfx::Color color)
doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, const ui::Paint& paint)
{
Fonts* fonts = Fonts::instance();
ASSERT(fonts);
@ -113,10 +109,6 @@ doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, gfx
const text::FontMgrRef fontMgr = fonts->fontMgr();
ASSERT(fontMgr);
os::Paint paint;
paint.style(os::Paint::StrokeAndFill);
paint.color(color);
// We have to measure all text runs which might use different
// fonts (e.g. if the given font is not enough to shape other code
// points/languages).

View File

@ -11,25 +11,28 @@
#include "doc/image_ref.h"
#include "gfx/color.h"
#include "gfx/rect.h"
#include "text/text_blob.h"
#include "ui/paint.h"
#include <string>
namespace app {
class Color;
class FontInfo;
namespace skin {
class SkinTheme;
}
// Returns the exact bounds that are required to draw this TextBlob,
// i.e. the image size that will be required in render_text_blob().
gfx::Size get_text_blob_required_size(const text::TextBlobRef& blob);
// Returns the exact bounds that are required to draw this TextBlob in
// the origin point (0, 0), i.e. the image size that will be required
// in render_text_blob().
gfx::RectF get_text_blob_required_bounds(const text::TextBlobRef& blob);
doc::ImageRef render_text_blob(const text::TextBlobRef& blob, gfx::Color color);
doc::ImageRef render_text_blob(const text::TextBlobRef& blob,
const gfx::RectF& textBounds,
const ui::Paint& paint);
doc::ImageRef render_text(const FontInfo& fontInfo, const std::string& text, gfx::Color color);
doc::ImageRef render_text(const FontInfo& fontInfo,
const std::string& text,
const ui::Paint& paint);
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -257,11 +257,9 @@ Widget* WidgetLoader::convertXmlElementToWidget(const XMLElement* elem,
((ExprEntry*)widget)->setDecimals(strtol(decimals, nullptr, 10));
}
if (elem_name == "filename") {
const char* button_only = elem->Attribute("button_only");
const app::FilenameField::Type type = ((button_only != nullptr &&
strtol(button_only, nullptr, 10) == 1) ?
app::FilenameField::Type::ButtonOnly :
app::FilenameField::Type::EntryAndButton);
const bool buttononly = bool_attr(elem, "buttononly", false);
const app::FilenameField::Type type = (buttononly ? app::FilenameField::Type::ButtonOnly :
app::FilenameField::Type::EntryAndButton);
widget = new app::FilenameField(type, "");
}
@ -534,7 +532,7 @@ Widget* WidgetLoader::convertXmlElementToWidget(const XMLElement* elem,
}
else if (elem_name == "font") {
if (!widget)
widget = new FontEntry;
widget = new FontEntry(false);
}
// Was the widget created?

View File

@ -1,4 +1,4 @@
#!/usr/bin/sh
#!/usr/bin/bash
# Aseprite Desktop Integration Module
# Copyright (C) 2016 Gabriel Rauter

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -20,11 +20,12 @@
#include "doc/primitives.h"
#include <algorithm>
#include <atomic>
#include <cmath>
namespace doc {
static int generation = 0;
static std::atomic<int> g_generation = 0;
Brush::Brush()
{
@ -300,7 +301,7 @@ void Brush::setCenter(const gfx::Point& center)
// Cleans the brush's data (image and region).
void Brush::clean()
{
m_gen = ++generation;
m_gen = ++g_generation;
m_image.reset();
m_maskBitmap.reset();
m_backupImage.reset();

View File

@ -106,7 +106,6 @@ void Box::onResize(ResizeEvent& ev)
continue; \
\
int size = 0; \
int sizeDiff = 0; \
\
if (align() & HOMOGENEOUS) { \
if (i < visibleChildren - 1) \

View File

@ -128,6 +128,15 @@ int Entry::lastCaretPos() const
return int(m_boxes.size() - 1);
}
gfx::Point Entry::caretPosOnScreen() const
{
const gfx::Point caretPos = getCharBoxBounds(m_caret).point2();
const os::Window* nativeWindow = display()->nativeWindow();
const gfx::Point pos = nativeWindow->pointToScreen(caretPos + bounds().origin());
return pos;
}
void Entry::setCaretPos(const int pos)
{
gfx::Size caretSize = theme()->getEntryCaretSize(this);
@ -160,6 +169,8 @@ void Entry::setCaretPos(const int pos)
startTimer();
m_state = true;
os::System::instance()->setTextInput(true, caretPosOnScreen());
invalidate();
}
@ -251,7 +262,7 @@ gfx::Rect Entry::getEntryTextBounds() const
return onGetEntryTextBounds();
}
gfx::Rect Entry::getCharBoxBounds(const int i)
gfx::Rect Entry::getCharBoxBounds(const int i) const
{
ASSERT(i >= 0 && i < int(m_boxes.size()));
if (i >= 0 && i < int(m_boxes.size()))
@ -288,8 +299,9 @@ bool Entry::onProcessMessage(Message* msg)
}
// Start processing dead keys
if (m_translate_dead_keys)
os::System::instance()->setTranslateDeadKeys(true);
if (m_translate_dead_keys) {
os::System::instance()->setTextInput(true, caretPosOnScreen());
}
break;
case kFocusLeaveMessage:
@ -304,7 +316,7 @@ bool Entry::onProcessMessage(Message* msg)
// Stop processing dead keys
if (m_translate_dead_keys)
os::System::instance()->setTranslateDeadKeys(false);
os::System::instance()->setTextInput(false);
break;
case kKeyDownMessage:

View File

@ -43,6 +43,7 @@ public:
int caretPos() const { return m_caret; }
int lastCaretPos() const;
gfx::Point caretPosOnScreen() const;
void setCaretPos(int pos);
void setCaretToEnd();
@ -76,7 +77,7 @@ public:
obs::signal<void()> Change;
protected:
gfx::Rect getCharBoxBounds(int i);
gfx::Rect getCharBoxBounds(int i) const;
// Events
bool onProcessMessage(Message* msg) override;

View File

@ -495,10 +495,10 @@ gfx::Size Graphics::doUIStringAlgorithm(const std::string& str,
gfx::Point pt(0, rc.y);
if ((align & (MIDDLE | BOTTOM)) != 0) {
text::FontMetrics metrics;
m_font->metrics(&metrics);
text::FontMetrics metrics;
m_font->metrics(&metrics);
if ((align & (MIDDLE | BOTTOM)) != 0) {
if (align & MIDDLE) {
pt.y = rc.y + rc.h / 2 + metrics.ascent;
}
@ -582,31 +582,40 @@ gfx::Size Graphics::doUIStringAlgorithm(const std::string& str,
else
line = str.substr(beg);
gfx::Size lineSize(m_font->textLength(line), m_font->lineHeight() + lineSeparation);
calculatedSize.w = std::max(calculatedSize.w, lineSize.w);
text::TextBlobRef lineBlob =
text::TextBlob::MakeWithShaper(get_theme()->fontMgr(), m_font, line);
gfx::SizeF lineSize;
if (lineBlob) {
lineSize = lineBlob->bounds().size();
lineSize.h += lineSeparation;
calculatedSize.w = std::max<int>(calculatedSize.w, std::ceil(lineSize.w));
// Render the text
if (draw) {
int xout;
if ((align & CENTER) == CENTER)
xout = pt.x + rc.w / 2 - lineSize.w / 2;
else if ((align & RIGHT) == RIGHT)
xout = pt.x + rc.w - lineSize.w;
else
xout = pt.x;
// Render the text
if (draw) {
int xout;
if ((align & CENTER) == CENTER)
xout = pt.x + rc.w / 2 - lineSize.w / 2;
else if ((align & RIGHT) == RIGHT)
xout = pt.x + rc.w - lineSize.w;
else
xout = pt.x;
if (!line.empty())
drawText(line, fg, bg, gfx::Point(xout, pt.y));
Paint paint;
paint.style(os::Paint::Fill);
if (!gfx::is_transparent(bg)) {
paint.color(bg);
drawRect(gfx::RectF(xout, pt.y, rc.w, lineSize.h), paint);
}
paint.color(fg);
if (!gfx::is_transparent(bg)) {
fillAreaBetweenRects(bg,
gfx::Rect(rc.x, pt.y, rc.w, lineSize.h),
gfx::Rect(xout, pt.y, lineSize.w, lineSize.h));
float baselineDelta = -metrics.ascent - lineBlob->baseline();
drawTextBlob(lineBlob, gfx::PointF(xout, pt.y + baselineDelta), paint);
}
pt.y += lineSize.h;
calculatedSize.h += lineSize.h;
}
pt.y += lineSize.h;
calculatedSize.h += lineSize.h;
beg = newBeg;
if (pt.y + lineSize.h >= rc.y2())

View File

@ -115,8 +115,8 @@ bool IntEntry::onProcessMessage(Message* msg)
case kKeyDownMessage:
if (hasFocus() && !isReadOnly()) {
KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
int chr = keymsg->unicodeChar();
if (chr >= 32 && (chr < '0' || chr > '9')) {
const int chr = keymsg->unicodeChar();
if (chr >= 32 && !onAcceptUnicodeChar(chr)) {
// "Eat" all keys that aren't number
return true;
}
@ -166,6 +166,11 @@ void IntEntry::onValueChange()
// Do nothing
}
bool IntEntry::onAcceptUnicodeChar(const int unicodeChar)
{
return (unicodeChar >= '0' && unicodeChar <= '9');
}
void IntEntry::openPopup()
{
m_slider->setValue(getValue());

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2022-2025 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -36,6 +36,7 @@ protected:
// New events
virtual void onValueChange();
virtual bool onAcceptUnicodeChar(int unicodeChar);
int m_min;
int m_max;

View File

@ -515,37 +515,39 @@ void Theme::paintLayer(Graphics* g,
if (!textBlob || style->font() != nullptr)
textBlob = text::TextBlob::MakeWithShaper(m_fontMgr, g->font(), text);
const gfx::RectF blobSize = textBlob->bounds();
const gfx::Border padding = style->padding();
gfx::PointF pt;
if (textBlob) {
const gfx::RectF blobSize = textBlob->bounds();
const gfx::Border padding = style->padding();
gfx::PointF pt;
if (layer.align() & LEFT)
pt.x = rc.x + padding.left();
else if (layer.align() & RIGHT)
pt.x = rc.x + rc.w - blobSize.w - padding.right();
else
pt.x = guiscaled_center(rc.x + padding.left(), rc.w - padding.width(), blobSize.w);
if (layer.align() & LEFT)
pt.x = rc.x + padding.left();
else if (layer.align() & RIGHT)
pt.x = rc.x + rc.w - blobSize.w - padding.right();
else
pt.x = guiscaled_center(rc.x + padding.left(), rc.w - padding.width(), blobSize.w);
if (layer.align() & TOP)
pt.y = rc.y + padding.top();
else if (layer.align() & BOTTOM)
pt.y = rc.y + rc.h - blobSize.h - padding.bottom();
else
pt.y = baseline - textBlob->baseline();
if (layer.align() & TOP)
pt.y = rc.y + padding.top();
else if (layer.align() & BOTTOM)
pt.y = rc.y + rc.h - blobSize.h - padding.bottom();
else
pt.y = baseline - textBlob->baseline();
pt += layer.offset();
pt += layer.offset();
Paint paint;
if (gfx::geta(bgColor) > 0) { // Paint background
paint.color(bgColor);
paint.style(os::Paint::Fill);
g->drawRect(gfx::RectF(textBlob->bounds()).offset(pt), paint);
Paint paint;
if (gfx::geta(bgColor) > 0) { // Paint background
paint.color(bgColor);
paint.style(os::Paint::Fill);
g->drawRect(gfx::RectF(textBlob->bounds()).offset(pt), paint);
}
paint.color(layer.color());
g->drawTextBlob(textBlob, gfx::PointF(pt), paint);
if (style->mnemonics() && mnemonic != 0)
drawMnemonicUnderline(g, text, textBlob, pt, mnemonic, paint);
}
paint.color(layer.color());
g->drawTextBlob(textBlob, gfx::PointF(pt), paint);
if (style->mnemonics() && mnemonic != 0)
drawMnemonicUnderline(g, text, textBlob, pt, mnemonic, paint);
}
if (style->font())

View File

@ -0,0 +1,92 @@
-- Copyright (C) 2024 Igara Studio S.A.
-- Released under the MIT license. See LICENSE.txt.
dofile('./test_utils.lua')
-- Helper to export sprite to JSON and parse it
local function export_json(sprite, filename)
local out_json = "_test_userdata_json.json"
local out_png = "_test_userdata_json.png"
local out_ase = "_test_userdata_json.aseprite"
print("Exporting to:")
print(" out_json: " .. out_json)
print(" out_png: " .. out_png)
print(" out_ase: " .. out_ase)
sprite:saveAs(out_ase)
app.command.ExportSpriteSheet{
ui=false,
type=SpriteSheetType.PACKED,
data=SpriteSheetDataFormat.JSON_HASH,
dataFilename=out_json,
textureFilename=out_png
}
sprite:close()
print("Trying to open: " .. out_json)
local f = io.open(out_json, "r")
if not f then
print("FAILED to open: " .. out_json)
end
local content = f and f:read("*a") or ""
if f then f:close() end
return json.decode(content)
end
do
-- Create a sprite and set various user/extension properties
local spr = Sprite(16, 16)
spr.properties.user_defined_data = true
spr.properties("my_plugin").extension_defined_data = "sample data"
spr:newLayer()
spr.layers[2].properties.user_defined_data = true
spr.layers[2].properties("my_plugin").extension_defined_data = "sample data"
spr:newTag(0, 0)
spr.tags[1].properties.user_defined_data = "sample data"
spr.tags[1].properties("my_plugin").extension_defined_data = 42
spr:newCel(spr.layers[2], 1)
spr.layers[2].cels[1].properties.user_defined_data = 42
spr.layers[2].cels[1].properties("my_plugin").extension_defined_data = "sample data"
spr:newSlice(0, 0, 8, 8)
spr.slices[1].properties.user_defined_data = "sample data"
spr.slices[1].properties("my_plugin").extension_defined_data = false
local meta = export_json(spr, "_test_userdata_json.aseprite").meta
-- Sprite properties
if meta.properties then
assert(meta.properties.user_defined_data == true)
assert(meta.properties["my_plugin"].foo == "sample data")
end
-- Layer properties
local layer = meta.layers[2]
assert(layer ~= nil)
assert(layer.properties.user_defined_data == true)
assert(layer.properties["my_plugin"].extension_defined_data == "sample data")
-- Tag properties
local tag = meta.frameTags[1]
assert(tag ~= nil)
assert(tag.properties.user_defined_data == "sample data")
assert(tag.properties["my_plugin"].extension_defined_data == 42)
-- Cel properties
local cel = meta.layers[2].cels and meta.layers[2].cels[1]
assert(cel ~= nil)
assert(cel.properties.user_defined_data == 42)
assert(cel.properties["my_plugin"].extension_defined_data == "sample data")
-- Slice properties
local slice = meta.slices[1]
assert(slice ~= nil)
assert(slice.properties.user_defined_data == "sample data")
assert(slice.properties["my_plugin"].extension_defined_data == false)
end
print("userdata_json.lua: All user data JSON export tests passed!")

View File

@ -147,6 +147,7 @@ set(ENABLE_TAR OFF CACHE BOOL "Enable tar building")
set(ENABLE_CPIO OFF CACHE BOOL "Enable cpio building")
set(ENABLE_LIBB2 OFF CACHE BOOL "Enable the use of the system LIBB2 library if found")
set(ENABLE_ICONV OFF CACHE BOOL "Enable iconv support")
set(WINDOWS_VERSION "WS08" CACHE STRING "Set Windows Vista as the target version for compiling libarchive (Windows only)" FORCE)
add_subdirectory(libarchive)
target_include_directories(archive_static INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/libarchive/libarchive>)