mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-01 03:57:17 +01:00
Drawing now takes the style and scale too, like measuring, so the text drawers have no font state left. UIContext keeps its current style and passes it, and fills it in from the theme if nothing has set one yet. PPGe passes its one font. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
483 lines
16 KiB
C++
483 lines
16 KiB
C++
#include "ppsspp_config.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "Common/System/Display.h"
|
|
#include "Common/System/System.h"
|
|
#include "Common/UI/UI.h"
|
|
#include "Common/UI/View.h"
|
|
#include "Common/UI/Context.h"
|
|
#include "Common/Render/DrawBuffer.h"
|
|
#include "Common/Render/Text/draw_text.h"
|
|
#include "Common/Render/ManagedTexture.h"
|
|
#include "Common/Log.h"
|
|
#include "Common/TimeUtil.h"
|
|
#include "Common/LogReporting.h"
|
|
#include "Common/Render/AtlasGen.h"
|
|
|
|
UIContext::UIContext() {
|
|
fontStyle_ = new FontStyle();
|
|
bounds_ = Bounds(0, 0, g_display.dp_xres, g_display.dp_yres);
|
|
}
|
|
|
|
UIContext::~UIContext() {
|
|
sampler_->Release();
|
|
delete fontStyle_;
|
|
delete textDrawer_;
|
|
if (uitexture_)
|
|
uitexture_->Release();
|
|
if (fontTexture_)
|
|
fontTexture_->Release();
|
|
}
|
|
|
|
void UIContext::Init(Draw::DrawContext *thin3d, Draw::Pipeline *uipipe, Draw::Pipeline *uipipenotex, DrawBuffer *uidrawbuffer) {
|
|
using namespace Draw;
|
|
draw_ = thin3d;
|
|
sampler_ = draw_->CreateSamplerState({ TextureFilter::LINEAR, TextureFilter::LINEAR, TextureFilter::LINEAR, 0.0, TextureAddressMode::CLAMP_TO_EDGE, TextureAddressMode::CLAMP_TO_EDGE, TextureAddressMode::CLAMP_TO_EDGE, });
|
|
ui_pipeline_ = uipipe;
|
|
ui_pipeline_notex_ = uipipenotex;
|
|
uidrawbuffer_ = uidrawbuffer;
|
|
textDrawer_ = TextDrawer::Create(thin3d); // May return nullptr if no implementation is available for this platform.
|
|
}
|
|
|
|
void UIContext::BeginFrame() {
|
|
frameStartTime_ = time_now_d();
|
|
// Text drawn before anything set a style uses the theme's.
|
|
if (fontStyle_->sizePts <= 0 && theme) {
|
|
*fontStyle_ = theme->uiFont;
|
|
}
|
|
if (atlasInvalid_ || !uitexture_) {
|
|
if (uitexture_) {
|
|
uitexture_->Release();
|
|
}
|
|
AtlasData data = atlasProvider_(draw_, AtlasChoice::General, 1.0f / g_display.dpi_scale_x, atlasInvalid_);
|
|
uitexture_ = data.texture;
|
|
_dbg_assert_(uitexture_);
|
|
ui_draw2d.SetAtlas(data.atlas);
|
|
atlasInvalid_ = false;
|
|
}
|
|
|
|
if (!fontTexture_) {
|
|
AtlasData data = atlasProvider_(draw_, AtlasChoice::Font, 1.0f / g_display.dpi_scale_x, false);
|
|
fontTexture_ = data.texture;
|
|
_dbg_assert_(fontTexture_);
|
|
ui_draw2d.SetFontAtlas(data.atlas);
|
|
}
|
|
|
|
uidrawbuffer_->SetCurZ(0.0f);
|
|
ActivateTopScissor();
|
|
}
|
|
|
|
void UIContext::InvalidateAtlas() {
|
|
atlasInvalid_ = true; // will cause it to be reloaded on the next frame.
|
|
}
|
|
|
|
void UIContext::SetTintSaturation(float tint, float sat) {
|
|
uidrawbuffer_->SetTintSaturation(tint, sat);
|
|
}
|
|
|
|
void UIContext::Begin() {
|
|
BeginPipeline(ui_pipeline_, sampler_);
|
|
}
|
|
|
|
void UIContext::BeginNoTex() {
|
|
draw_->BindSamplerStates(0, 1, &sampler_);
|
|
ui_draw2d.Begin(ui_pipeline_notex_);
|
|
}
|
|
|
|
void UIContext::BeginPipeline(Draw::Pipeline *pipeline, Draw::SamplerState *samplerState) {
|
|
_assert_(pipeline != nullptr);
|
|
// Also clear out any other textures bound.
|
|
Draw::SamplerState *samplers[3]{ samplerState };
|
|
draw_->BindSamplerStates(0, 3, samplers);
|
|
Draw::Texture *textures[2]{};
|
|
draw_->BindTextures(1, 2, textures);
|
|
RebindTexture();
|
|
ui_draw2d.Begin(pipeline);
|
|
}
|
|
|
|
void UIContext::RebindTexture() const {
|
|
_dbg_assert_(uitexture_);
|
|
if (uitexture_)
|
|
draw_->BindTexture(0, uitexture_);
|
|
}
|
|
|
|
void UIContext::BindFontTexture() const {
|
|
// Fall back to the UI texture, in case they have an old atlas.
|
|
if (fontTexture_)
|
|
draw_->BindTexture(0, fontTexture_);
|
|
else if (uitexture_)
|
|
draw_->BindTexture(0, uitexture_);
|
|
}
|
|
|
|
void UIContext::Flush() {
|
|
if (uidrawbuffer_) {
|
|
uidrawbuffer_->Flush();
|
|
}
|
|
}
|
|
|
|
void UIContext::SetCurZ(float curZ) {
|
|
ui_draw2d.SetCurZ(curZ);
|
|
}
|
|
|
|
// TODO: Support transformed bounds using stencil instead.
|
|
void UIContext::PushScissor(const Bounds &bounds) {
|
|
Flush();
|
|
Bounds clipped = TransformBounds(bounds);
|
|
if (scissorStack_.size())
|
|
clipped.Clip(scissorStack_.back());
|
|
else
|
|
clipped.Clip(bounds_);
|
|
scissorStack_.push_back(clipped);
|
|
ActivateTopScissor();
|
|
}
|
|
|
|
void UIContext::PopScissor() {
|
|
Flush();
|
|
scissorStack_.pop_back();
|
|
ActivateTopScissor();
|
|
}
|
|
|
|
Bounds UIContext::GetScissorBounds() {
|
|
if (!scissorStack_.empty())
|
|
return scissorStack_.back();
|
|
else
|
|
return bounds_;
|
|
}
|
|
|
|
Bounds UIContext::GetLayoutBounds(ViewLayoutMode layoutMode, bool immersiveMode) const {
|
|
float left = System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_LEFT);
|
|
float right = System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_RIGHT);
|
|
float top = System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_TOP);
|
|
float bottom = System_GetPropertyFloat(SYSPROP_DISPLAY_SAFE_INSET_BOTTOM);
|
|
|
|
const bool hasCameraCutout = System_GetPropertyBool(SYSPROP_DISPLAY_HAS_CAMERA_CUTOUT);
|
|
|
|
switch (layoutMode) {
|
|
case ViewLayoutMode::ApplyInsets:
|
|
// Nothing to do
|
|
break;
|
|
case ViewLayoutMode::IgnoreInsets:
|
|
left = 0.0f;
|
|
top = 0.0f;
|
|
right = 0.0f;
|
|
bottom = 0.0f;
|
|
break;
|
|
case ViewLayoutMode::IgnoreBottomInset:
|
|
bottom = 0.0f;
|
|
break;
|
|
case ViewLayoutMode::IgnoreSideInsets:
|
|
left = 0.0f;
|
|
right = 0.0f;
|
|
break;
|
|
}
|
|
|
|
// The side insets are only evened out in landscape. In portrait, a camera cutout is at the top, so a
|
|
// side inset is a real edge (like the sensor bar on the iPhone Duo's cover screen) and stays as is.
|
|
const Bounds screenBounds = GetBounds();
|
|
const bool landscape = screenBounds.w > screenBounds.h;
|
|
|
|
if (immersiveMode) {
|
|
if (!hasCameraCutout) {
|
|
// No cutout, so if the nav bar is hidden, we can use the full width of the screen.
|
|
left = 0.0f;
|
|
right = 0.0f;
|
|
} else if (!landscape) {
|
|
// Keep the insets.
|
|
} else if (left > 0 && right > 0) {
|
|
// Navigation bar is available, so insets leave space for it
|
|
// even if it's hidden.
|
|
int smallestNonZero = std::min(right, left);
|
|
left = smallestNonZero;
|
|
right = smallestNonZero;
|
|
} else {
|
|
int sideWidth = std::max(left, right);
|
|
left = sideWidth;
|
|
right = sideWidth;
|
|
}
|
|
} else {
|
|
if (landscape && (left > 0) != (right > 0)) {
|
|
// Only one side has a cutout (surely the camera, which is usually smaller than the nav bar), so use the larger of the two for both sides.
|
|
int sideWidth = std::max(left, right);
|
|
left = sideWidth;
|
|
right = sideWidth;
|
|
}
|
|
}
|
|
|
|
// Note that we ignore bottom here, to let lists etc. extend to the bottom of the screen.
|
|
// However, we will add the same space to the bottom of scrolled lists, so that you don't have to
|
|
// touch things below the safe inset.
|
|
|
|
// Adjust left edge to compensate for cutouts (notches) if any.
|
|
Bounds bounds = GetBounds();
|
|
|
|
if (useOverrideScreenFrame_) {
|
|
float leftMax = overrideScreenFrame_.x;
|
|
left = std::max(left, leftMax);
|
|
float rightMax = bounds.x2() - overrideScreenFrame_.x2();
|
|
right = std::max(right, rightMax);
|
|
float topMax = overrideScreenFrame_.y;
|
|
top = std::max(top, topMax);
|
|
float bottomMax = bounds.y2() - overrideScreenFrame_.y2();
|
|
bottom = std::max(bottom, bottomMax);
|
|
}
|
|
|
|
bounds.x += left;
|
|
bounds.w -= (left + right);
|
|
bounds.y += top;
|
|
bounds.h -= (top + bottom);
|
|
|
|
return bounds;
|
|
}
|
|
|
|
void UIContext::ActivateTopScissor() {
|
|
Bounds bounds;
|
|
if (scissorStack_.size()) {
|
|
float scale_x = g_display.pixel_in_dps_x;
|
|
float scale_y = g_display.pixel_in_dps_y;
|
|
bounds = scissorStack_.back();
|
|
int x = floorf(scale_x * bounds.x);
|
|
int y = floorf(scale_y * bounds.y);
|
|
int w = std::max(0.0f, ceilf(scale_x * bounds.w));
|
|
int h = std::max(0.0f, ceilf(scale_y * bounds.h));
|
|
if (x < 0 || y < 0 || x + w > g_display.pixel_xres || y + h > g_display.pixel_yres) {
|
|
DEBUG_LOG(Log::G3D, "UI scissor out of bounds: %d,%d-%d,%d / %d,%d", x, y, w, h, g_display.pixel_xres, g_display.pixel_yres);
|
|
if (x < 0) { w += x; x = 0; }
|
|
if (y < 0) { h += y; y = 0; }
|
|
if (x >= g_display.pixel_xres) { x = g_display.pixel_xres - 1; }
|
|
if (y >= g_display.pixel_yres) { y = g_display.pixel_yres - 1; }
|
|
if (x + w > g_display.pixel_xres) { w = std::min(w, g_display.pixel_xres - x); }
|
|
if (y + w > g_display.pixel_yres) { h = std::min(h, g_display.pixel_yres - y); }
|
|
if (w == 0) w = 1;
|
|
if (h == 0) h = 1;
|
|
draw_->SetScissorRect(x, y, w, h);
|
|
} else {
|
|
// Avoid invalid rects
|
|
if (w == 0) w = 1;
|
|
if (h == 0) h = 1;
|
|
draw_->SetScissorRect(x, y, w, h);
|
|
}
|
|
} else {
|
|
// Avoid rounding errors
|
|
draw_->SetScissorRect(0, 0, g_display.pixel_xres, g_display.pixel_yres);
|
|
}
|
|
}
|
|
|
|
void UIContext::SetFontScale(float scaleX, float scaleY) {
|
|
fontScaleX_ = scaleX;
|
|
fontScaleY_ = scaleY;
|
|
}
|
|
|
|
void UIContext::SetFontStyle(const FontStyle &fontStyle) {
|
|
*fontStyle_ = fontStyle;
|
|
}
|
|
|
|
static FontID AtlasFontFromStyle(const FontStyle &style) {
|
|
// For now, we only have one font in the atlas.
|
|
return FontID("UBUNTU24");
|
|
}
|
|
|
|
void UIContext::MeasureText(const FontStyle &style, float scaleX, float scaleY, std::string_view str, float *x, float *y, int align) const {
|
|
_dbg_assert_(str.data() != nullptr);
|
|
if (!textDrawer_ || (align & FLAG_DYNAMIC_ASCII)) {
|
|
float sizeFactor = (float)style.sizePts / 24.0f;
|
|
Draw()->SetFontScale(scaleX * sizeFactor, scaleY * sizeFactor);
|
|
Draw()->MeasureText(AtlasFontFromStyle(style), str, x, y);
|
|
} else {
|
|
textDrawer_->MeasureString(style, scaleX, scaleY, str, x, y);
|
|
}
|
|
}
|
|
|
|
void UIContext::MeasureTextRect(const FontStyle &style, float scaleX, float scaleY, std::string_view str, float maxWidth, float *x, float *y, int align) const {
|
|
_dbg_assert_(str.data() != nullptr);
|
|
if (!textDrawer_ || (align & FLAG_DYNAMIC_ASCII)) {
|
|
float sizeFactor = (float)style.sizePts / 24.0f;
|
|
Draw()->SetFontScale(scaleX * sizeFactor, scaleY * sizeFactor);
|
|
Draw()->MeasureTextRect(AtlasFontFromStyle(style), str, maxWidth, x, y, align);
|
|
} else {
|
|
textDrawer_->MeasureStringRect(style, scaleX, scaleY, str, maxWidth, x, y, align);
|
|
}
|
|
}
|
|
|
|
void UIContext::DrawText(std::string_view str, float x, float y, uint32_t color, int align) {
|
|
_dbg_assert_(str.data() != nullptr);
|
|
if (!textDrawer_ || (align & FLAG_DYNAMIC_ASCII)) {
|
|
// Use the font texture if this font is in that texture instead.
|
|
bool useFontTexture = Draw()->GetFontAtlas()->getFont(AtlasFontFromStyle(*fontStyle_)) != nullptr;
|
|
if (useFontTexture) {
|
|
Flush();
|
|
BindFontTexture();
|
|
}
|
|
float sizeFactor = (float)fontStyle_->sizePts / 24.0f;
|
|
Draw()->SetFontScale(fontScaleX_ * sizeFactor, fontScaleY_ * sizeFactor);
|
|
Draw()->DrawText(AtlasFontFromStyle(*fontStyle_), str, x, y, color, align);
|
|
if (useFontTexture)
|
|
Flush();
|
|
} else {
|
|
textDrawer_->DrawString(*Draw(), *fontStyle_, fontScaleX_, fontScaleY_, str, x, y, color, align);
|
|
}
|
|
RebindTexture();
|
|
}
|
|
|
|
void UIContext::DrawTextShadow(std::string_view str, float x, float y, uint32_t color, int align) {
|
|
uint32_t alpha = (color >> 1) & 0xFF000000;
|
|
DrawText(str, x + 2, y + 2, alpha, align);
|
|
DrawText(str, x, y, color, align);
|
|
}
|
|
|
|
void UIContext::DrawTextRect(std::string_view str, const Bounds &bounds, uint32_t color, int align) {
|
|
if (!textDrawer_ || (align & FLAG_DYNAMIC_ASCII)) {
|
|
// Use the font texture if this font is in that texture instead.
|
|
bool useFontTexture = Draw()->GetFontAtlas()->getFont(AtlasFontFromStyle(*fontStyle_)) != nullptr;
|
|
if (useFontTexture) {
|
|
Flush();
|
|
BindFontTexture();
|
|
}
|
|
float sizeFactor = (float)fontStyle_->sizePts / 24.0f;
|
|
Draw()->SetFontScale(fontScaleX_ * sizeFactor, fontScaleY_ * sizeFactor);
|
|
Draw()->DrawTextRect(AtlasFontFromStyle(*fontStyle_), str, bounds.x, bounds.y, bounds.w, bounds.h, color, align);
|
|
if (useFontTexture)
|
|
Flush();
|
|
} else {
|
|
Bounds rounded = bounds;
|
|
rounded.x = floorf(rounded.x);
|
|
rounded.y = floorf(rounded.y);
|
|
textDrawer_->DrawStringRect(*Draw(), *fontStyle_, fontScaleX_, fontScaleY_, str, rounded, color, align);
|
|
}
|
|
RebindTexture();
|
|
}
|
|
|
|
static constexpr float MIN_TEXT_SCALE = 0.7f;
|
|
|
|
float UIContext::CalculateTextScale(std::string_view str, float availWidth) const {
|
|
float actualWidth, actualHeight;
|
|
MeasureTextRect(theme->uiFont, 1.0f, 1.0f, str, availWidth, &actualWidth, &actualHeight, ALIGN_VCENTER);
|
|
if (actualWidth > availWidth) {
|
|
// TODO: Return feedback that wrapping was needed.
|
|
return std::max(MIN_TEXT_SCALE, availWidth / actualWidth);
|
|
}
|
|
return 1.0f;
|
|
}
|
|
|
|
void UIContext::DrawTextRectSqueeze(std::string_view str, const Bounds &bounds, uint32_t color, int align) {
|
|
if (bounds.w <= 0 || bounds.h <= 0) {
|
|
// Probably mid-layout.
|
|
return;
|
|
}
|
|
float origScaleX = fontScaleX_;
|
|
float origScaleY = fontScaleY_;
|
|
float scale = CalculateTextScale(str, bounds.w / origScaleX);
|
|
SetFontScale(scale * origScaleX, scale * origScaleY);
|
|
Bounds textBounds(bounds.x, bounds.y, bounds.w, bounds.h);
|
|
DrawTextRect(str, textBounds, color, align);
|
|
SetFontScale(origScaleX, origScaleY);
|
|
}
|
|
|
|
void UIContext::DrawTextShadowRect(std::string_view str, const Bounds &bounds, uint32_t color, int align) {
|
|
uint32_t alpha = (color >> 1) & 0xFF000000;
|
|
Bounds shadowBounds(bounds.x+2, bounds.y+2, bounds.w, bounds.h);
|
|
DrawTextRect(str, shadowBounds, alpha, align);
|
|
DrawTextRect(str, bounds, color, align);
|
|
}
|
|
|
|
void UIContext::FillRoundRect(const UI::Drawable &drawable, const Bounds &bounds, float radius) {
|
|
FillRoundRect(drawable, bounds, radius, RoundCorners::All);
|
|
}
|
|
|
|
void UIContext::FillRoundRect(const UI::Drawable &drawable, const Bounds &bounds, float radius, RoundCorners corners) {
|
|
// Only draw if alpha is non-zero.
|
|
if ((drawable.color & 0xFF000000) == 0)
|
|
return;
|
|
switch (drawable.type) {
|
|
case UI::DRAW_SOLID_COLOR:
|
|
uidrawbuffer_->DrawRoundRect(bounds, radius, drawable.color, RoundRectStyle::Filled, corners);
|
|
break;
|
|
default:
|
|
// Not supported
|
|
break;
|
|
}
|
|
}
|
|
|
|
void UIContext::FillRect(const UI::Drawable &drawable, const Bounds &bounds) {
|
|
// Only draw if alpha is non-zero.
|
|
if ((drawable.color & 0xFF000000) == 0)
|
|
return;
|
|
|
|
switch (drawable.type) {
|
|
case UI::DRAW_SOLID_COLOR:
|
|
uidrawbuffer_->DrawImageCenterTexel(theme->whiteImage, bounds.x, bounds.y, bounds.x2(), bounds.y2(), drawable.color);
|
|
break;
|
|
case UI::DRAW_4GRID:
|
|
uidrawbuffer_->DrawImage4Grid(drawable.image, bounds.x, bounds.y, bounds.x2(), bounds.y2(), drawable.color);
|
|
break;
|
|
case UI::DRAW_STRETCH_IMAGE:
|
|
uidrawbuffer_->DrawImageStretch(drawable.image, bounds.x, bounds.y, bounds.x2(), bounds.y2(), drawable.color);
|
|
break;
|
|
case UI::DRAW_NOTHING:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void UIContext::DrawRectDropShadow(const Bounds &bounds, float radius, float alpha, uint32_t color) {
|
|
if (alpha <= 0.0f)
|
|
return;
|
|
|
|
color = colorAlpha(color, alpha);
|
|
|
|
// Bias the shadow downwards a bit.
|
|
Bounds shadowBounds = bounds.Expand(radius, 0.5 * radius, radius, 1.5 * radius);
|
|
Draw()->DrawImage4Grid(theme->dropShadow4Grid, shadowBounds.x, shadowBounds.y, shadowBounds.x2(), shadowBounds.y2(), color, radius * (1.0f / 24.0f) * 2.0f);
|
|
}
|
|
|
|
void UIContext::DrawImageVGradient(ImageID image, uint32_t color1, uint32_t color2, const Bounds &bounds) {
|
|
uidrawbuffer_->DrawImageStretchVGradient(image, bounds.x, bounds.y, bounds.x2(), bounds.y2(), color1, color2);
|
|
}
|
|
|
|
void UIContext::DrawImageRotated(ImageID atlas_image, float x, float y, float scale, float angle, uint32_t color, bool mirror_h) {
|
|
uidrawbuffer_->DrawImageRotated(atlas_image, x, y, scale, angle, color, mirror_h);
|
|
}
|
|
|
|
|
|
void UIContext::PushTransform(const UITransform &transform) {
|
|
Flush();
|
|
|
|
using namespace Lin;
|
|
|
|
Matrix4x4 m = Draw()->GetDrawMatrix();
|
|
const Vec3 &t = transform.translate;
|
|
Vec3 scaledTranslate = Vec3(
|
|
t.x * m.xx + t.y * m.xy + t.z * m.xz + m.xw,
|
|
t.x * m.yx + t.y * m.yy + t.z * m.yz + m.yw,
|
|
t.x * m.zx + t.y * m.zy + t.z * m.zz + m.zw);
|
|
|
|
m.translateAndScale(scaledTranslate, transform.scale);
|
|
Draw()->PushDrawMatrix(m);
|
|
Draw()->PushAlpha(transform.alpha);
|
|
|
|
transformStack_.push_back(transform);
|
|
}
|
|
|
|
void UIContext::PopTransform() {
|
|
Flush();
|
|
|
|
transformStack_.pop_back();
|
|
|
|
Draw()->PopDrawMatrix();
|
|
Draw()->PopAlpha();
|
|
}
|
|
|
|
Bounds UIContext::TransformBounds(const Bounds &bounds) {
|
|
if (!transformStack_.empty()) {
|
|
const UITransform t = transformStack_.back();
|
|
Bounds translated = bounds.Offset(t.translate.x, t.translate.y);
|
|
|
|
// Scale around the center as the origin.
|
|
float scaledX = (translated.x - g_display.dp_xres * 0.5f) * t.scale.x + g_display.dp_xres * 0.5f;
|
|
float scaledY = (translated.y - g_display.dp_yres * 0.5f) * t.scale.y + g_display.dp_yres * 0.5f;
|
|
|
|
return Bounds(scaledX, scaledY, translated.w * t.scale.x, translated.h * t.scale.y);
|
|
}
|
|
|
|
return bounds;
|
|
}
|