mirror of
https://github.com/aseprite/aseprite.git
synced 2025-02-28 06:39:53 +00:00
New font selector (fix #4363)
This new font selector list installed fonts with its proper name. It still needs some extra work to select font set styles (regular, bold, italic, etc.)
This commit is contained in:
parent
1d9f14665f
commit
cf514f0c53
@ -51,7 +51,7 @@ To compile Aseprite you will need:
|
||||
|
||||
* The latest version of [CMake](https://cmake.org) (3.16 or greater)
|
||||
* [Ninja](https://ninja-build.org) build system
|
||||
* And a compiled version of the `aseprite-m102` branch of
|
||||
* And a compiled version of the `aseprite-m124` branch of
|
||||
the [Skia library](https://github.com/aseprite/skia#readme).
|
||||
There are [pre-built packages available](https://github.com/aseprite/skia/releases).
|
||||
You can get some extra information in
|
||||
|
@ -349,6 +349,7 @@
|
||||
<option id="font_face" type="std::string" />
|
||||
<option id="font_size" type="int" default="12" />
|
||||
<option id="antialias" type="bool" default="false" />
|
||||
<option id="font_info" type="std::string" />
|
||||
</section>
|
||||
<section id="symmetry_mode">
|
||||
<option id="enabled" type="bool" default="false" />
|
||||
|
@ -765,8 +765,10 @@ preview = &Preview
|
||||
tiled = &Tiled
|
||||
|
||||
[font_popup]
|
||||
title = Fonts
|
||||
load = Load
|
||||
theme_fonts = Theme Fonts
|
||||
system_fonts = System Fonts
|
||||
load = Load External Font
|
||||
select_truetype_fonts = Select a Font File
|
||||
empty_fonts = No system fonts were found
|
||||
|
||||
[frame_combo]
|
||||
|
@ -1,12 +1,12 @@
|
||||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2024 by Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2015-2017 by David Capello -->
|
||||
<gui>
|
||||
<vbox id="font_popup">
|
||||
<search id="search" magnet="true" />
|
||||
<view id="view" expansive="true" />
|
||||
<hbox>
|
||||
<boxfiller />
|
||||
<button id="load_font" text="@.load" magnet="true" minwidth="80" />
|
||||
<button id="load_font" text="@.load" minwidth="80" />
|
||||
</hbox>
|
||||
</vbox>
|
||||
</gui>
|
||||
|
@ -1,24 +1,19 @@
|
||||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2024 by Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2015-2018 by David Capello -->
|
||||
<gui>
|
||||
<window id="paste_text" text="@.title">
|
||||
<grid columns="2">
|
||||
<label text="@.text" />
|
||||
<entry expansive="true" maxsize="256" id="user_text" magnet="true" cell_align="horizontal" />
|
||||
|
||||
<label text="@.font_size" />
|
||||
<expr id="font_size" text="32" cell_align="horizontal" />
|
||||
<entry expansive="true" maxsize="256" minwidth="256" id="user_text" magnet="true" cell_align="horizontal" />
|
||||
|
||||
<label text="@.font" />
|
||||
<dropdownbutton minwidth="60" id="font_face" text="@.select_font" cell_align="horizontal" />
|
||||
<font id="font_face" cell_align="horizontal" />
|
||||
|
||||
<label text="@.color" />
|
||||
<colorpicker id="font_color" cell_align="horizontal" />
|
||||
|
||||
<hbox />
|
||||
<check id="antialias"
|
||||
text="@.antialias" cell_align="horizontal"
|
||||
tooltip="@.antialias_tooltip" />
|
||||
<hbox>
|
||||
<colorpicker id="font_color" />
|
||||
</hbox>
|
||||
|
||||
<separator horizontal="true" cell_hspan="2" />
|
||||
|
||||
|
@ -413,6 +413,7 @@ if(ENABLE_UI)
|
||||
ui/file_list_view.cpp
|
||||
ui/file_selector.cpp
|
||||
ui/filename_field.cpp
|
||||
ui/font_entry.cpp
|
||||
ui/font_popup.cpp
|
||||
ui/hex_color_entry.cpp
|
||||
ui/home_view.cpp
|
||||
@ -661,6 +662,7 @@ target_sources(app-lib PRIVATE
|
||||
file_system.cpp
|
||||
filename_formatter.cpp
|
||||
flatten.cpp
|
||||
font_info.cpp
|
||||
font_path.cpp
|
||||
gui_xml.cpp
|
||||
i18n/strings.cpp
|
||||
@ -710,7 +712,6 @@ target_sources(app-lib PRIVATE
|
||||
util/conversion_to_surface.cpp
|
||||
util/expand_cel_canvas.cpp
|
||||
util/filetoks.cpp
|
||||
util/freetype_utils.cpp
|
||||
util/layer_boundaries.cpp
|
||||
util/layer_utils.cpp
|
||||
util/msk_file.cpp
|
||||
@ -720,6 +721,7 @@ target_sources(app-lib PRIVATE
|
||||
util/pixel_ratio.cpp
|
||||
util/range_utils.cpp
|
||||
util/readable_time.cpp
|
||||
util/render_text.cpp
|
||||
util/resize_image.cpp
|
||||
util/shader_helpers.cpp
|
||||
util/tile_flags_utils.cpp
|
||||
@ -744,7 +746,6 @@ target_link_libraries(app-lib
|
||||
tga-lib
|
||||
laf-gfx
|
||||
render-lib
|
||||
laf-ft
|
||||
laf-os
|
||||
laf-text
|
||||
ui-lib
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2019-2022 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -13,13 +13,12 @@
|
||||
#include "app/commands/command.h"
|
||||
#include "app/console.h"
|
||||
#include "app/context.h"
|
||||
#include "app/file_selector.h"
|
||||
#include "app/pref/preferences.h"
|
||||
#include "app/ui/drop_down_button.h"
|
||||
#include "app/ui/editor/editor.h"
|
||||
#include "app/ui/font_popup.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
#include "app/ui/timeline/timeline.h"
|
||||
#include "app/util/freetype_utils.h"
|
||||
#include "app/util/render_text.h"
|
||||
#include "base/fs.h"
|
||||
#include "base/string.h"
|
||||
#include "doc/image.h"
|
||||
@ -57,84 +56,16 @@ bool PasteTextCommand::onEnabled(Context* ctx)
|
||||
|
||||
class PasteTextWindow : public app::gen::PasteText {
|
||||
public:
|
||||
PasteTextWindow(const std::string& face, int size,
|
||||
bool antialias,
|
||||
const app::Color& color)
|
||||
: m_face(face) {
|
||||
ok()->setEnabled(!m_face.empty());
|
||||
if (!m_face.empty())
|
||||
updateFontFaceButton();
|
||||
|
||||
fontSize()->setTextf("%d", size);
|
||||
fontFace()->Click.connect([this]{ onSelectFontFile(); });
|
||||
fontFace()->DropDownClick.connect([this]{ onSelectSystemFont(); });
|
||||
PasteTextWindow(const FontInfo& fontInfo,
|
||||
const app::Color& color) {
|
||||
fontFace()->setInfo(fontInfo);
|
||||
fontColor()->setColor(color);
|
||||
this->antialias()->setSelected(antialias);
|
||||
}
|
||||
|
||||
std::string faceValue() const {
|
||||
return m_face;
|
||||
FontInfo fontInfo() const {
|
||||
return fontFace()->info();
|
||||
}
|
||||
|
||||
int sizeValue() const {
|
||||
int size = fontSize()->textInt();
|
||||
size = std::clamp(size, 1, 5000);
|
||||
return size;
|
||||
}
|
||||
|
||||
private:
|
||||
void updateFontFaceButton() {
|
||||
fontFace()->mainButton()
|
||||
->setTextf("Select Font: %s",
|
||||
base::get_file_title(m_face).c_str());
|
||||
}
|
||||
|
||||
void onSelectFontFile() {
|
||||
base::paths exts = { "ttf", "ttc", "otf", "dfont" };
|
||||
base::paths face;
|
||||
if (!show_file_selector(
|
||||
"Select a TrueType Font",
|
||||
m_face, exts,
|
||||
FileSelectorType::Open, face))
|
||||
return;
|
||||
|
||||
ASSERT(!face.empty());
|
||||
setFontFace(face.front());
|
||||
}
|
||||
|
||||
void setFontFace(const std::string& face) {
|
||||
m_face = face;
|
||||
ok()->setEnabled(true);
|
||||
updateFontFaceButton();
|
||||
}
|
||||
|
||||
void onSelectSystemFont() {
|
||||
if (!m_fontPopup) {
|
||||
try {
|
||||
m_fontPopup.reset(new FontPopup());
|
||||
m_fontPopup->Load.connect(&PasteTextWindow::setFontFace, this);
|
||||
m_fontPopup->Close.connect([this]{ onCloseFontPopup(); });
|
||||
}
|
||||
catch (const std::exception& ex) {
|
||||
Console::showException(ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_fontPopup->isVisible()) {
|
||||
m_fontPopup->showPopup(display(), fontFace()->bounds());
|
||||
}
|
||||
else {
|
||||
m_fontPopup->closeWindow(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
void onCloseFontPopup() {
|
||||
fontFace()->dropDown()->requestFocus();
|
||||
}
|
||||
|
||||
std::string m_face;
|
||||
std::unique_ptr<FontPopup> m_fontPopup;
|
||||
};
|
||||
|
||||
void PasteTextCommand::onExecute(Context* ctx)
|
||||
@ -144,10 +75,21 @@ void PasteTextCommand::onExecute(Context* ctx)
|
||||
return;
|
||||
|
||||
Preferences& pref = Preferences::instance();
|
||||
PasteTextWindow window(pref.textTool.fontFace(),
|
||||
pref.textTool.fontSize(),
|
||||
pref.textTool.antialias(),
|
||||
pref.colorBar.fgColor());
|
||||
FontInfo fontInfo;
|
||||
|
||||
// Old configuration
|
||||
if (!pref.textTool.fontFace().empty()) {
|
||||
fontInfo = FontInfo(FontInfo::Type::File,
|
||||
pref.textTool.fontFace(),
|
||||
pref.textTool.fontSize(),
|
||||
pref.textTool.antialias());
|
||||
}
|
||||
// New configuration
|
||||
if (!pref.textTool.fontInfo().empty()) {
|
||||
fontInfo = base::convert_to<FontInfo>(pref.textTool.fontInfo());
|
||||
}
|
||||
|
||||
PasteTextWindow window(fontInfo, pref.colorBar.fgColor());
|
||||
|
||||
window.userText()->setText(last_text_used);
|
||||
|
||||
@ -157,23 +99,25 @@ void PasteTextCommand::onExecute(Context* ctx)
|
||||
|
||||
last_text_used = window.userText()->text();
|
||||
|
||||
bool antialias = window.antialias()->isSelected();
|
||||
std::string faceName = window.faceValue();
|
||||
int size = window.sizeValue();
|
||||
size = std::clamp(size, 1, 999);
|
||||
pref.textTool.fontFace(faceName);
|
||||
pref.textTool.fontSize(size);
|
||||
pref.textTool.antialias(antialias);
|
||||
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();
|
||||
}
|
||||
|
||||
try {
|
||||
auto* theme = skin::SkinTheme::instance();
|
||||
std::string text = window.userText()->text();
|
||||
app::Color appColor = window.fontColor()->getColor();
|
||||
doc::color_t color = doc::rgba(appColor.getRed(),
|
||||
appColor.getGreen(),
|
||||
appColor.getBlue(),
|
||||
appColor.getAlpha());
|
||||
app::Color color = window.fontColor()->getColor();
|
||||
|
||||
doc::ImageRef image(render_text(faceName, size, text, color, antialias));
|
||||
doc::ImageRef image = render_text(
|
||||
theme->fontMgr(), fontInfo, text,
|
||||
gfx::rgba(color.getRed(),
|
||||
color.getGreen(),
|
||||
color.getBlue(),
|
||||
color.getAlpha()));
|
||||
if (image) {
|
||||
Sprite* sprite = editor->sprite();
|
||||
if (image->pixelFormat() != sprite->pixelFormat()) {
|
||||
|
142
src/app/font_info.cpp
Normal file
142
src/app/font_info.cpp
Normal file
@ -0,0 +1,142 @@
|
||||
// Aseprite
|
||||
// Copyright (c) 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/font_info.h"
|
||||
#include "base/fs.h"
|
||||
#include "base/split_string.h"
|
||||
#include "fmt/format.h"
|
||||
#include "text/font_mgr.h"
|
||||
#include "text/font_style_set.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace app {
|
||||
|
||||
FontInfo::FontInfo(Type type,
|
||||
const std::string& name,
|
||||
const float size,
|
||||
const bool antialias,
|
||||
const text::TypefaceRef& typeface)
|
||||
: m_type(type)
|
||||
, m_name(name)
|
||||
, m_size(size)
|
||||
, m_antialias(antialias)
|
||||
, m_typeface(typeface)
|
||||
{
|
||||
}
|
||||
|
||||
FontInfo::FontInfo(const FontInfo& other,
|
||||
const float size,
|
||||
const bool antialias)
|
||||
: m_type(other.type())
|
||||
, m_name(other.name())
|
||||
, m_size(size)
|
||||
, m_antialias(antialias)
|
||||
, m_typeface(other.typeface())
|
||||
{
|
||||
}
|
||||
|
||||
std::string FontInfo::title() const
|
||||
{
|
||||
return m_type == FontInfo::Type::File ?
|
||||
base::get_file_name(m_name):
|
||||
m_name;
|
||||
}
|
||||
|
||||
std::string FontInfo::thumbnailId() const
|
||||
{
|
||||
switch (m_type) {
|
||||
case app::FontInfo::Type::Unknown: break;
|
||||
case app::FontInfo::Type::Name: return m_name;
|
||||
case app::FontInfo::Type::File: return "file=" + m_name;
|
||||
case app::FontInfo::Type::System: return "system=" + m_name;
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
void FontInfo::findTypeface(const text::FontMgrRef& fontMgr) const
|
||||
{
|
||||
if (m_type != Type::System ||
|
||||
m_typeface != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text::FontStyleSetRef set = fontMgr->matchFamily(m_name);
|
||||
if (set && set->typeface(0))
|
||||
m_typeface = set->typeface(0);
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
||||
namespace base {
|
||||
|
||||
template<> app::FontInfo convert_to(const std::string& from)
|
||||
{
|
||||
std::vector<std::string> parts;
|
||||
base::split_string(from, parts, ",");
|
||||
|
||||
app::FontInfo::Type type = app::FontInfo::Type::Unknown;
|
||||
std::string name;
|
||||
float size = 0.0f;
|
||||
bool antialias = false;
|
||||
|
||||
if (!parts.empty()) {
|
||||
if (parts[0].compare(0, 5, "file=") == 0) {
|
||||
type = app::FontInfo::Type::File;
|
||||
name = parts[0].substr(5);
|
||||
}
|
||||
else if (parts[0].compare(0, 7, "system=") == 0) {
|
||||
type = app::FontInfo::Type::System;
|
||||
name = parts[0].substr(7);
|
||||
}
|
||||
else {
|
||||
type = app::FontInfo::Type::Name;
|
||||
name = parts[0];
|
||||
}
|
||||
for (int i=1; i<parts.size(); ++i) {
|
||||
if (parts[i] == "antialias")
|
||||
antialias = true;
|
||||
else if (parts[i].compare(0, 5, "size=") == 0) {
|
||||
size = std::strtof(parts[i].substr(5).c_str(), nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return app::FontInfo(type, name, size, antialias);
|
||||
}
|
||||
|
||||
template<> std::string convert_to(const app::FontInfo& from)
|
||||
{
|
||||
std::string result;
|
||||
switch (from.type()) {
|
||||
case app::FontInfo::Type::Unknown:
|
||||
// Do nothing
|
||||
break;
|
||||
case app::FontInfo::Type::Name:
|
||||
result = from.name();
|
||||
break;
|
||||
case app::FontInfo::Type::File:
|
||||
result = "file=" + from.name();
|
||||
break;
|
||||
case app::FontInfo::Type::System:
|
||||
result = "system=" + from.name();
|
||||
break;
|
||||
}
|
||||
if (!result.empty()) {
|
||||
if (from.size() > 0.0f)
|
||||
result += fmt::format(",size={}", from.size());
|
||||
if (from.antialias())
|
||||
result += ",antialias";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace base
|
86
src/app/font_info.h
Normal file
86
src/app/font_info.h
Normal file
@ -0,0 +1,86 @@
|
||||
// Aseprite
|
||||
// Copyright (c) 2024 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifndef APP_UI_FONT_INFO_H_INCLUDED
|
||||
#define APP_UI_FONT_INFO_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "base/convert_to.h"
|
||||
#include "text/fwd.h"
|
||||
#include "text/typeface.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
namespace app {
|
||||
|
||||
// TODO should we merge this with skin::FontData?
|
||||
class FontInfo {
|
||||
public:
|
||||
enum class Type {
|
||||
Unknown,
|
||||
Name,
|
||||
File,
|
||||
System,
|
||||
};
|
||||
|
||||
static constexpr const float kDefaultSize = 0.0f;
|
||||
|
||||
FontInfo(Type type = Type::Unknown,
|
||||
const std::string& name = {},
|
||||
float size = kDefaultSize,
|
||||
bool antialias = false,
|
||||
const text::TypefaceRef& typeface = nullptr);
|
||||
|
||||
FontInfo(const FontInfo& other,
|
||||
float size,
|
||||
bool antialias);
|
||||
|
||||
bool isValid() const { return m_type != Type::Unknown; }
|
||||
bool useDefaultSize() const { return m_size == kDefaultSize; }
|
||||
|
||||
Type type() const { return m_type; }
|
||||
|
||||
// Depending on the font type this field indicates a "integrated
|
||||
// font name" (e.g. "Aseprite" or any font from <font>
|
||||
// definitions/themes), or a TTF filename, or a system font name.
|
||||
const std::string& name() const { return m_name; }
|
||||
|
||||
// Visible label for this font in the UI.
|
||||
std::string title() const;
|
||||
std::string thumbnailId() const;
|
||||
|
||||
float size() const { return m_size; }
|
||||
bool antialias() const { return m_antialias; }
|
||||
|
||||
void findTypeface(const text::FontMgrRef& fontMgr) const;
|
||||
text::TypefaceRef typeface() const { return m_typeface; }
|
||||
|
||||
bool operator==(const FontInfo& other) const {
|
||||
return (m_type == other.m_type &&
|
||||
m_name == other.m_name &&
|
||||
std::fabs(m_size-other.m_size) < 0.001f &&
|
||||
m_antialias == other.m_antialias);
|
||||
}
|
||||
|
||||
private:
|
||||
Type m_type = Type::Unknown;
|
||||
std::string m_name;
|
||||
float m_size = kDefaultSize;
|
||||
bool m_antialias = false;
|
||||
mutable text::TypefaceRef m_typeface;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
namespace base {
|
||||
|
||||
template<> app::FontInfo convert_to(const std::string& from);
|
||||
template<> std::string convert_to(const app::FontInfo& from);
|
||||
|
||||
} // namespace base
|
||||
|
||||
#endif
|
56
src/app/font_info_tests.cpp
Normal file
56
src/app/font_info_tests.cpp
Normal file
@ -0,0 +1,56 @@
|
||||
// Aseprite
|
||||
// Copyright (c) 2024 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#include "tests/app_test.h"
|
||||
|
||||
#include "app/font_info.h"
|
||||
|
||||
using namespace app;
|
||||
using namespace std::literals;
|
||||
|
||||
namespace app {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const FontInfo& info) {
|
||||
return os << base::convert_to<std::string>(info);
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
||||
TEST(FontInfo, ByName)
|
||||
{
|
||||
FontInfo a(FontInfo::Type::Name, "Aseprite");
|
||||
FontInfo b(FontInfo::Type::Name, "Aseprite", 24);
|
||||
FontInfo c(FontInfo::Type::Name, "Arial Unicode");
|
||||
EXPECT_EQ("Aseprite", base::convert_to<std::string>(a));
|
||||
EXPECT_EQ("Aseprite,size=24", base::convert_to<std::string>(b));
|
||||
EXPECT_EQ("Arial Unicode", base::convert_to<std::string>(c));
|
||||
|
||||
EXPECT_EQ(a, base::convert_to<FontInfo>("Aseprite"s));
|
||||
EXPECT_EQ(b, base::convert_to<FontInfo>("Aseprite,size=24"s));
|
||||
EXPECT_EQ(c, base::convert_to<FontInfo>("Arial Unicode"s));
|
||||
}
|
||||
|
||||
TEST(FontInfo, ByFile)
|
||||
{
|
||||
FontInfo a(FontInfo::Type::File, "C:/Windows/fonts/Arial.ttf", 12);
|
||||
FontInfo b(FontInfo::Type::File, "/usr/share/fonts/truetype/noto/NotoSansMath-Regular.ttf");
|
||||
EXPECT_EQ("file=C:/Windows/fonts/Arial.ttf,size=12", base::convert_to<std::string>(a));
|
||||
EXPECT_EQ("file=/usr/share/fonts/truetype/noto/NotoSansMath-Regular.ttf", base::convert_to<std::string>(b));
|
||||
|
||||
EXPECT_EQ(a, base::convert_to<FontInfo>("file=C:/Windows/fonts/Arial.ttf,size=12"s));
|
||||
EXPECT_EQ(b, base::convert_to<FontInfo>("file=/usr/share/fonts/truetype/noto/NotoSansMath-Regular.ttf"s));
|
||||
}
|
||||
|
||||
TEST(FontInfo, BySystem)
|
||||
{
|
||||
FontInfo a(FontInfo::Type::System, "FreeMono");
|
||||
FontInfo b(FontInfo::Type::System, "DejaVu Serif", 12, true);
|
||||
EXPECT_EQ("system=FreeMono", base::convert_to<std::string>(a));
|
||||
EXPECT_EQ("system=DejaVu Serif,size=12,antialias", base::convert_to<std::string>(b));
|
||||
|
||||
EXPECT_EQ(a, base::convert_to<FontInfo>("system=FreeMono"s));
|
||||
EXPECT_EQ(b, base::convert_to<FontInfo>("system=DejaVu Serif,size=12,antialias"s));
|
||||
}
|
188
src/app/ui/font_entry.cpp
Normal file
188
src/app/ui/font_entry.cpp
Normal file
@ -0,0 +1,188 @@
|
||||
// Aseprite
|
||||
// Copyright (c) 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/font_entry.h"
|
||||
|
||||
#include "app/console.h"
|
||||
#include "app/ui/font_popup.h"
|
||||
#include "fmt/format.h"
|
||||
#include "ui/display.h"
|
||||
#include "ui/manager.h"
|
||||
#include "ui/message.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace app {
|
||||
|
||||
using namespace ui;
|
||||
|
||||
FontEntry::FontFace::FontFace()
|
||||
{
|
||||
}
|
||||
|
||||
bool FontEntry::FontFace::onProcessMessage(Message* msg)
|
||||
{
|
||||
switch (msg->type()) {
|
||||
|
||||
// If we press the mouse button in the FontFace widget, and drag
|
||||
// the mouse (without releasing the mouse button) to the popup, we
|
||||
// send the mouse message to the popup.
|
||||
case kMouseMoveMessage:
|
||||
if (hasCapture() && m_popup) {
|
||||
MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
|
||||
const gfx::Point screenPos =
|
||||
mouseMsg->display()->nativeWindow()->pointToScreen(mouseMsg->position());
|
||||
Widget* pick = manager()->pickFromScreenPos(screenPos);
|
||||
Widget* target = m_popup->getListBox();
|
||||
|
||||
if (pick && (pick == target ||
|
||||
pick->hasAncestor(target))) {
|
||||
releaseMouse();
|
||||
|
||||
MouseMessage mouseMsg2(
|
||||
kMouseDownMessage,
|
||||
*mouseMsg,
|
||||
mouseMsg->positionForDisplay(pick->display()));
|
||||
mouseMsg2.setRecipient(pick);
|
||||
mouseMsg2.setDisplay(pick->display());
|
||||
pick->sendMessage(&mouseMsg2);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case kMouseDownMessage:
|
||||
case kFocusEnterMessage:
|
||||
if (!m_popup) {
|
||||
try {
|
||||
const FontInfo info = static_cast<FontEntry*>(parent())->info();
|
||||
|
||||
m_popup.reset(new FontPopup(info));
|
||||
m_popup->ChangeFont.connect([this](const FontInfo& fontInfo){
|
||||
FontChange(fontInfo);
|
||||
});
|
||||
|
||||
// If we press ESC in the popup we focus this FontFace field.
|
||||
m_popup->EscKey.connect([this](){
|
||||
requestFocus();
|
||||
});
|
||||
}
|
||||
catch (const std::exception& ex) {
|
||||
Console::showException(ex);
|
||||
}
|
||||
}
|
||||
if (!m_popup->isVisible()) {
|
||||
m_popup->showPopup(display(), bounds());
|
||||
requestFocus();
|
||||
}
|
||||
break;
|
||||
|
||||
case kFocusLeaveMessage: {
|
||||
// If we lost focus by a widget that is not part of the popup,
|
||||
// we close the popup.
|
||||
auto* newFocus = static_cast<FocusMessage*>(msg)->newFocus();
|
||||
if (m_popup &&
|
||||
newFocus &&
|
||||
newFocus->window() != m_popup.get()) {
|
||||
m_popup->closeWindow(nullptr);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case kKeyDownMessage:
|
||||
// If the popup is visible and we press the Down arrow key, we
|
||||
// start navigating the popup list.
|
||||
if (hasFocus() &&
|
||||
m_popup &&
|
||||
m_popup->isVisible()) {
|
||||
const auto* keymsg = static_cast<const KeyMessage*>(msg);
|
||||
switch (keymsg->scancode()) {
|
||||
case kKeyDown:
|
||||
m_popup->focusListBox();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
return SearchEntry::onProcessMessage(msg);
|
||||
}
|
||||
|
||||
void FontEntry::FontFace::onChange()
|
||||
{
|
||||
SearchEntry::onChange();
|
||||
|
||||
m_popup->setSearchText(text());
|
||||
}
|
||||
|
||||
FontEntry::FontSize::FontSize()
|
||||
{
|
||||
setEditable(true);
|
||||
for (int i : { 8, 9, 10, 11, 12, 14, 16, 18, 22, 24, 26, 28, 36, 48, 72 })
|
||||
addItem(fmt::format("{}", i));
|
||||
}
|
||||
|
||||
void FontEntry::FontSize::onEntryChange()
|
||||
{
|
||||
ComboBox::onEntryChange();
|
||||
Change();
|
||||
}
|
||||
|
||||
FontEntry::FontEntry()
|
||||
: m_antialias("Antialias")
|
||||
{
|
||||
m_face.setExpansive(true);
|
||||
m_size.setExpansive(false);
|
||||
m_antialias.setExpansive(false);
|
||||
addChild(&m_face);
|
||||
addChild(&m_size);
|
||||
addChild(&m_antialias);
|
||||
|
||||
m_face.FontChange.connect([this](const FontInfo& newTypeName) {
|
||||
setInfo(FontInfo(newTypeName,
|
||||
m_info.size(),
|
||||
m_info.antialias()),
|
||||
From::Face);
|
||||
invalidate();
|
||||
});
|
||||
|
||||
m_size.Change.connect([this](){
|
||||
const float newSize = std::strtof(m_size.getValue().c_str(), nullptr);
|
||||
setInfo(FontInfo(m_info,
|
||||
newSize,
|
||||
m_info.antialias()),
|
||||
From::Size);
|
||||
});
|
||||
|
||||
m_antialias.Click.connect([this](){
|
||||
setInfo(FontInfo(m_info,
|
||||
m_info.size(),
|
||||
m_antialias.isSelected()),
|
||||
From::Antialias);
|
||||
});
|
||||
}
|
||||
|
||||
void FontEntry::setInfo(const FontInfo& info,
|
||||
const From fromField)
|
||||
{
|
||||
m_info = info;
|
||||
|
||||
m_face.setText(info.title());
|
||||
|
||||
if (fromField != From::Size)
|
||||
m_size.setValue(fmt::format("{}", info.size()));
|
||||
|
||||
if (fromField != From::Antialias)
|
||||
m_antialias.setSelected(info.antialias());
|
||||
|
||||
FontChange(m_info);
|
||||
}
|
||||
|
||||
} // namespace app
|
66
src/app/ui/font_entry.h
Normal file
66
src/app/ui/font_entry.h
Normal file
@ -0,0 +1,66 @@
|
||||
// Aseprite
|
||||
// Copyright (c) 2024 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifndef APP_UI_FONT_ENTRY_H_INCLUDED
|
||||
#define APP_UI_FONT_ENTRY_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "app/font_info.h"
|
||||
#include "app/ui/search_entry.h"
|
||||
#include "ui/box.h"
|
||||
#include "ui/button.h"
|
||||
#include "ui/combobox.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace app {
|
||||
class FontPopup;
|
||||
|
||||
class FontEntry : public ui::HBox {
|
||||
public:
|
||||
enum class From {
|
||||
User,
|
||||
Face,
|
||||
Size,
|
||||
Antialias,
|
||||
};
|
||||
|
||||
FontEntry();
|
||||
|
||||
FontInfo info() { return m_info; }
|
||||
void setInfo(const FontInfo& info, From from = From::User);
|
||||
|
||||
obs::signal<void(const FontInfo&)> FontChange;
|
||||
|
||||
private:
|
||||
|
||||
class FontFace : public SearchEntry {
|
||||
public:
|
||||
FontFace();
|
||||
obs::signal<void(const FontInfo&)> FontChange;
|
||||
protected:
|
||||
bool onProcessMessage(ui::Message* msg) override;
|
||||
void onChange() override;
|
||||
private:
|
||||
std::unique_ptr<FontPopup> m_popup;
|
||||
};
|
||||
|
||||
class FontSize : public ui::ComboBox {
|
||||
public:
|
||||
FontSize();
|
||||
protected:
|
||||
void onEntryChange() override;
|
||||
};
|
||||
|
||||
FontInfo m_info;
|
||||
FontFace m_face;
|
||||
FontSize m_size;
|
||||
ui::CheckBox m_antialias;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif
|
@ -11,28 +11,29 @@
|
||||
|
||||
#include "app/ui/font_popup.h"
|
||||
|
||||
#include "app/commands/cmd_set_palette.h"
|
||||
#include "app/commands/commands.h"
|
||||
#include "app/console.h"
|
||||
#include "app/file_selector.h"
|
||||
#include "app/font_info.h"
|
||||
#include "app/font_path.h"
|
||||
#include "app/i18n/strings.h"
|
||||
#include "app/match_words.h"
|
||||
#include "app/ui/search_entry.h"
|
||||
#include "app/ui/separator_in_view.h"
|
||||
#include "app/ui/skin/font_data.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
#include "app/ui_context.h"
|
||||
#include "app/util/conversion_to_surface.h"
|
||||
#include "app/util/freetype_utils.h"
|
||||
#include "app/util/render_text.h"
|
||||
#include "base/fs.h"
|
||||
#include "base/string.h"
|
||||
#include "doc/image.h"
|
||||
#include "doc/image_ref.h"
|
||||
#include "os/surface.h"
|
||||
#include "os/system.h"
|
||||
#include "text/text.h"
|
||||
#include "ui/box.h"
|
||||
#include "ui/button.h"
|
||||
#include "ui/fit_bounds.h"
|
||||
#include "ui/graphics.h"
|
||||
#include "ui/listitem.h"
|
||||
#include "ui/message.h"
|
||||
#include "ui/paint_event.h"
|
||||
#include "ui/size_hint_event.h"
|
||||
#include "ui/theme.h"
|
||||
@ -53,144 +54,270 @@ namespace app {
|
||||
|
||||
using namespace ui;
|
||||
|
||||
static std::map<std::string, doc::ImageRef> g_thumbnails;
|
||||
static std::map<std::string, os::SurfaceRef> g_thumbnails;
|
||||
|
||||
class FontItem : public ListItem {
|
||||
public:
|
||||
struct ByName { };
|
||||
|
||||
FontItem(const std::string& name, ByName)
|
||||
: ListItem(name)
|
||||
, m_fontInfo(FontInfo::Type::Name, name,
|
||||
FontInfo::kDefaultSize, true) {
|
||||
getCachedThumbnail();
|
||||
}
|
||||
|
||||
FontItem(const std::string& fn)
|
||||
: ListItem(base::get_file_title(fn))
|
||||
, m_image(g_thumbnails[fn])
|
||||
, m_filename(fn) {
|
||||
, m_fontInfo(FontInfo::Type::File, fn,
|
||||
FontInfo::kDefaultSize, true) {
|
||||
getCachedThumbnail();
|
||||
}
|
||||
|
||||
const std::string& filename() const {
|
||||
return m_filename;
|
||||
FontItem(const std::string& name,
|
||||
const text::TypefaceRef& typeface)
|
||||
: ListItem(name)
|
||||
, m_fontInfo(FontInfo::Type::System, name,
|
||||
FontInfo::kDefaultSize, true, typeface) {
|
||||
getCachedThumbnail();
|
||||
}
|
||||
|
||||
FontInfo fontInfo() const {
|
||||
return m_fontInfo;
|
||||
}
|
||||
|
||||
obs::signal<void()> ThumbnailGenerated;
|
||||
|
||||
private:
|
||||
void getCachedThumbnail() {
|
||||
m_thumbnail = g_thumbnails[m_fontInfo.thumbnailId()];
|
||||
}
|
||||
|
||||
void onPaint(PaintEvent& ev) override {
|
||||
ListItem::onPaint(ev);
|
||||
|
||||
if (m_image) {
|
||||
generateThumbnail();
|
||||
|
||||
if (m_thumbnail) {
|
||||
Graphics* g = ev.graphics();
|
||||
os::SurfaceRef sur = os::System::instance()
|
||||
->makeRgbaSurface(m_image->width(),
|
||||
m_image->height());
|
||||
|
||||
convert_image_to_surface(
|
||||
m_image.get(), nullptr, sur.get(),
|
||||
0, 0, 0, 0, m_image->width(), m_image->height());
|
||||
|
||||
g->drawRgbaSurface(sur.get(), textWidth()+4, 0);
|
||||
g->drawRgbaSurface(m_thumbnail.get(), textWidth()+4, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void onSizeHint(SizeHintEvent& ev) override {
|
||||
ListItem::onSizeHint(ev);
|
||||
if (m_image) {
|
||||
if (m_thumbnail) {
|
||||
gfx::Size sz = ev.sizeHint();
|
||||
ev.setSizeHint(
|
||||
sz.w + 4 + m_image->width(),
|
||||
std::max(sz.h, m_image->height()));
|
||||
sz.w + 4 + m_thumbnail->width(),
|
||||
std::max(sz.h, m_thumbnail->height()));
|
||||
}
|
||||
}
|
||||
|
||||
void onSelect(bool selected) override {
|
||||
if (!selected || m_image)
|
||||
void generateThumbnail() {
|
||||
if (m_thumbnail)
|
||||
return;
|
||||
|
||||
ListBox* listbox = static_cast<ListBox*>(parent());
|
||||
if (!listbox)
|
||||
return;
|
||||
|
||||
auto theme = app::skin::SkinTheme::get(this);
|
||||
gfx::Color color = theme->colors.text();
|
||||
const auto* theme = app::skin::SkinTheme::get(this);
|
||||
|
||||
try {
|
||||
m_image.reset(
|
||||
render_text(
|
||||
m_filename, 16,
|
||||
"ABCDEabcde", // TODO custom text
|
||||
doc::rgba(gfx::getr(color),
|
||||
gfx::getg(color),
|
||||
gfx::getb(color),
|
||||
gfx::geta(color)),
|
||||
true)); // antialias
|
||||
const text::FontMgrRef fontMgr = theme->fontMgr();
|
||||
const gfx::Color color = theme->colors.text();
|
||||
doc::ImageRef image =
|
||||
render_text(fontMgr, m_fontInfo, text(), color);
|
||||
if (!image)
|
||||
return;
|
||||
|
||||
View* view = View::getView(listbox);
|
||||
view->updateView();
|
||||
listbox->makeChildVisible(this);
|
||||
// Convert the doc::Image into a os::Surface
|
||||
m_thumbnail = os::System::instance()
|
||||
->makeRgbaSurface(image->width(),
|
||||
image->height());
|
||||
convert_image_to_surface(
|
||||
image.get(), nullptr, m_thumbnail.get(),
|
||||
0, 0, 0, 0, image->width(), image->height());
|
||||
|
||||
// Save the thumbnail for future FontPopups
|
||||
g_thumbnails[m_filename] = m_image;
|
||||
g_thumbnails[m_fontInfo.thumbnailId()] = m_thumbnail;
|
||||
|
||||
ThumbnailGenerated();
|
||||
}
|
||||
catch (const std::exception&) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
void onSelect(bool selected) override {
|
||||
if (!selected || m_thumbnail)
|
||||
return;
|
||||
|
||||
ListBox* listbox = static_cast<ListBox*>(parent());
|
||||
if (!listbox)
|
||||
return;
|
||||
|
||||
generateThumbnail();
|
||||
listbox->makeChildVisible(this);
|
||||
}
|
||||
|
||||
private:
|
||||
doc::ImageRef m_image;
|
||||
std::string m_filename;
|
||||
os::SurfaceRef m_thumbnail;
|
||||
FontInfo m_fontInfo;
|
||||
};
|
||||
|
||||
FontPopup::FontPopup()
|
||||
: PopupWindow(Strings::font_popup_title(),
|
||||
bool FontPopup::FontListBox::onProcessMessage(ui::Message* msg)
|
||||
{
|
||||
const bool result = ui::ListBox::onProcessMessage(msg);
|
||||
|
||||
// When we release the mouse button we close the popup, i.e. like
|
||||
// selecting an item from a combo box.
|
||||
if (msg->type() == ui::kMouseUpMessage) {
|
||||
if (auto* win = this->window()) {
|
||||
win->closeWindow(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
FontPopup::FontPopup(const FontInfo& fontInfo)
|
||||
: PopupWindow(std::string(),
|
||||
ClickBehavior::CloseOnClickInOtherWindow,
|
||||
EnterBehavior::DoNothingOnEnter)
|
||||
, m_popup(new gen::FontPopup())
|
||||
, m_timer(100)
|
||||
{
|
||||
setAutoRemap(false);
|
||||
setBorder(gfx::Border(4*guiscale()));
|
||||
|
||||
addChild(m_popup);
|
||||
|
||||
m_popup->search()->Change.connect([this]{ onSearchChange(); });
|
||||
m_timer.Tick.connect([this]{ onTickRelayout(); });
|
||||
m_popup->loadFont()->Click.connect([this]{ onLoadFont(); });
|
||||
m_listBox.setFocusMagnet(true);
|
||||
m_listBox.Change.connect([this]{ onChangeFont(); });
|
||||
m_listBox.DoubleClickItem.connect([this]{ onLoadFont(); });
|
||||
m_listBox.Change.connect([this]{
|
||||
if (m_listBox.hasFocus() ||
|
||||
m_listBox.hasCapture()) {
|
||||
onFontChange();
|
||||
}
|
||||
});
|
||||
m_listBox.DoubleClickItem.connect([this]{
|
||||
onFontChange();
|
||||
});
|
||||
|
||||
m_popup->view()->attachToView(&m_listBox);
|
||||
|
||||
base::paths fontDirs;
|
||||
get_font_dirs(fontDirs);
|
||||
|
||||
// Create a list of fullpaths to every font found in all font
|
||||
// directories (fontDirs)
|
||||
base::paths files;
|
||||
for (const auto& fontDir : fontDirs) {
|
||||
for (const auto& file : base::list_files(fontDir)) {
|
||||
std::string fullpath = base::join_path(fontDir, file);
|
||||
if (base::is_file(fullpath))
|
||||
files.push_back(fullpath);
|
||||
// Default fonts
|
||||
bool firstThemeFont = true;
|
||||
for (auto kv : skin::SkinTheme::get(this)->getWellKnownFonts()) {
|
||||
if (!kv.second->filename().empty()) {
|
||||
if (firstThemeFont) {
|
||||
m_listBox.addChild(new SeparatorInView(Strings::font_popup_theme_fonts()));
|
||||
firstThemeFont = false;
|
||||
}
|
||||
m_listBox.addChild(new FontItem(kv.first, FontItem::ByName()));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort all files by "file title"
|
||||
std::sort(
|
||||
files.begin(), files.end(),
|
||||
[](const std::string& a, const std::string& b){
|
||||
return base::utf8_icmp(base::get_file_title(a), base::get_file_title(b)) < 0;
|
||||
});
|
||||
|
||||
// Create one FontItem for each font
|
||||
for (auto& file : files) {
|
||||
std::string ext = base::string_to_lower(base::get_file_extension(file));
|
||||
if (ext == "ttf" || ext == "ttc" ||
|
||||
ext == "otf" || ext == "dfont")
|
||||
m_listBox.addChild(new FontItem(file));
|
||||
m_listBox.addChild(new SeparatorInView(Strings::font_popup_system_fonts()));
|
||||
bool empty = true;
|
||||
|
||||
// Get system fonts from laf-text module
|
||||
const text::FontMgrRef fontMgr = theme()->fontMgr();
|
||||
const int n = fontMgr->countFamilies();
|
||||
if (n > 0) {
|
||||
for (int i=0; i<n; ++i) {
|
||||
std::string name = fontMgr->familyName(i);
|
||||
text::FontStyleSetRef set = fontMgr->familyStyleSet(i);
|
||||
if (set && set->count() > 0) {
|
||||
auto* item = new FontItem(name, set->typeface(0));
|
||||
item->ThumbnailGenerated.connect([this]{ onThumbnailGenerated(); });
|
||||
m_listBox.addChild(item);
|
||||
empty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get fonts listing .ttf files TODO we should be able to remove
|
||||
// this code in the future (probably after DirectWrite API is always
|
||||
// available).
|
||||
else {
|
||||
base::paths fontDirs;
|
||||
get_font_dirs(fontDirs);
|
||||
|
||||
// Create a list of fullpaths to every font found in all font
|
||||
// directories (fontDirs)
|
||||
base::paths files;
|
||||
for (const auto& fontDir : fontDirs) {
|
||||
for (const auto& file : base::list_files(fontDir)) {
|
||||
std::string fullpath = base::join_path(fontDir, file);
|
||||
if (base::is_file(fullpath))
|
||||
files.push_back(fullpath);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort all files by "file title"
|
||||
std::sort(
|
||||
files.begin(), files.end(),
|
||||
[](const std::string& a, const std::string& b){
|
||||
return base::utf8_icmp(base::get_file_title(a), base::get_file_title(b)) < 0;
|
||||
});
|
||||
|
||||
for (auto& file : files) {
|
||||
std::string ext = base::string_to_lower(base::get_file_extension(file));
|
||||
if (ext == "ttf" || ext == "ttc" ||
|
||||
ext == "otf" || ext == "dfont") {
|
||||
m_listBox.addChild(new FontItem(file));
|
||||
empty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_listBox.children().empty())
|
||||
if (empty)
|
||||
m_listBox.addChild(new ListItem(Strings::font_popup_empty_fonts()));
|
||||
|
||||
for (auto* child : m_listBox.children()) {
|
||||
if (auto* childItem = dynamic_cast<FontItem*>(child)) {
|
||||
if (childItem->fontInfo().title() == childItem->text()) {
|
||||
m_listBox.selectChild(childItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FontPopup::~FontPopup()
|
||||
{
|
||||
m_timer.stop();
|
||||
}
|
||||
|
||||
void FontPopup::focusListBox()
|
||||
{
|
||||
m_listBox.requestFocus();
|
||||
}
|
||||
|
||||
void FontPopup::setSearchText(const std::string& searchText)
|
||||
{
|
||||
FontItem* firstItem = nullptr;
|
||||
|
||||
const MatchWords match(searchText);
|
||||
for (auto* child : m_listBox.children()) {
|
||||
auto* childItem = dynamic_cast<FontItem*>(child);
|
||||
if (!childItem)
|
||||
continue;
|
||||
|
||||
const bool visible = match(childItem->text());
|
||||
if (visible && !firstItem)
|
||||
firstItem = childItem;
|
||||
childItem->setVisible(visible);
|
||||
}
|
||||
|
||||
m_listBox.selectChild(firstItem);
|
||||
layout();
|
||||
}
|
||||
|
||||
void FontPopup::showPopup(Display* display,
|
||||
const gfx::Rect& buttonBounds)
|
||||
{
|
||||
m_popup->loadFont()->setEnabled(false);
|
||||
m_listBox.selectChild(NULL);
|
||||
m_listBox.selectChild(nullptr);
|
||||
|
||||
ui::fit_bounds(display, this,
|
||||
gfx::Rect(buttonBounds.x, buttonBounds.y2(), 32, 32),
|
||||
@ -201,45 +328,74 @@ void FontPopup::showPopup(Display* display,
|
||||
bounds.h = workarea.h / 2;
|
||||
});
|
||||
|
||||
// Setup the hot-region
|
||||
setHotRegion(gfx::Region(gfx::Rect(boundsOnScreen()).enlarge(32*guiscale()*display->scale())));
|
||||
|
||||
openWindow();
|
||||
}
|
||||
|
||||
void FontPopup::onSearchChange()
|
||||
FontInfo FontPopup::selectedFont()
|
||||
{
|
||||
std::string searchText = m_popup->search()->text();
|
||||
Widget* firstItem = nullptr;
|
||||
|
||||
MatchWords match(searchText);
|
||||
for (auto child : m_listBox.children()) {
|
||||
bool visible = match(child->text());
|
||||
if (visible && !firstItem)
|
||||
firstItem = child;
|
||||
child->setVisible(visible);
|
||||
}
|
||||
|
||||
m_listBox.selectChild(firstItem);
|
||||
layout();
|
||||
const FontItem* child = dynamic_cast<FontItem*>(m_listBox.getSelectedChild());
|
||||
if (child)
|
||||
return child->fontInfo();
|
||||
return FontInfo();
|
||||
}
|
||||
|
||||
void FontPopup::onChangeFont()
|
||||
void FontPopup::onFontChange()
|
||||
{
|
||||
m_popup->loadFont()->setEnabled(true);
|
||||
const FontInfo fontInfo = selectedFont();
|
||||
if (fontInfo.isValid())
|
||||
ChangeFont(fontInfo);
|
||||
}
|
||||
|
||||
void FontPopup::onLoadFont()
|
||||
{
|
||||
FontItem* child = dynamic_cast<FontItem*>(m_listBox.getSelectedChild());
|
||||
if (!child)
|
||||
std::string currentFile;
|
||||
const FontInfo fontInfo = selectedFont();
|
||||
if (fontInfo.isValid() && fontInfo.type() == FontInfo::Type::File)
|
||||
currentFile = fontInfo.name();
|
||||
|
||||
base::paths exts = { "ttf", "ttc", "otf", "dfont" };
|
||||
base::paths face;
|
||||
if (!show_file_selector(
|
||||
Strings::font_popup_select_truetype_fonts(),
|
||||
currentFile, exts,
|
||||
FileSelectorType::Open, face))
|
||||
return;
|
||||
|
||||
std::string filename = child->filename();
|
||||
if (base::is_file(filename))
|
||||
Load(filename); // Fire Load signal
|
||||
ASSERT(!face.empty());
|
||||
ChangeFont(FontInfo(FontInfo::Type::File,
|
||||
face.front()));
|
||||
}
|
||||
|
||||
closeWindow(nullptr);
|
||||
void FontPopup::onThumbnailGenerated()
|
||||
{
|
||||
m_timer.start();
|
||||
}
|
||||
|
||||
void FontPopup::onTickRelayout()
|
||||
{
|
||||
m_popup->view()->updateView();
|
||||
m_timer.stop();
|
||||
}
|
||||
|
||||
bool FontPopup::onProcessMessage(ui::Message* msg)
|
||||
{
|
||||
switch (msg->type()) {
|
||||
|
||||
case kKeyDownMessage: {
|
||||
const auto* keymsg = static_cast<const KeyMessage*>(msg);
|
||||
|
||||
// Pressing Esc or Enter will just close the popup.
|
||||
if (keymsg->scancode() == kKeyEsc ||
|
||||
keymsg->scancode() == kKeyEnter ||
|
||||
keymsg->scancode() == kKeyEnterPad) {
|
||||
EscKey();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
return ui::PopupWindow::onProcessMessage(msg);
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2021 Igara Studio S.A.
|
||||
// Copyright (C) 2021-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -11,6 +11,7 @@
|
||||
|
||||
#include "ui/listbox.h"
|
||||
#include "ui/popup_window.h"
|
||||
#include "ui/timer.h"
|
||||
|
||||
namespace ui {
|
||||
class Button;
|
||||
@ -23,23 +24,43 @@ namespace app {
|
||||
class FontPopup;
|
||||
}
|
||||
|
||||
class FontInfo;
|
||||
|
||||
class FontPopup : public ui::PopupWindow {
|
||||
public:
|
||||
FontPopup();
|
||||
class FontListBox : public ui::ListBox {
|
||||
protected:
|
||||
bool onProcessMessage(ui::Message* msg) override;
|
||||
};
|
||||
|
||||
FontPopup(const FontInfo& fontInfo);
|
||||
~FontPopup();
|
||||
|
||||
void focusListBox();
|
||||
void setSearchText(const std::string& searchText);
|
||||
|
||||
void showPopup(ui::Display* display,
|
||||
const gfx::Rect& buttonBounds);
|
||||
|
||||
obs::signal<void(const std::string&)> Load;
|
||||
FontListBox* getListBox() { return &m_listBox; }
|
||||
|
||||
obs::signal<void(const FontInfo&)> ChangeFont;
|
||||
obs::signal<void()> EscKey;
|
||||
|
||||
protected:
|
||||
void onSearchChange();
|
||||
void onChangeFont();
|
||||
FontInfo currentFontInfo();
|
||||
void onFontChange();
|
||||
void onLoadFont();
|
||||
void onThumbnailGenerated();
|
||||
void onTickRelayout();
|
||||
bool onProcessMessage(ui::Message* msg) override;
|
||||
|
||||
private:
|
||||
FontInfo selectedFont();
|
||||
|
||||
gen::FontPopup* m_popup;
|
||||
ui::ListBox m_listBox;
|
||||
FontListBox m_listBox;
|
||||
ui::Timer m_timer;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2024 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2015 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -16,12 +17,13 @@ namespace app {
|
||||
public:
|
||||
SearchEntry();
|
||||
|
||||
private:
|
||||
protected:
|
||||
bool onProcessMessage(ui::Message* msg) override;
|
||||
void onPaint(ui::PaintEvent& ev) override;
|
||||
void onSizeHint(ui::SizeHintEvent& ev) override;
|
||||
gfx::Rect onGetEntryTextBounds() const override;
|
||||
|
||||
private:
|
||||
gfx::Rect getCloseIconBounds() const;
|
||||
};
|
||||
|
||||
|
@ -18,6 +18,7 @@
|
||||
namespace app {
|
||||
namespace skin {
|
||||
|
||||
// TODO should we merge this with FontInfo?
|
||||
class FontData {
|
||||
public:
|
||||
FontData(text::FontType type);
|
||||
@ -32,6 +33,8 @@ namespace skin {
|
||||
text::FontRef getFont(text::FontMgrRef& fontMgr, int size, int uiscale);
|
||||
text::FontRef getFont(text::FontMgrRef& fontMgr, int size);
|
||||
|
||||
const std::string& filename() const { return m_filename; }
|
||||
|
||||
private:
|
||||
text::FontType m_type;
|
||||
std::string m_filename;
|
||||
|
@ -1678,6 +1678,15 @@ void SkinTheme::drawEntryCaret(ui::Graphics* g, Entry* widget, int x, int y)
|
||||
g->drawVLine(color, u, y+textHeight/2-caretSize.h/2, caretSize.h);
|
||||
}
|
||||
|
||||
text::FontRef SkinTheme::getFontByName(const std::string& name,
|
||||
const int size)
|
||||
{
|
||||
auto it = m_fonts.find(name);
|
||||
if (it == m_fonts.end())
|
||||
return nullptr;
|
||||
return it->second->getFont(m_fontMgr, size);
|
||||
}
|
||||
|
||||
SkinPartPtr SkinTheme::getToolPart(const char* toolId) const
|
||||
{
|
||||
return getPartById(std::string("tool_") + toolId);
|
||||
|
@ -34,6 +34,7 @@ namespace app {
|
||||
namespace skin {
|
||||
|
||||
class FontData;
|
||||
using FontDataMap = std::map<std::string, FontData*>;
|
||||
|
||||
class ThemeFont {
|
||||
public:
|
||||
@ -155,6 +156,9 @@ namespace app {
|
||||
|
||||
void drawEntryCaret(ui::Graphics* g, ui::Entry* widget, int x, int y);
|
||||
|
||||
const FontDataMap& getWellKnownFonts() const { return m_fonts; }
|
||||
text::FontRef getFontByName(const std::string& name, int size);
|
||||
|
||||
protected:
|
||||
void onRegenerateTheme() override;
|
||||
|
||||
@ -195,7 +199,7 @@ namespace app {
|
||||
std::map<std::string, ui::Cursor*> m_cursors;
|
||||
std::array<ui::Cursor*, ui::kCursorTypes> m_standardCursors;
|
||||
std::map<std::string, ui::Style*> m_styles;
|
||||
std::map<std::string, FontData*> m_fonts;
|
||||
FontDataMap m_fonts;
|
||||
std::map<std::string, ThemeFont> m_themeFonts;
|
||||
// Stores the unscaled font version of the Font pointer used as a key.
|
||||
std::map<text::Font*, text::FontRef> m_unscaledFonts;
|
||||
|
@ -1,107 +0,0 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2022 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// 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/util/freetype_utils.h"
|
||||
|
||||
#include "doc/blend_funcs.h"
|
||||
#include "doc/blend_internals.h"
|
||||
#include "doc/color.h"
|
||||
#include "doc/image.h"
|
||||
#include "doc/primitives.h"
|
||||
#include "ft/algorithm.h"
|
||||
#include "ft/face.h"
|
||||
#include "ft/hb_shaper.h"
|
||||
#include "ft/lib.h"
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace app {
|
||||
|
||||
doc::Image* render_text(const std::string& fontfile, int fontsize,
|
||||
const std::string& text,
|
||||
doc::color_t color,
|
||||
bool antialias)
|
||||
{
|
||||
std::unique_ptr<doc::Image> image(nullptr);
|
||||
ft::Lib ft;
|
||||
|
||||
ft::Face face(ft.open(fontfile));
|
||||
if (face.isValid()) {
|
||||
// Set font size
|
||||
face.setSize(fontsize);
|
||||
face.setAntialias(antialias);
|
||||
|
||||
// Calculate text size
|
||||
gfx::Rect bounds = ft::calc_text_bounds(face, text);
|
||||
|
||||
// Render the image and copy it to the clipboard
|
||||
if (!bounds.isEmpty()) {
|
||||
image.reset(doc::Image::create(doc::IMAGE_RGB, bounds.w, bounds.h));
|
||||
doc::clear_image(image.get(), 0);
|
||||
|
||||
ft::ForEachGlyph<ft::Face> feg(face, text);
|
||||
while (feg.next()) {
|
||||
auto glyph = feg.glyph();
|
||||
if (!glyph)
|
||||
continue;
|
||||
|
||||
int t, yimg = - bounds.y + int(glyph->y);
|
||||
|
||||
for (int v=0; v<int(glyph->bitmap->rows); ++v, ++yimg) {
|
||||
const uint8_t* p = glyph->bitmap->buffer + v*glyph->bitmap->pitch;
|
||||
int ximg = - bounds.x + int(glyph->x);
|
||||
int bit = 0;
|
||||
|
||||
for (int u=0; u<int(glyph->bitmap->width); ++u, ++ximg) {
|
||||
int alpha;
|
||||
|
||||
if (antialias) {
|
||||
alpha = *(p++);
|
||||
}
|
||||
else {
|
||||
alpha = ((*p) & (1 << (7 - (bit++))) ? 255: 0);
|
||||
if (bit == 8) {
|
||||
bit = 0;
|
||||
++p;
|
||||
}
|
||||
}
|
||||
|
||||
int output_alpha = MUL_UN8(doc::rgba_geta(color), alpha, t);
|
||||
if (output_alpha) {
|
||||
doc::color_t output_color =
|
||||
doc::rgba(doc::rgba_getr(color),
|
||||
doc::rgba_getg(color),
|
||||
doc::rgba_getb(color),
|
||||
output_alpha);
|
||||
|
||||
doc::put_pixel(
|
||||
image.get(), ximg, yimg,
|
||||
doc::rgba_blender_normal(
|
||||
doc::get_pixel(image.get(), ximg, yimg),
|
||||
output_color));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw std::runtime_error("There is no text");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw std::runtime_error("Error loading font face");
|
||||
}
|
||||
|
||||
return (image ? image.release(): nullptr);
|
||||
}
|
||||
|
||||
} // namespace app
|
@ -1,28 +0,0 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2001-2015 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifndef APP_UTIL_FREETYPE_UTILS_H_INCLUDED
|
||||
#define APP_UTIL_FREETYPE_UTILS_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "doc/color.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace doc {
|
||||
class Image;
|
||||
}
|
||||
|
||||
namespace app {
|
||||
|
||||
doc::Image* render_text(const std::string& fontfile, int fontsize,
|
||||
const std::string& text,
|
||||
doc::color_t color,
|
||||
bool antialias);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif
|
105
src/app/util/render_text.cpp
Normal file
105
src/app/util/render_text.cpp
Normal file
@ -0,0 +1,105 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2022-2024 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// 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/util/render_text.h"
|
||||
|
||||
#include "app/font_info.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
#include "base/fs.h"
|
||||
#include "doc/blend_funcs.h"
|
||||
#include "doc/blend_internals.h"
|
||||
#include "doc/color.h"
|
||||
#include "doc/image.h"
|
||||
#include "doc/primitives.h"
|
||||
#include "text/draw_text.h"
|
||||
#include "text/font_metrics.h"
|
||||
|
||||
#ifdef LAF_SKIA
|
||||
#include "app/util/shader_helpers.h"
|
||||
#include "os/skia/skia_surface.h"
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace app {
|
||||
|
||||
doc::ImageRef render_text(
|
||||
const text::FontMgrRef& fontMgr,
|
||||
const FontInfo& fontInfo,
|
||||
const std::string& text,
|
||||
gfx::Color color)
|
||||
{
|
||||
doc::ImageRef image;
|
||||
|
||||
auto* theme = skin::SkinTheme::instance();
|
||||
ASSERT(theme);
|
||||
if (!theme)
|
||||
return nullptr;
|
||||
|
||||
text::FontRef font;
|
||||
|
||||
if (fontInfo.type() == FontInfo::Type::System) {
|
||||
// Just in case the typeface is not present in the FontInfo
|
||||
fontInfo.findTypeface(fontMgr);
|
||||
|
||||
const text::FontMgrRef fontMgr = theme->fontMgr();
|
||||
font = fontMgr->makeFont(fontInfo.typeface());
|
||||
if (!fontInfo.useDefaultSize())
|
||||
font->setSize(fontInfo.size());
|
||||
}
|
||||
else {
|
||||
const int size = (fontInfo.useDefaultSize() ? 18: fontInfo.size());
|
||||
font = theme->getFontByName(fontInfo.name(), size);
|
||||
|
||||
if (!font && fontInfo.type() == FontInfo::Type::File) {
|
||||
font = fontMgr->loadTrueTypeFont(fontInfo.name().c_str(), size);
|
||||
}
|
||||
}
|
||||
|
||||
if (!font)
|
||||
return nullptr;
|
||||
|
||||
font->setAntialias(fontInfo.antialias());
|
||||
|
||||
os::Paint paint;
|
||||
paint.style(os::Paint::StrokeAndFill);
|
||||
paint.color(color);
|
||||
|
||||
gfx::RectF bounds;
|
||||
bounds.w = font->measureText(text, &bounds, &paint);
|
||||
|
||||
text::FontMetrics metrics;
|
||||
bounds.w += 1;
|
||||
bounds.h = font->metrics(&metrics);
|
||||
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));
|
||||
|
||||
#ifdef LAF_SKIA
|
||||
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_with_shaper(
|
||||
surface.get(), fontMgr, font, text, gfx::PointF(0, 0), &paint);
|
||||
}
|
||||
else {
|
||||
text::draw_text(
|
||||
surface.get(), fontMgr, font, text,
|
||||
color, gfx::ColorNone, 0, 0, nullptr);
|
||||
}
|
||||
#endif // LAF_SKIA
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
} // namespace app
|
31
src/app/util/render_text.h
Normal file
31
src/app/util/render_text.h
Normal file
@ -0,0 +1,31 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2024 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2015 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifndef APP_UTIL_RENDER_TEXT_H_INCLUDED
|
||||
#define APP_UTIL_RENDER_TEXT_H_INCLUDED
|
||||
#pragma once
|
||||
|
||||
#include "doc/image_ref.h"
|
||||
#include "gfx/color.h"
|
||||
#include "text/font_mgr.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace app {
|
||||
|
||||
class Color;
|
||||
class FontInfo;
|
||||
|
||||
doc::ImageRef render_text(
|
||||
const text::FontMgrRef& fontMgr,
|
||||
const FontInfo& fontInfo,
|
||||
const std::string& text,
|
||||
gfx::Color color);
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif
|
@ -8,19 +8,24 @@
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#if SK_ENABLE_SKSL
|
||||
|
||||
#include "app/util/shader_helpers.h"
|
||||
|
||||
#include "base/exception.h"
|
||||
#include "doc/image.h"
|
||||
#include "fmt/format.h"
|
||||
|
||||
#include "include/effects/SkRuntimeEffect.h"
|
||||
#include "src/core/SkRuntimeEffectPriv.h"
|
||||
#if LAF_SKIA
|
||||
|
||||
#include "include/core/SkSurface.h"
|
||||
#if SK_ENABLE_SKSL
|
||||
#include "include/effects/SkRuntimeEffect.h"
|
||||
#include "src/core/SkRuntimeEffectPriv.h"
|
||||
#endif
|
||||
|
||||
namespace app {
|
||||
|
||||
#if SK_ENABLE_SKSL
|
||||
|
||||
sk_sp<SkRuntimeEffect> make_shader(const char* code)
|
||||
{
|
||||
SkRuntimeEffect::Options options;
|
||||
@ -40,6 +45,8 @@ sk_sp<SkRuntimeEffect> make_shader(const char* code)
|
||||
return result.effect;
|
||||
}
|
||||
|
||||
#endif // SK_ENABLE_SKSL
|
||||
|
||||
SkImageInfo get_skimageinfo_for_docimage(const doc::Image* img)
|
||||
{
|
||||
switch (img->colorMode()) {
|
||||
@ -96,6 +103,14 @@ std::unique_ptr<SkCanvas> make_skcanvas_for_docimage(const doc::Image* img)
|
||||
img->rowBytes());
|
||||
}
|
||||
|
||||
sk_sp<SkSurface> wrap_docimage_in_sksurface(const doc::Image* img)
|
||||
{
|
||||
return SkSurfaces::WrapPixels(
|
||||
get_skimageinfo_for_docimage(img),
|
||||
(void*)img->getPixelAddress(0, 0),
|
||||
img->rowBytes());
|
||||
}
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // SK_ENABLE_SKSL
|
||||
#endif // LAF_SKIA
|
||||
|
@ -93,6 +93,7 @@ sk_sp<SkRuntimeEffect> make_shader(const char* code);
|
||||
SkImageInfo get_skimageinfo_for_docimage(const doc::Image* img);
|
||||
sk_sp<SkImage> make_skimage_for_docimage(const doc::Image* img);
|
||||
std::unique_ptr<SkCanvas> make_skcanvas_for_docimage(const doc::Image* img);
|
||||
sk_sp<SkSurface> wrap_docimage_in_sksurface(const doc::Image* img);
|
||||
|
||||
} // namespace app
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
#include "app/ui/color_button.h"
|
||||
#include "app/ui/drop_down_button.h"
|
||||
#include "app/ui/expr_entry.h"
|
||||
#include "app/ui/font_entry.h"
|
||||
#include "app/ui/icon_button.h"
|
||||
#include "app/ui/mini_help_button.h"
|
||||
#include "app/ui/search_entry.h"
|
||||
@ -544,6 +545,10 @@ Widget* WidgetLoader::convertXmlElementToWidget(const XMLElement* elem, Widget*
|
||||
if (!widget)
|
||||
widget = new SearchEntry;
|
||||
}
|
||||
else if (elem_name == "font") {
|
||||
if (!widget)
|
||||
widget = new FontEntry;
|
||||
}
|
||||
|
||||
// Was the widget created?
|
||||
if (widget) {
|
||||
|
@ -115,6 +115,9 @@ static Item convert_to_item(XMLElement* elem)
|
||||
if (name == "expr")
|
||||
return item.typeIncl("app::ExprEntry",
|
||||
"app/ui/expr_entry.h");
|
||||
if (name == "font")
|
||||
return item.typeIncl("app::FontEntry",
|
||||
"app/ui/font_entry.h");
|
||||
if (name == "grid")
|
||||
return item.typeIncl("ui::Grid",
|
||||
"ui/grid.h");
|
||||
|
Loading…
x
Reference in New Issue
Block a user