Add new Text tool (fix #28)

This is the first (not yet production-ready) version of the
interactive Text tool. The text input is done with a transparent
ui::Entry, and on each text modification an ExtraCel is rendered with
this same ui::Entry's TextBlob to be displayed in the canvas with the
active zoom level.

The ui::Entry is being painted along the text in the canvas (just for
testing), but this is something to be fixed. Probably it will not be
the case in the future and a fully customized rendering (onPaint())
process will be required.
This commit is contained in:
David Capello 2024-06-11 22:31:13 -03:00
parent a15e5aa376
commit a487bcec7d
35 changed files with 779 additions and 79 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -412,6 +412,7 @@
<part id="tool_filled_ellipse" x="192" y="80" w="16" h="16" />
<part id="tool_contour" x="144" y="96" w="16" h="16" />
<part id="tool_polygon" x="160" y="96" w="16" h="16" />
<part id="tool_text" x="144" y="112" w="16" h="16" />
<part id="tool_blur" x="160" y="112" w="16" h="16" />
<part id="tool_jumble" x="176" y="112" w="16" h="16" />
<part id="tool_configuration" x="144" y="128" w="16" h="16" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -408,6 +408,7 @@
<part id="tool_filled_ellipse" x="192" y="80" w="16" h="16" />
<part id="tool_contour" x="144" y="96" w="16" h="16" />
<part id="tool_polygon" x="160" y="96" w="16" h="16" />
<part id="tool_text" x="144" y="112" w="16" h="16" />
<part id="tool_blur" x="160" y="112" w="16" h="16" />
<part id="tool_jumble" x="176" y="112" w="16" h="16" />
<part id="tool_configuration" x="144" y="128" w="16" h="16" />

View File

@ -66,7 +66,6 @@
<key command="Outline" shortcut="Shift+O" />
<key command="ColorCurve" shortcut="Ctrl+M" />
<key command="ColorCurve" shortcut="F10" />
<key command="PasteText" shortcut="T" />
<key command="Options" mac="Cmd+," />
<key command="Options" shortcut="Ctrl+K" mac="Cmd+K" />
<key command="KeyboardShortcuts" shortcut="Ctrl+Alt+Shift+K" mac="Cmd+Alt+Shift+K" />
@ -611,6 +610,8 @@
<key tool="blur" shortcut="R" />
<key tool="jumble" shortcut="R" />
<key tool="text" shortcut="T" />
</tools>
<!-- Editor Quicktools: these are modifiers to select quickly a
@ -1493,6 +1494,16 @@
default_brush_size="16" />
</group>
<group id="text">
<tool id="text"
text="@.text"
ink="text"
controller="two_points"
pointshape="pixel"
intertwine="as_rectangles"
traceepolicy="last" />
</group>
</tools>
</gui>

View File

@ -1809,6 +1809,7 @@ contour = Contour Tool
polygon = Polygon Tool
blur = Blur Tool
jumble = Jumble Tool
text = Text Tool
shortcut = Shortcut: {0}
preview_hide = Hide Preview
preview_show = Show Preview

View File

@ -403,9 +403,11 @@ if(ENABLE_UI)
ui/editor/play_state.cpp
ui/editor/scrolling_state.cpp
ui/editor/select_box_state.cpp
ui/editor/select_text_box_state.cpp
ui/editor/standby_state.cpp
ui/editor/state_with_wheel_behavior.cpp
ui/editor/transform_handles.cpp
ui/editor/writing_text_state.cpp
ui/editor/zooming_state.cpp
ui/export_file_window.cpp
ui/expr_entry.cpp

View File

@ -16,7 +16,6 @@
#include "app/pref/preferences.h"
#include "app/ui/drop_down_button.h"
#include "app/ui/editor/editor.h"
#include "app/ui/skin/skin_theme.h"
#include "app/ui/timeline/timeline.h"
#include "app/util/render_text.h"
#include "base/fs.h"
@ -75,21 +74,7 @@ void PasteTextCommand::onExecute(Context* ctx)
return;
Preferences& pref = Preferences::instance();
FontInfo fontInfo;
// Old configuration
if (!pref.textTool.fontFace().empty()) {
fontInfo = FontInfo(FontInfo::Type::File,
pref.textTool.fontFace(),
pref.textTool.fontSize(),
text::FontStyle(),
pref.textTool.antialias());
}
// New configuration
if (!pref.textTool.fontInfo().empty()) {
fontInfo = base::convert_to<FontInfo>(pref.textTool.fontInfo());
}
FontInfo fontInfo = FontInfo::getFromPreferences();
PasteTextWindow window(fontInfo, pref.colorBar.fgColor());
window.userText()->setText(last_text_used);
@ -101,20 +86,14 @@ void PasteTextCommand::onExecute(Context* ctx)
last_text_used = window.userText()->text();
fontInfo = window.fontInfo();
pref.textTool.fontInfo(base::convert_to<std::string>(fontInfo));
if (!pref.textTool.fontFace().empty()) {
pref.textTool.fontFace.clearValue();
pref.textTool.fontSize.clearValue();
pref.textTool.antialias.clearValue();
}
fontInfo.updatePreferences();
try {
auto* theme = skin::SkinTheme::instance();
std::string text = window.userText()->text();
app::Color color = window.fontColor()->getColor();
doc::ImageRef image = render_text(
theme->fontMgr(), fontInfo, text,
fontInfo, text,
gfx::rgba(color.getRed(),
color.getGreen(),
color.getBlue(),

View File

@ -9,6 +9,7 @@
#endif
#include "app/font_info.h"
#include "app/pref/preferences.h"
#include "base/fs.h"
#include "base/split_string.h"
#include "fmt/format.h"
@ -76,6 +77,39 @@ text::TypefaceRef FontInfo::findTypeface(const text::FontMgrRef& fontMgr) const
return nullptr;
}
// static
FontInfo FontInfo::getFromPreferences()
{
Preferences& pref = Preferences::instance();
FontInfo fontInfo;
// Old configuration
if (!pref.textTool.fontFace().empty()) {
fontInfo = FontInfo(FontInfo::Type::File,
pref.textTool.fontFace(),
pref.textTool.fontSize(),
text::FontStyle(),
pref.textTool.antialias());
}
// New configuration
if (!pref.textTool.fontInfo().empty()) {
fontInfo = base::convert_to<FontInfo>(pref.textTool.fontInfo());
}
return fontInfo;
}
void FontInfo::updatePreferences()
{
Preferences& pref = Preferences::instance();
pref.textTool.fontInfo(base::convert_to<std::string>(*this));
if (!pref.textTool.fontFace().empty()) {
pref.textTool.fontFace.clearValue();
pref.textTool.fontSize.clearValue();
pref.textTool.antialias.clearValue();
}
}
} // namespace app
namespace base {

View File

@ -61,6 +61,9 @@ namespace app {
text::TypefaceRef findTypeface(const text::FontMgrRef& fontMgr) const;
static FontInfo getFromPreferences();
void updatePreferences();
bool operator==(const FontInfo& other) const {
return (m_type == other.m_type &&
m_name == other.m_name &&

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2020 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -68,6 +68,9 @@ namespace app {
// Returns true if this ink is used to mark slices
virtual bool isSlice() const { return false; }
// Returns true if this ink acts like the text tool
virtual bool isText() const { return false; }
// Returns true if this tool uses the dithering options
virtual bool withDitheringOptions() const { return false; }

View File

@ -529,5 +529,17 @@ public:
};
class TextInk : public BaseInk {
public:
Ink* clone() override { return new TextInk(*this); }
bool isText() const override { return true; }
void prepareInk(ToolLoop* loop) override {
setProc(get_ink_proc<XorInkProcessing>(loop));
}
};
} // namespace tools
} // namespace app

View File

@ -75,6 +75,7 @@ const char* WellKnownInks::Slice = "slice";
const char* WellKnownInks::MoveSlice = "move_slice";
const char* WellKnownInks::Blur = "blur";
const char* WellKnownInks::Jumble = "jumble";
const char* WellKnownInks::Text = "text";
const char* WellKnownControllers::Freehand = "freehand";
const char* WellKnownControllers::PointByPoint = "point_by_point";
@ -135,6 +136,7 @@ ToolBox::ToolBox()
m_inks[WellKnownInks::Slice] = new SliceInk();
m_inks[WellKnownInks::Blur] = new BlurInk();
m_inks[WellKnownInks::Jumble] = new JumbleInk();
m_inks[WellKnownInks::Text] = new TextInk();
m_controllers[WellKnownControllers::Freehand] = new FreehandController();
m_controllers[WellKnownControllers::PointByPoint] = new PointByPointController();

View File

@ -56,6 +56,7 @@ namespace app {
extern const char* MoveSlice;
extern const char* Blur;
extern const char* Jumble;
extern const char* Text;
};
namespace WellKnownControllers {

View File

@ -43,6 +43,7 @@
#include "app/ui/dynamics_popup.h"
#include "app/ui/editor/editor.h"
#include "app/ui/expr_entry.h"
#include "app/ui/font_entry.h"
#include "app/ui/icon_button.h"
#include "app/ui/keyboard_shortcuts.h"
#include "app/ui/sampling_selector.h"
@ -1854,6 +1855,22 @@ private:
std::string m_filter;
};
class ContextBar::FontSelector : public FontEntry {
public:
FontSelector(ContextBar* contextBar) {
// Load the font from the preferences
setInfo(FontInfo::getFromPreferences());
FontChange.connect([contextBar](){
contextBar->FontChange();
});
}
~FontSelector() {
info().updatePreferences();
}
};
ContextBar::ContextBar(TooltipManager* tooltipManager,
ColorBar* colorBar)
{
@ -1908,6 +1925,7 @@ ContextBar::ContextBar(TooltipManager* tooltipManager,
m_symmetry->setVisible(pref.symmetryMode.enabled());
addChild(m_sliceFields = new SliceFields);
addChild(m_fontSelector = new FontSelector(this));
setupTooltips(tooltipManager);
@ -2200,6 +2218,11 @@ void ContextBar::updateForTool(tools::Tool* tool)
(tool->getInk(0)->isSlice() ||
tool->getInk(1)->isSlice());
// True if the current tool is text tool.
const bool isText = tool &&
(tool->getInk(0)->isText() ||
tool->getInk(1)->isText());
// True if the current tool is floodfill
const bool isFloodfill = tool &&
(tool->getPointShape(0)->isFloodFill() ||
@ -2284,6 +2307,8 @@ void ContextBar::updateForTool(tools::Tool* tool)
if (isSlice)
updateSliceFields(UIContext::instance()->activeSite());
m_fontSelector->setVisible(isText);
// Update ink shades with the current selected palette entries
if (updateShade)
m_inkShades->updateShadeFromColorBarPicks();
@ -2571,6 +2596,11 @@ void ContextBar::setInkType(tools::InkType type)
m_inkType->setInkType(type);
}
FontInfo ContextBar::fontInfo() const
{
return m_fontSelector->info();
}
render::DitheringMatrix ContextBar::ditheringMatrix()
{
return m_ditheringSelector->ditheringMatrix();

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -52,6 +52,7 @@ namespace app {
class BrushSlot;
class ColorBar;
class DitheringSelector;
class FontInfo;
class GradientTypeSelector;
class SamplingSelector;
class Transformation;
@ -89,6 +90,9 @@ namespace app {
void setInkType(tools::InkType type);
// For text tool
FontInfo fontInfo() const;
// For gradients
render::DitheringMatrix ditheringMatrix();
render::DitheringAlgorithmBase* ditheringAlgorithm();
@ -99,6 +103,7 @@ namespace app {
// Signals
obs::signal<void()> BrushChange;
obs::signal<void()> FontChange;
protected:
void onInitTheme(ui::InitThemeEvent& ev) override;
@ -163,6 +168,7 @@ namespace app {
class AutoSelectLayerField;
class SymmetryField;
class SliceFields;
class FontSelector;
ZoomButtons* m_zoomButtons;
SamplingSelector* m_samplingSelector;
@ -201,6 +207,7 @@ namespace app {
ui::Label* m_selectBoxHelp;
SymmetryField* m_symmetry;
SliceFields* m_sliceFields;
FontSelector* m_fontSelector = nullptr;
obs::scoped_connection m_symmModeConn;
obs::scoped_connection m_fgColorConn;
obs::scoped_connection m_bgColorConn;

View File

@ -2230,6 +2230,9 @@ void Editor::onResize(ui::ResizeEvent& ev)
{
Widget::onResize(ev);
m_padding = calcExtraPadding(m_proj);
if (m_state)
m_state->onEditorResize(this);
}
void Editor::onPaint(ui::PaintEvent& ev)

View File

@ -83,6 +83,11 @@ namespace app {
// Called when the editor gets the focus.
virtual void onEditorGotFocus(Editor* editor) { }
// Called when the editor is resized. E.g. If a EditorState adds a
// temporary widget inside the editor, this method can layout that
// widget.
virtual void onEditorResize(Editor* editor) { }
// Called when the user presses a mouse button over the editor.
virtual bool onMouseDown(Editor* editor, ui::MouseMessage* msg) { return false; }

View File

@ -0,0 +1,71 @@
// Aseprite
// Copyright (c) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/ui/editor/select_text_box_state.h"
#include "app/ui/editor/editor.h"
#include "app/ui/editor/writing_text_state.h"
#include "app/ui/status_bar.h"
#include "fmt/format.h"
namespace app {
SelectTextBoxState::SelectTextBoxState(Editor* editor,
ui::MouseMessage* msg)
: SelectBoxState(this, editor->sprite()->bounds(),
Flags(int(Flags::QuickBox) |
int(Flags::DarkOutside)))
, m_editor(editor)
{
onMouseDown(editor, msg);
}
bool SelectTextBoxState::onUpdateStatusBar(Editor* editor)
{
gfx::PointF spritePos =
editor->screenToEditorF(editor->mousePosInDisplay())
- gfx::PointF(editor->mainTilePosition());
const gfx::Rect bounds = getBoxBounds();
const std::string buf = fmt::format(
":pos: {} {}"
" :start: {} {} :size: {} {}",
int(spritePos.x), int(spritePos.y),
int(bounds.x), int(bounds.y),
int(bounds.w), int(bounds.h));
StatusBar::instance()->setStatusText(0, buf);
return true;
}
void SelectTextBoxState::onChangeRectangle(const gfx::Rect&)
{
onUpdateStatusBar(m_editor);
}
void SelectTextBoxState::onQuickboxEnd(Editor* editor,
const gfx::Rect& rect,
ui::MouseButton)
{
editor->backToPreviousState();
// A 1x1 rectangle will cancel the operation
if (rect.w > 1 && rect.h > 1) {
EditorStatePtr newState = std::make_shared<WritingTextState>(editor, rect);
editor->setState(newState);
}
}
void SelectTextBoxState::onQuickboxCancel(Editor* editor)
{
editor->backToPreviousState();
}
} // namespace app

View File

@ -0,0 +1,35 @@
// Aseprite
// Copyright (c) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_UI_EDITOR_SELECT_TEXT_BOX_STATE_H_INCLUDED
#define APP_UI_EDITOR_SELECT_TEXT_BOX_STATE_H_INCLUDED
#pragma once
#include "app/ui/editor/select_box_state.h"
namespace app {
class SelectTextBoxState : public SelectBoxState
, public SelectBoxDelegate {
public:
SelectTextBoxState(Editor* editor, ui::MouseMessage* msg);
bool isTemporalState() const override { return true; }
bool onUpdateStatusBar(Editor* editor) override;
// SelectBoxDelegate impl
void onChangeRectangle(const gfx::Rect& rect) override;
void onQuickboxEnd(Editor* editor, const gfx::Rect& rect, ui::MouseButton button) override;
void onQuickboxCancel(Editor* editor) override;
private:
Editor* m_editor;
};
} // namespace app
#endif // APP_UI_EDITOR_DRAWING_STATE_H_INCLUDED

View File

@ -41,6 +41,7 @@
#include "app/ui/editor/pivot_helpers.h"
#include "app/ui/editor/pixels_movement.h"
#include "app/ui/editor/scrolling_state.h"
#include "app/ui/editor/select_text_box_state.h"
#include "app/ui/editor/tool_loop_impl.h"
#include "app/ui/editor/transform_handles.h"
#include "app/ui/editor/vec2.h"
@ -231,6 +232,7 @@ bool StandbyState::onMouseDown(Editor* editor, MouseMessage* msg)
return true;
}
// Handle Slice tool
if (clickedInk->isSlice()) {
EditorHit hit = editor->calcHit(msg->position());
switch (hit.type()) {
@ -269,6 +271,13 @@ bool StandbyState::onMouseDown(Editor* editor, MouseMessage* msg)
}
}
// Handle Text tool
if (clickedInk->isText()) {
EditorStatePtr newState(new SelectTextBoxState(editor, msg));
editor->setState(newState);
return true;
}
// Only if the selected tool or quick tool is selection, we give the
// possibility to transform/move the selection. In other case,
// e.g. when selection is used with right-click mode, the
@ -487,6 +496,10 @@ bool StandbyState::onSetCursor(Editor* editor, const gfx::Point& mouseScreenPos)
return true;
}
}
else if (ink->isText()) {
editor->showMouseCursor(kCrosshairCursor);
return true;
}
}
return StateWithWheelBehavior::onSetCursor(editor, mouseScreenPos);
@ -698,7 +711,7 @@ bool StandbyState::checkStartDrawingStraightLine(Editor* editor,
if (drawingState) {
// Disable stabilizer so that it does not affect the line preview
drawingState->disableMouseStabilizer();
drawingState->sendMovementToToolLoop(
tools::Pointer(
pointer ? pointer->point(): editor->screenToEditor(editor->mousePosInDisplay()),

View File

@ -0,0 +1,349 @@
// Aseprite
// Copyright (C) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/ui/editor/writing_text_state.h"
#include "app/app.h"
#include "app/color_utils.h"
#include "app/commands/command.h"
#include "app/extra_cel.h"
#include "app/font_info.h"
#include "app/pref/preferences.h"
#include "app/site.h"
#include "app/tx.h"
#include "app/ui/context_bar.h"
#include "app/ui/editor/editor.h"
#include "app/ui/skin/skin_theme.h"
#include "app/ui/status_bar.h"
#include "app/ui_context.h"
#include "app/util/expand_cel_canvas.h"
#include "app/util/render_text.h"
#include "doc/blend_image.h"
#include "doc/blend_internals.h"
#include "doc/layer.h"
#include "render/dithering.h"
#include "render/quantization.h"
#include "render/render.h"
#include "ui/entry.h"
#include "ui/message.h"
#include "ui/paint_event.h"
namespace app {
using namespace ui;
class WritingTextState::TextEditor : public Entry {
public:
TextEditor(Editor* editor,
const Site& site,
const gfx::Rect& bounds)
: Entry(4096, "")
, m_editor(editor)
, m_doc(site.document())
, m_extraCel(new ExtraCel) {
// We have to draw the editor as background of this ui::Entry.
setTransparent(true);
// TODO move this opacity() to Site class
int t, opacity = (site.layer()->isImage() ?
static_cast<LayerImage*>(site.layer())->opacity(): 255);
Cel* cel = site.cel();
if (cel) opacity = MUL_UN8(opacity, cel->opacity(), t);
m_extraCel->create(
site.tilemapMode(),
site.sprite(),
bounds,
bounds.size(),
site.frame(),
255);
m_extraCel->setType(render::ExtraType::PATCH);
m_extraCel->setBlendMode(site.layer()->isImage() ?
static_cast<LayerImage*>(site.layer())->blendMode():
doc::BlendMode::NORMAL);
renderExtraCelBase();
FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
if (auto font = get_font_from_info(fontInfo))
setFont(font);
}
~TextEditor() {
m_doc->setExtraCel(ExtraCelRef(nullptr));
m_doc->generateMaskBoundaries();
}
ExtraCelRef extraCel() const { return m_extraCel; }
private:
bool onProcessMessage(Message* msg) override {
switch (msg->type()) {
case kMouseDownMessage:
case kMouseMoveMessage: {
auto* mouseMsg = static_cast<MouseMessage*>(msg);
// Ignore middle mouse button so we can scroll with it.
if (mouseMsg->middle()) {
auto* parent = this->parent();
MouseMessage mouseMsg2(kMouseDownMessage,
*mouseMsg,
mouseMsg->position());
mouseMsg2.setRecipient(parent);
parent->sendMessage(&mouseMsg2);
return true;
}
break;
}
}
return Entry::onProcessMessage(msg);
}
void onInitTheme(InitThemeEvent& ev) override {
Entry::onInitTheme(ev);
setBgColor(gfx::ColorNone);
}
void onPaint(PaintEvent& ev) override {
Entry::onPaint(ev);
if (!hasText())
return;
try {
text::TextBlobRef blob = textBlob();
if (!blob) {
m_doc->setExtraCel(nullptr);
m_editor->invalidate();
return;
}
const auto textColor =
color_utils::color_for_image(
Preferences::instance().colorBar.fgColor(),
IMAGE_RGB);
doc::ImageRef image = render_text_blob(blob, textColor);
if (image) {
renderExtraCelBase();
doc::blend_image(
m_extraCel->image(), image.get(),
gfx::Clip(image->bounds().size()),
m_doc->sprite()->palette(m_editor->frame()),
255, doc::BlendMode::NORMAL);
m_doc->setExtraCel(m_extraCel);
m_editor->invalidate();
}
}
catch (const std::exception& e) {
StatusBar::instance()->showTip(500, std::string(e.what()));
}
}
void renderExtraCelBase() {
auto extraImg = m_extraCel->image();
extraImg->clear(extraImg->maskColor());
render::Render().renderLayer(
extraImg,
m_editor->layer(),
m_editor->frame(),
gfx::Clip(0, 0, m_extraCel->cel()->bounds()),
doc::BlendMode::SRC);
}
Editor* m_editor;
Doc* m_doc;
ExtraCelRef m_extraCel;
};
WritingTextState::WritingTextState(Editor* editor,
const gfx::Rect& bounds)
: m_editor(editor)
, m_bounds(bounds)
, m_entry(new TextEditor(editor, editor->getSite(), bounds))
{
m_beforeCmdConn =
UIContext::instance()->BeforeCommandExecution.connect(
&WritingTextState::onBeforeCommandExecution, this);
m_fontChangeConn =
App::instance()->contextBar()->FontChange.connect(
&WritingTextState::onFontChange, this);
m_entry->setBounds(calcEntryBounds());
}
WritingTextState::~WritingTextState()
{
}
bool WritingTextState::onMouseDown(Editor* editor, MouseMessage* msg)
{
if (msg->left()) {
drop();
}
else if (msg->right()) {
cancel();
return true;
}
return StandbyState::onMouseDown(editor, msg);
}
bool WritingTextState::onMouseUp(Editor* editor, MouseMessage* msg)
{
const bool result = StandbyState::onMouseUp(editor, msg);
if (msg->middle()) {
if (m_entry)
m_entry->requestFocus();
}
return result;
}
bool WritingTextState::onSetCursor(Editor* editor, const gfx::Point&)
{
editor->showMouseCursor(kArrowCursor);
return true;
}
bool WritingTextState::onKeyDown(Editor*, KeyMessage*)
{
return false;
}
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
{
// Cancel loop pressing Esc key
if (msg->scancode() == ui::kKeyEsc) {
cancel();
}
// Drop text pressing Enter key
else if (msg->scancode() == ui::kKeyEnter) {
drop();
}
return true;
}
void WritingTextState::onEditorResize(Editor*)
{
m_entry->setBounds(calcEntryBounds());
}
gfx::Rect WritingTextState::calcEntryBounds()
{
const View* view = View::getView(m_editor);
const gfx::Rect vp = view->viewportBounds();
const gfx::Point scroll = view->viewScroll();
const auto& padding = m_editor->padding();
const auto& proj = m_editor->projection();
gfx::Point pt1(m_bounds.origin());
gfx::Point pt2(m_bounds.point2());
pt1.x = vp.x - scroll.x + padding.x + proj.applyX(pt1.x);
pt1.y = vp.y - scroll.y + padding.y + proj.applyY(pt1.y);
pt2.x = vp.x - scroll.x + padding.x + proj.applyX(pt2.x);
pt2.y = vp.y - scroll.y + padding.y + proj.applyY(pt2.y);
return gfx::Rect(pt1, pt2);
}
void WritingTextState::onEnterState(Editor* editor)
{
StandbyState::onEnterState(editor);
editor->invalidate();
editor->addChild(m_entry.get());
m_entry->requestFocus();
}
EditorState::LeaveAction WritingTextState::onLeaveState(Editor* editor, EditorState* newState)
{
if (!newState || !newState->isTemporalState()) {
if (!m_discarded) {
// 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");
ExpandCelCanvas expand(
site, site.layer(),
TiledMode::NONE, tx,
ExpandCelCanvas::None);
expand.validateDestCanvas(
gfx::Region(extraCel->cel()->bounds()));
doc::blend_image(
expand.getDestCanvas(),
extraCel->image(),
gfx::Clip(extraCel->cel()->position(),
extraCel->image()->bounds()),
site.palette(),
255, doc::BlendMode::NORMAL);
expand.commit();
tx.commit();
}
m_editor->releaseMouse();
m_editor->document()->notifyGeneralUpdate();
return DiscardState;
}
editor->releaseMouse();
return KeepState;
}
void WritingTextState::onBeforePopState(Editor* editor)
{
editor->removeChild(m_entry.get());
m_beforeCmdConn.disconnect();
m_fontChangeConn.disconnect();
StandbyState::onBeforePopState(editor);
}
void WritingTextState::onBeforeCommandExecution(CommandExecutionEvent& ev)
{
if (// Undo/Redo/Cancel will cancel this state
ev.command()->id() == CommandId::Undo() ||
ev.command()->id() == CommandId::Redo() ||
ev.command()->id() == CommandId::Cancel()) {
cancel();
}
else {
drop();
}
}
void WritingTextState::onFontChange()
{
const FontInfo fontInfo = App::instance()->contextBar()->fontInfo();
if (auto font = get_font_from_info(fontInfo)) {
m_entry->setFont(font);
m_entry->invalidate();
m_editor->invalidate();
}
}
void WritingTextState::cancel()
{
m_discarded = true;
m_editor->backToPreviousState();
m_editor->invalidate();
}
void WritingTextState::drop()
{
m_editor->backToPreviousState();
m_editor->invalidate();
}
} // namespace app

View File

@ -0,0 +1,58 @@
// Aseprite
// Copyright (c) 2022-2024 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifndef APP_UI_EDITOR_WRITING_TEXT_STATE_H_INCLUDED
#define APP_UI_EDITOR_WRITING_TEXT_STATE_H_INCLUDED
#pragma once
#include "app/ui/editor/standby_state.h"
#include <memory>
namespace app {
class CommandExecutionEvent;
class WritingTextState : public StandbyState {
public:
WritingTextState(Editor* editor,
const gfx::Rect& bounds);
~WritingTextState();
LeaveAction onLeaveState(Editor* editor, EditorState* newState) override;
void onEnterState(Editor* editor) override;
void onBeforePopState(Editor* editor) override;
void onEditorResize(Editor* editor) override;
bool onMouseDown(Editor* editor, ui::MouseMessage* msg) override;
bool onMouseUp(Editor* editor, ui::MouseMessage* msg) override;
bool onSetCursor(Editor* editor, const gfx::Point& mouseScreenPos) override;
bool onKeyDown(Editor* editor, ui::KeyMessage* msg) override;
bool onKeyUp(Editor* editor, ui::KeyMessage* msg) override;
private:
gfx::Rect calcEntryBounds();
void onBeforeCommandExecution(CommandExecutionEvent& ev);
void onFontChange();
void cancel();
void drop();
void switchCaretVisiblity();
class TextEditor;
Editor* m_editor;
gfx::Rect m_bounds;
std::unique_ptr<TextEditor> m_entry;
// True if the text was discarded.
bool m_discarded = false;
obs::scoped_connection m_beforeCmdConn;
obs::scoped_connection m_fontChangeConn;
};
} // namespace app
#endif // APP_UI_EDITOR_DRAWING_STATE_H_INCLUDED

View File

@ -16,6 +16,7 @@
#include "ui/display.h"
#include "ui/manager.h"
#include "ui/message.h"
#include "ui/scale.h"
#include <cstdlib>
@ -155,6 +156,8 @@ FontEntry::FontEntry()
addChild(&m_style);
addChild(&m_antialias);
m_face.setMinSize(gfx::Size(128*guiscale(), 0));
m_face.FontChange.connect([this](const FontInfo& newTypeName) {
setInfo(FontInfo(newTypeName,
m_info.size(),
@ -212,6 +215,12 @@ FontEntry::FontEntry()
});
}
// Defined here as FontPopup type is not fully defined in the header
// file (and we have a std::unique_ptr<FontPopup> in FontEntry::FontFace).
FontEntry::~FontEntry()
{
}
void FontEntry::setInfo(const FontInfo& info,
const From fromField)
{

View File

@ -31,6 +31,7 @@ namespace app {
};
FontEntry();
~FontEntry();
FontInfo info() { return m_info; }
void setInfo(const FontInfo& info, From from = From::User);

View File

@ -125,10 +125,9 @@ private:
const auto* theme = app::skin::SkinTheme::get(this);
try {
const text::FontMgrRef fontMgr = theme->fontMgr();
const gfx::Color color = theme->colors.text();
doc::ImageRef image =
render_text(fontMgr, m_fontInfo, text(), color);
render_text(m_fontInfo, text(), color);
if (!image)
return;
@ -330,12 +329,12 @@ void FontPopup::showPopup(Display* display,
m_listBox.selectChild(nullptr);
ui::fit_bounds(display, this,
gfx::Rect(buttonBounds.x, buttonBounds.y2(), 32, 32),
gfx::Rect(buttonBounds.x, buttonBounds.y2(),
buttonBounds.w*2, buttonBounds.h),
[](const gfx::Rect& workarea,
gfx::Rect& bounds,
std::function<gfx::Rect(Widget*)> getWidgetBounds) {
bounds.w = workarea.w / 2;
bounds.h = workarea.h / 2;
bounds.h = workarea.y2() - bounds.y;
});
openWindow();

View File

@ -1141,17 +1141,21 @@ void SkinTheme::paintEntry(PaintEvent& ev)
gfx::Rect bounds = widget->clientBounds();
// Outside borders
g->fillRect(BGCOLOR, bounds);
const gfx::Color bg = BGCOLOR;
if (!is_transparent(bg))
g->fillRect(bg, bounds);
bool isMiniLook = false;
auto skinPropery = std::static_pointer_cast<SkinProperty>(widget->getProperty(SkinProperty::Name));
if (skinPropery)
isMiniLook = (skinPropery->getLook() == MiniLook);
drawRect(g, bounds,
drawRect(
g, bounds,
(widget->hasFocus() ?
(isMiniLook ? parts.sunkenMiniFocused().get(): parts.sunkenFocused().get()):
(isMiniLook ? parts.sunkenMiniNormal().get() : parts.sunkenNormal().get())));
(isMiniLook ? parts.sunkenMiniNormal().get() : parts.sunkenNormal().get())),
!is_transparent(bg)); // Paint center if the BG is not transparent
drawEntryText(g, widget);
}

View File

@ -65,26 +65,23 @@ private:
} // anonymous namespace
doc::ImageRef render_text(
const text::FontMgrRef& fontMgr,
const FontInfo& fontInfo,
const std::string& text,
gfx::Color color)
text::FontRef get_font_from_info(
const FontInfo& fontInfo)
{
doc::ImageRef image;
auto* theme = skin::SkinTheme::instance();
ASSERT(theme);
if (!theme)
return nullptr;
text::FontRef font;
const text::FontMgrRef fontMgr = theme->fontMgr();
if (!fontMgr)
return nullptr;
text::FontRef font;
if (fontInfo.type() == FontInfo::Type::System) {
// Just in case the typeface is not present in the FontInfo
auto typeface = fontInfo.findTypeface(fontMgr);
const text::FontMgrRef fontMgr = theme->fontMgr();
font = fontMgr->makeFont(typeface);
if (!fontInfo.useDefaultSize())
font->setSize(fontInfo.size());
@ -98,50 +95,98 @@ doc::ImageRef render_text(
}
}
if (font)
font->setAntialias(fontInfo.antialias());
return font;
}
text::TextBlobRef create_text_blob(
const FontInfo& fontInfo,
const std::string& text)
{
const text::FontRef font = get_font_from_info(fontInfo);
if (!font)
return nullptr;
font->setAntialias(fontInfo.antialias());
auto* theme = skin::SkinTheme::instance();
const text::FontMgrRef fontMgr = theme->fontMgr();
return text::TextBlob::MakeWithShaper(fontMgr, font, text);
}
doc::ImageRef render_text_blob(
const text::TextBlobRef& blob,
gfx::Color color)
{
ASSERT(blob != nullptr);
os::Paint paint;
// TODO offer Stroke, StrokeAndFill, and Fill styles
paint.style(os::Paint::Fill);
paint.color(color);
gfx::RectF bounds(0, 0, 1, 1);
blob->visitRuns([&bounds](text::TextBlob::RunInfo& run){
for (int i=0; i<run.glyphCount; ++i) {
bounds |= run.getGlyphBounds(i);
bounds |= gfx::RectF(0, 0, 1, run.font->metrics(nullptr));
}
});
if (bounds.w < 1) bounds.w = 1;
if (bounds.h < 1) bounds.h = 1;
doc::ImageRef image(
doc::Image::create(doc::IMAGE_RGB, bounds.w, bounds.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);
#endif // LAF_SKIA
return image;
}
doc::ImageRef render_text(
const FontInfo& fontInfo,
const std::string& text,
gfx::Color color)
{
const text::FontRef font = get_font_from_info(fontInfo);
if (!font)
return nullptr;
auto* theme = skin::SkinTheme::instance();
const text::FontMgrRef fontMgr = theme->fontMgr();
os::Paint paint;
paint.style(os::Paint::StrokeAndFill);
paint.color(color);
text::TextBlobRef blob;
gfx::RectF bounds;
// 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).
MeasureHandler handler;
text::TextBlobRef blob =
text::TextBlob::MakeWithShaper(fontMgr, font, text, &handler);
if (!blob)
return nullptr;
if (fontInfo.type() == FontInfo::Type::System) {
// For native fonts 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).
MeasureHandler handler;
blob = text::TextBlob::MakeWithShaper(fontMgr, font, text, &handler);
bounds = handler.bounds();
}
else {
font->measureText(text, &bounds, &paint);
bounds.w += 1 + std::abs(bounds.x);
text::FontMetrics metrics;
bounds.h = font->metrics(&metrics);
}
gfx::RectF bounds = handler.bounds();
if (bounds.w < 1) bounds.w = 1;
if (bounds.h < 1) bounds.h = 1;
image.reset(doc::Image::create(doc::IMAGE_RGB, bounds.w, bounds.h));
doc::ImageRef image(
doc::Image::create(doc::IMAGE_RGB, bounds.w, bounds.h));
#ifdef LAF_SKIA
// Wrap the doc::Image into a os::Surface to render the text
sk_sp<SkSurface> skSurface = wrap_docimage_in_sksurface(image.get());
os::SurfaceRef surface = base::make_ref<os::SkiaSurface>(skSurface);
if (fontInfo.type() == FontInfo::Type::System) {
text::draw_text(surface.get(), blob,
gfx::PointF(0, 0), &paint);
}
else {
text::draw_text(
surface.get(), fontMgr, font, text,
color, gfx::ColorNone, 0, 0, nullptr);
}
text::draw_text(surface.get(), blob,
gfx::PointF(0, 0), &paint);
#endif // LAF_SKIA
return image;

View File

@ -12,6 +12,7 @@
#include "doc/image_ref.h"
#include "gfx/color.h"
#include "text/font_mgr.h"
#include "text/text_blob.h"
#include <string>
@ -20,8 +21,18 @@ namespace app {
class Color;
class FontInfo;
text::FontRef get_font_from_info(
const FontInfo& fontInfo);
text::TextBlobRef create_text_blob(
const FontInfo& fontInfo,
const std::string& text);
doc::ImageRef render_text_blob(
const text::TextBlobRef& blob,
gfx::Color color);
doc::ImageRef render_text(
const text::FontMgrRef& fontMgr,
const FontInfo& fontInfo,
const std::string& text,
gfx::Color color);

View File

@ -23,7 +23,7 @@ namespace render {
COMPOSITE,
// Composite the current cel two times (don't use the extral cel),
// but the second time using the extral blend mode.
// but the second time using the extra cel blend mode.
OVER_COMPOSITE,
};

View File

@ -46,6 +46,7 @@ namespace ui {
void setCaretPos(int pos);
void setCaretToEnd();
bool isCaretVisible() const { return !m_hidden && m_state; }
void selectText(int from, int to);
void selectAllText();

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2018-2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -193,7 +193,7 @@ Viewport* View::viewport()
return &m_viewport;
}
Rect View::viewportBounds()
Rect View::viewportBounds() const
{
return m_viewport.bounds() - m_viewport.border();
}

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -54,7 +54,7 @@ namespace ui {
void updateView(const bool restoreScrollPos = true);
Viewport* viewport();
gfx::Rect viewportBounds();
gfx::Rect viewportBounds() const;
// For viewable widgets
static View* getView(const Widget* viewableWidget);

View File

@ -174,6 +174,14 @@ text::Font* Widget::font() const
return m_font.get();
}
void Widget::setFont(const text::FontRef& font)
{
if (m_font != font) {
m_font = font;
m_blob.reset();
}
}
void Widget::setBgColor(gfx::Color color)
{
assert_ui_thread();

View File

@ -139,6 +139,7 @@ namespace ui {
// ===============================================================
text::Font* font() const;
void setFont(const text::FontRef& font);
// Gets the background color of the widget.
gfx::Color bgColor() const {