提交 0b8038fa 编写于 作者: J Jason Simmons

Support loading of custom fonts

The FLX will contain a font manifest JSON file that maps font family names
to custom font assets.  Flutter will provide a FontSelector that loads
fonts on demand and caches typeface and style data.
上级 ba2590bb
......@@ -21,19 +21,47 @@ void Ignored(bool) {
} // namespace
ZipAssetBundle* ZipAssetBundle::Create(
unzFile ScopedUnzFileTraits::InvalidValue() {
return nullptr;
}
void ScopedUnzFileTraits::Free(unzFile file) {
unzClose(file);
}
scoped_refptr<ZipAssetBundle> ZipAssetService::Create(
InterfaceRequest<AssetBundle> request,
const base::FilePath& zip_path,
scoped_refptr<base::TaskRunner> worker_runner) {
return new ZipAssetBundle(request.Pass(), zip_path, worker_runner.Pass());
scoped_refptr<ZipAssetBundle> zip_asset_bundle(
new ZipAssetBundle(zip_path, worker_runner.Pass()));
// Register as a Mojo service.
new ZipAssetService(request.Pass(), zip_asset_bundle);
return zip_asset_bundle;
}
ZipAssetBundle::ZipAssetBundle(
ZipAssetService::ZipAssetService(
InterfaceRequest<AssetBundle> request,
const scoped_refptr<ZipAssetBundle>& zip_asset_bundle)
: binding_(this, request.Pass()),
zip_asset_bundle_(zip_asset_bundle) {
}
ZipAssetService::~ZipAssetService() {
}
void ZipAssetService::GetAsStream(
const String& asset_name,
const Callback<void(ScopedDataPipeConsumerHandle)>& callback) {
zip_asset_bundle_->GetAsStream(asset_name, callback);
}
ZipAssetBundle::ZipAssetBundle(
const base::FilePath& zip_path,
scoped_refptr<base::TaskRunner> worker_runner)
: binding_(this, request.Pass()),
zip_path_(zip_path),
: zip_path_(zip_path),
worker_runner_(worker_runner.Pass()) {
}
......@@ -72,6 +100,49 @@ void ZipAssetBundle::GetAsStream(
base::Bind(&ZipAssetHandler::Start, base::Unretained(handler)));
}
bool ZipAssetBundle::GetAsBuffer(const std::string& asset_name,
std::vector<uint8_t>* data) {
ScopedUnzFile zip_file(unzOpen2(zip_path_.AsUTF8Unsafe().c_str(), NULL));
if (!zip_file.is_valid()) {
LOG(ERROR) << "Unable to open ZIP file: " << zip_path_.value();
return false;
}
int result = unzLocateFile(zip_file.get(), asset_name.c_str(), 0);
if (result != UNZ_OK) {
LOG(WARNING) << "Requested asset '" << asset_name << "' does not exist.";
return false;
}
unz_file_info file_info;
result = unzGetCurrentFileInfo(zip_file.get(), &file_info, nullptr, 0,
nullptr, 0, nullptr, 0);
if (result != UNZ_OK) {
LOG(WARNING) << "unzGetCurrentFileInfo failed, error=" << result;
return false;
}
result = unzOpenCurrentFile(zip_file.get());
if (result != UNZ_OK) {
LOG(WARNING) << "unzOpenCurrentFile failed, error=" << result;
return false;
}
data->resize(file_info.uncompressed_size);
int total_read = 0;
while (total_read < static_cast<int>(data->size())) {
int bytes_read = unzReadCurrentFile(zip_file.get(),
data->data() + total_read,
data->size() - total_read);
if (bytes_read <= 0) {
return false;
}
total_read += bytes_read;
}
return true;
}
ZipAssetHandler::ZipAssetHandler(
const base::FilePath& zip_path,
const std::string& asset_name,
......@@ -82,33 +153,30 @@ ZipAssetHandler::ZipAssetHandler(
producer_(producer.Pass()),
main_runner_(base::MessageLoop::current()->task_runner()),
worker_runner_(worker_runner.Pass()),
zip_file_(nullptr),
buffer_(nullptr),
buffer_size_(0) {
}
ZipAssetHandler::~ZipAssetHandler() {
if (zip_file_) {
unzClose(zip_file_);
}
}
void ZipAssetHandler::Start() {
zip_file_ = unzOpen2(zip_path_.AsUTF8Unsafe().c_str(), NULL);
if (!zip_file_) {
zip_file_.reset(unzOpen2(zip_path_.AsUTF8Unsafe().c_str(), NULL));
if (!zip_file_.is_valid()) {
LOG(ERROR) << "Unable to open ZIP file: " << zip_path_.value();
delete this;
return;
}
int result = unzLocateFile(zip_file_, asset_name_.c_str(), 0);
int result = unzLocateFile(zip_file_.get(), asset_name_.c_str(), 0);
if (result != UNZ_OK) {
LOG(WARNING) << "Requested asset '" << asset_name_ << "' does not exist.";
delete this;
return;
}
result = unzOpenCurrentFile(zip_file_);
result = unzOpenCurrentFile(zip_file_.get());
if (result != UNZ_OK) {
LOG(WARNING) << "unzOpenCurrentFile failed, error=" << result;
delete this;
......@@ -134,7 +202,7 @@ void ZipAssetHandler::CopyData() {
return;
}
int bytes_read = unzReadCurrentFile(zip_file_, buffer_, buffer_size_);
int bytes_read = unzReadCurrentFile(zip_file_.get(), buffer_, buffer_size_);
mojo_result = EndWriteDataRaw(producer_.get(), std::max(0, bytes_read));
if (bytes_read == 0) {
......
......@@ -3,7 +3,9 @@
// found in the LICENSE file.
#include <map>
#include <vector>
#include "base/scoped_generic.h"
#include "base/task_runner.h"
#include "base/files/file_path.h"
#include "base/memory/weak_ptr.h"
......@@ -18,28 +20,30 @@ namespace asset_bundle {
// An implementation of AssetBundle that serves assets directly out of a
// ZIP archive.
class ZipAssetBundle : public AssetBundle {
public:
static ZipAssetBundle* Create(InterfaceRequest<AssetBundle> request,
const base::FilePath& zip_path,
scoped_refptr<base::TaskRunner> worker_runner);
~ZipAssetBundle() override;
class ZipAssetBundle : public base::RefCountedThreadSafe<ZipAssetBundle> {
friend class ZipAssetService;
public:
// AssetBundle implementation
void GetAsStream(
const String& asset_name,
const Callback<void(ScopedDataPipeConsumerHandle)>& callback) override;
const Callback<void(ScopedDataPipeConsumerHandle)>& callback);
// Serve this asset from another file instead of using the ZIP contents.
void AddOverlayFile(const std::string& asset_name,
const base::FilePath& file_path);
// Read the asset into a buffer.
bool GetAsBuffer(const std::string& asset_name, std::vector<uint8_t>* data);
protected:
friend class base::RefCountedThreadSafe<ZipAssetBundle>;
virtual ~ZipAssetBundle();
private:
ZipAssetBundle(InterfaceRequest<AssetBundle> request,
const base::FilePath& zip_path,
ZipAssetBundle(const base::FilePath& zip_path,
scoped_refptr<base::TaskRunner> worker_runner);
StrongBinding<AssetBundle> binding_;
const base::FilePath zip_path_;
scoped_refptr<base::TaskRunner> worker_runner_;
std::map<String, base::FilePath> overlay_files_;
......@@ -47,6 +51,37 @@ class ZipAssetBundle : public AssetBundle {
DISALLOW_COPY_AND_ASSIGN(ZipAssetBundle);
};
// Wrapper that exposes the ZipAssetBundle as a Mojo service.
class ZipAssetService : public AssetBundle {
public:
// Construct a ZipAssetBundle and register it as a Mojo service.
static scoped_refptr<ZipAssetBundle> Create(
InterfaceRequest<AssetBundle> request,
const base::FilePath& zip_path,
scoped_refptr<base::TaskRunner> worker_runner);
public:
void GetAsStream(
const String& asset_name,
const Callback<void(ScopedDataPipeConsumerHandle)>& callback) override;
~ZipAssetService() override;
private:
ZipAssetService(InterfaceRequest<AssetBundle> request,
const scoped_refptr<ZipAssetBundle>& zip_asset_bundle);
StrongBinding<AssetBundle> binding_;
scoped_refptr<ZipAssetBundle> zip_asset_bundle_;
};
struct ScopedUnzFileTraits {
static unzFile InvalidValue();
static void Free(unzFile file);
};
typedef base::ScopedGeneric<unzFile, ScopedUnzFileTraits> ScopedUnzFile;
// Reads an asset from a ZIP archive and writes it to a Mojo pipe.
class ZipAssetHandler {
friend class ZipAssetBundle;
......@@ -72,7 +107,7 @@ class ZipAssetHandler {
scoped_refptr<base::SingleThreadTaskRunner> main_runner_;
scoped_refptr<base::TaskRunner> worker_runner_;
unzFile zip_file_;
ScopedUnzFile zip_file_;
void* buffer_;
uint32_t buffer_size_;
......
......@@ -5,7 +5,7 @@
import("//sky/engine/core/core.gni")
import("//mojo/dart/embedder/embedder.gni")
visibility = [ "//sky/engine/*" ]
visibility = [ "//sky/engine/*", "//sky/shell/*" ]
source_set("libraries") {
public_deps = [
......
......@@ -36,4 +36,13 @@ void DOMDartState::DidSetIsolate() {
color_class_.Set(this, Dart_GetType(library, ToDart("Color"), 0, 0));
}
void DOMDartState::set_font_selector(PassRefPtr<FontSelector> selector) {
font_selector_ = selector;
}
PassRefPtr<FontSelector> DOMDartState::font_selector() {
return font_selector_;
}
} // namespace blink
......@@ -8,6 +8,7 @@
#include <memory>
#include "dart/runtime/include/dart_api.h"
#include "sky/engine/platform/fonts/FontSelector.h"
#include "sky/engine/tonic/dart_state.h"
#include "sky/engine/wtf/RefPtr.h"
#include "sky/engine/wtf/text/WTFString.h"
......@@ -35,6 +36,9 @@ class DOMDartState : public DartState {
Dart_Handle value_handle() { return value_handle_.value(); }
Dart_Handle color_class() { return color_class_.value(); }
void set_font_selector(PassRefPtr<FontSelector> selector);
PassRefPtr<FontSelector> font_selector();
private:
std::unique_ptr<Window> window_;
std::string url_;
......@@ -45,6 +49,7 @@ class DOMDartState : public DartState {
DartPersistentValue dy_handle_;
DartPersistentValue value_handle_;
DartPersistentValue color_class_;
RefPtr<FontSelector> font_selector_;
};
} // namespace blink
......
......@@ -8,6 +8,7 @@
#include "sky/engine/core/rendering/RenderParagraph.h"
#include "sky/engine/core/rendering/RenderText.h"
#include "sky/engine/core/rendering/style/RenderStyle.h"
#include "sky/engine/core/script/dom_dart_state.h"
#include "sky/engine/platform/text/LocaleToScriptMapping.h"
#include "sky/engine/tonic/dart_args.h"
#include "sky/engine/tonic/dart_binding_macros.h"
......@@ -53,7 +54,7 @@ void createFontForDocument(RenderStyle* style)
fontDescription.setOrientation(fontOrientation);
fontDescription.setNonCJKGlyphOrientation(glyphOrientation);
style->setFontDescription(fontDescription);
style->font().update(nullptr);
style->font().update(DOMDartState::Current()->font_selector());
}
Color getColorFromARGB(int argb) {
......@@ -186,7 +187,7 @@ void ParagraphBuilder::pushStyle(Int32List& encoded, const std::string& fontFami
fontDescription.setWordSpacing(wordSpacing);
style->setFontDescription(fontDescription);
style->font().update(nullptr);
style->font().update(DOMDartState::Current()->font_selector());
}
if (mask & tsLineHeightMask) {
......
......@@ -34,6 +34,8 @@
#include "sky/engine/platform/fonts/SegmentedFontData.h"
#include "sky/engine/wtf/unicode/CharacterNames.h"
#include "base/logging.h"
namespace blink {
FontFallbackList::FontFallbackList()
......
......@@ -24,6 +24,8 @@ source_set("common") {
"ui/animator.h",
"ui/engine.cc",
"ui/engine.h",
"ui/flutter_font_selector.cc",
"ui/flutter_font_selector.h",
"ui/platform_impl.cc",
"ui/platform_impl.h",
"ui_delegate.cc",
......@@ -49,6 +51,7 @@ source_set("common") {
"//skia",
"//sky/engine",
"//sky/engine/bindings",
"//sky/engine/core:core",
"//sky/engine/tonic",
"//sky/engine/wtf",
"//sky/services/editing:interfaces",
......
......@@ -19,6 +19,7 @@
#include "sky/shell/dart/dart_library_provider_files.h"
#include "sky/shell/shell.h"
#include "sky/shell/ui/animator.h"
#include "sky/shell/ui/flutter_font_selector.h"
#include "sky/shell/ui/platform_impl.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkPictureRecorder.h"
......@@ -34,6 +35,7 @@ PlatformImpl* g_platform_impl = nullptr;
} // namespace
using mojo::asset_bundle::ZipAssetBundle;
using mojo::asset_bundle::ZipAssetService;
Engine::Config::Config() {
}
......@@ -186,9 +188,10 @@ void Engine::RunFromPrecompiledSnapshot(const mojo::String& bundle_path) {
TRACE_EVENT0("flutter", "Engine::RunFromPrecompiledSnapshot");
std::string path_str = bundle_path;
ZipAssetBundle::Create(mojo::GetProxy(&root_bundle_),
base::FilePath(path_str),
base::WorkerPool::GetTaskRunner(true));
zip_asset_bundle_ = ZipAssetService::Create(
mojo::GetProxy(&root_bundle_),
base::FilePath(path_str),
base::WorkerPool::GetTaskRunner(true));
sky_view_ = blink::SkyView::Create(this);
sky_view_->CreateView("http://localhost");
......@@ -211,9 +214,10 @@ void Engine::RunFromFile(const mojo::String& main,
void Engine::RunFromBundle(const mojo::String& path) {
TRACE_EVENT0("flutter", "Engine::RunFromBundle");
std::string path_str = path;
ZipAssetBundle::Create(mojo::GetProxy(&root_bundle_),
base::FilePath(path_str),
base::WorkerPool::GetTaskRunner(true));
zip_asset_bundle_ = ZipAssetService::Create(
mojo::GetProxy(&root_bundle_),
base::FilePath(path_str),
base::WorkerPool::GetTaskRunner(true));
root_bundle_->GetAsStream(kSnapshotKey,
base::Bind(&Engine::RunFromSnapshotStream,
......@@ -224,14 +228,14 @@ void Engine::RunFromBundleAndSnapshot(const mojo::String& bundle_path,
const mojo::String& snapshot_path) {
TRACE_EVENT0("flutter", "Engine::RunFromBundleAndSnapshot");
std::string bundle_path_str = bundle_path;
ZipAssetBundle* asset_bundle = ZipAssetBundle::Create(
zip_asset_bundle_ = ZipAssetService::Create(
mojo::GetProxy(&root_bundle_),
base::FilePath(bundle_path_str),
base::WorkerPool::GetTaskRunner(true));
std::string snapshot_path_str = snapshot_path;
asset_bundle->AddOverlayFile(kSnapshotKey,
base::FilePath(snapshot_path_str));
zip_asset_bundle_->AddOverlayFile(kSnapshotKey,
base::FilePath(snapshot_path_str));
root_bundle_->GetAsStream(kSnapshotKey,
base::Bind(&Engine::RunFromSnapshotStream,
......@@ -270,6 +274,9 @@ void Engine::OnAppLifecycleStateChanged(sky::AppLifecycleState state) {
void Engine::DidCreateIsolate(Dart_Isolate isolate) {
blink::MojoServices::Create(isolate, services_.Pass(), root_bundle_.Pass());
if (zip_asset_bundle_)
FlutterFontSelector::install(zip_asset_bundle_);
}
void Engine::StopAnimator() {
......
......@@ -23,6 +23,12 @@
#include "sky/shell/ui_delegate.h"
#include "third_party/skia/include/core/SkPicture.h"
namespace mojo {
namespace asset_bundle {
class ZipAssetBundle;
}
}
namespace sky {
class PlatformImpl;
namespace shell {
......@@ -81,6 +87,8 @@ class Engine : public UIDelegate,
void RunFromSnapshotStream(const std::string& name,
mojo::ScopedDataPipeConsumerHandle snapshot);
void SetupAssetBundle(const mojo::String& bundle_path);
void StopAnimator();
void StartAnimatorIfPossible();
......@@ -97,6 +105,7 @@ class Engine : public UIDelegate,
std::string language_code_;
std::string country_code_;
mojo::Binding<SkyEngine> binding_;
scoped_refptr<mojo::asset_bundle::ZipAssetBundle> zip_asset_bundle_;
// TODO(eseidel): This should move into an AnimatorStateMachine.
bool activity_running_;
......
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/logging.h"
#include "base/values.h"
#include "base/json/json_reader.h"
#include "services/asset_bundle/zip_asset_bundle.h"
#include "sky/engine/core/script/dom_dart_state.h"
#include "sky/engine/platform/fonts/FontData.h"
#include "sky/engine/platform/fonts/FontFaceCreationParams.h"
#include "sky/engine/platform/fonts/SimpleFontData.h"
#include "sky/shell/ui/flutter_font_selector.h"
#include "third_party/skia/include/core/SkStream.h"
#include "third_party/skia/include/core/SkTypeface.h"
#include "third_party/skia/include/ports/SkFontMgr.h"
namespace sky {
namespace shell {
namespace {
const char kFontManifestAssetPath[] = "FontManifest.json";
}
using base::DictionaryValue;
using base::JSONReader;
using base::ListValue;
using base::StringValue;
using base::Value;
using blink::FontCacheKey;
using blink::FontData;
using blink::FontDescription;
using blink::FontFaceCreationParams;
using blink::FontPlatformData;
using blink::SimpleFontData;
using mojo::asset_bundle::ZipAssetBundle;
void FlutterFontSelector::install(
const scoped_refptr<ZipAssetBundle>& zip_asset_bundle) {
RefPtr<FlutterFontSelector> font_selector = adoptRef(
new FlutterFontSelector(zip_asset_bundle));
font_selector->parseFontManifest();
blink::DOMDartState::Current()->set_font_selector(font_selector);
}
FlutterFontSelector::FlutterFontSelector(
const scoped_refptr<ZipAssetBundle>& zip_asset_bundle)
: zip_asset_bundle_(zip_asset_bundle) {
}
void FlutterFontSelector::parseFontManifest() {
std::vector<uint8_t> font_manifest_data;
if (!zip_asset_bundle_->GetAsBuffer(kFontManifestAssetPath,
&font_manifest_data))
return;
base::StringPiece font_manifest_str(
reinterpret_cast<const char*>(font_manifest_data.data()),
font_manifest_data.size());
scoped_ptr<Value> font_manifest_json = JSONReader::Read(font_manifest_str);
if (font_manifest_json == nullptr)
return;
ListValue* family_list;
if (!font_manifest_json->GetAsList(&family_list))
return;
for (auto family : *family_list) {
DictionaryValue* family_dict;
if (!family->GetAsDictionary(&family_dict))
continue;
std::string family_name;
if (!family_dict->GetString("family", &family_name))
continue;
ListValue* font_list;
if (!family_dict->GetList("fonts", &font_list))
continue;
if (font_list->GetSize() != 1) {
LOG(WARNING) << "Font family " << family_name
<< " must have exactly one font";
continue;
}
DictionaryValue* font_dict;
if (!font_list->GetDictionary(0, &font_dict))
continue;
std::string asset_path;
if (!font_dict->GetString("asset", &asset_path))
continue;
font_asset_path_map_.set(AtomicString::fromUTF8(family_name.c_str()),
AtomicString::fromUTF8(asset_path.c_str()));
}
}
PassRefPtr<FontData> FlutterFontSelector::getFontData(
const FontDescription& font_description,
const AtomicString& family_name) {
FontFaceCreationParams creationParams(family_name);
FontCacheKey key = font_description.cacheKey(creationParams);
RefPtr<SimpleFontData> font_data = font_platform_data_cache_.get(key);
if (font_data == nullptr) {
SkTypeface* typeface = getTypefaceAsset(family_name);
if (typeface == nullptr)
return nullptr;
bool synthetic_bold =
(font_description.weight() >= blink::FontWeight600 && !typeface->isBold()) ||
font_description.isSyntheticBold();
bool synthetic_italic =
(font_description.style() && !typeface->isItalic()) ||
font_description.isSyntheticItalic();
FontPlatformData platform_data(
typeface,
family_name.utf8().data(),
font_description.effectiveFontSize(),
synthetic_bold,
synthetic_italic,
font_description.orientation(),
font_description.useSubpixelPositioning());
font_data = SimpleFontData::create(platform_data);
font_platform_data_cache_.set(key, font_data);
}
return font_data;
}
SkTypeface* FlutterFontSelector::getTypefaceAsset(
const AtomicString& family_name) {
auto it = typeface_cache_.find(family_name);
if (it != typeface_cache_.end()) {
const TypefaceAsset* cache_asset = it->value.get();
return cache_asset ? cache_asset->typeface.get() : nullptr;
}
String font_asset_path = font_asset_path_map_.get(family_name);
if (font_asset_path.isEmpty())
return nullptr;
std::unique_ptr<TypefaceAsset> typeface_asset(new TypefaceAsset);
if (!zip_asset_bundle_->GetAsBuffer(font_asset_path.toUTF8(),
&typeface_asset->data)) {
typeface_cache_.set(family_name, nullptr);
return nullptr;
}
SkAutoTUnref<SkFontMgr> font_mgr(SkFontMgr::RefDefault());
SkMemoryStream* typeface_stream = new SkMemoryStream(
typeface_asset->data.data(), typeface_asset->data.size());
typeface_asset->typeface = adoptRef(
font_mgr->createFromStream(typeface_stream));
if (typeface_asset->typeface == nullptr) {
typeface_cache_.set(family_name, nullptr);
return nullptr;
}
SkTypeface* result = typeface_asset->typeface.get();
typeface_cache_.set(family_name, adoptPtr(typeface_asset.release()));
return result;
}
void FlutterFontSelector::willUseFontData(
const FontDescription& font_description,
const AtomicString& family,
UChar32 character) {
}
unsigned FlutterFontSelector::version() const {
return 0;
}
void FlutterFontSelector::fontCacheInvalidated() {
}
} // namespace shell
} // namespace sky
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SKY_SHELL_UI_FLUTTER_FONT_SELECTOR_H_
#define SKY_SHELL_UI_FLUTTER_FONT_SELECTOR_H_
#include <vector>
#include "sky/engine/platform/fonts/FontCacheKey.h"
#include "sky/engine/platform/fonts/FontSelector.h"
#include "sky/engine/platform/fonts/SimpleFontData.h"
namespace mojo {
namespace asset_bundle {
class ZipAssetBundle;
}
}
namespace sky {
namespace shell {
// A FontSelector implementation that resolves custon font names to assets
// loaded from the FLX.
class FlutterFontSelector : public blink::FontSelector {
public:
static void install(
const scoped_refptr<mojo::asset_bundle::ZipAssetBundle>& zip_asset_bundle);
PassRefPtr<blink::FontData> getFontData(
const blink::FontDescription& font_description,
const AtomicString& family_name) override;
void willUseFontData(const blink::FontDescription& font_description,
const AtomicString& family,
UChar32 character) override;
unsigned version() const override;
void fontCacheInvalidated() override;
private:
// A Skia typeface along with a buffer holding the raw typeface asset data.
struct TypefaceAsset {
RefPtr<SkTypeface> typeface;
std::vector<uint8_t> data;
};
FlutterFontSelector(
const scoped_refptr<mojo::asset_bundle::ZipAssetBundle>& zip_asset_bundle);
void parseFontManifest();
SkTypeface* getTypefaceAsset(const AtomicString& family_name);
scoped_refptr<mojo::asset_bundle::ZipAssetBundle> zip_asset_bundle_;
HashMap<AtomicString, String> font_asset_path_map_;
HashMap<AtomicString, OwnPtr<TypefaceAsset>> typeface_cache_;
typedef HashMap<blink::FontCacheKey, RefPtr<blink::SimpleFontData>,
blink::FontCacheKeyHash, blink::FontCacheKeyTraits>
FontPlatformDataCache;
FontPlatformDataCache font_platform_data_cache_;
};
} // namespace shell
} // namespace sky
#endif // SKY_SHELL_UI_FLUTTER_FONT_SELECTOR_H_
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册