CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
Path: blob/master/UI/OnScreenDisplay.cpp
Views: 1401
#include <algorithm>1#include <sstream>23#include "UI/OnScreenDisplay.h"45#include "Common/Data/Color/RGBAUtil.h"6#include "Common/Data/Encoding/Utf8.h"7#include "Common/Render/TextureAtlas.h"8#include "Common/Render/DrawBuffer.h"9#include "Common/Math/math_util.h"10#include "Common/UI/IconCache.h"11#include "UI/RetroAchievementScreens.h"12#include "UI/DebugOverlay.h"13#include "UI/Root.h"1415#include "Common/UI/Context.h"16#include "Common/System/OSD.h"1718#include "Common/TimeUtil.h"19#include "Common/Net/HTTPClient.h"20#include "Core/Config.h"2122static inline const char *DeNull(const char *ptr) {23return ptr ? ptr : "";24}2526extern bool g_TakeScreenshot;2728static const float g_atlasIconSize = 36.0f;29static const float extraTextScale = 0.7f;3031static uint32_t GetNoticeBackgroundColor(NoticeLevel type) {32// Colors from Infima33switch (type) {34case NoticeLevel::ERROR: return 0x3530d5; // danger-darker35case NoticeLevel::WARN: return 0x009ed9; // warning-darker36case NoticeLevel::INFO: return 0x706760; // gray-70037case NoticeLevel::SUCCESS: return 0x008b00; // nice green38default: return 0x606770;39}40}4142static ImageID GetOSDIcon(NoticeLevel level) {43switch (level) {44case NoticeLevel::INFO: return ImageID("I_INFO");45case NoticeLevel::ERROR: return ImageID("I_CROSS");46case NoticeLevel::WARN: return ImageID("I_WARNING");47case NoticeLevel::SUCCESS: return ImageID("I_CHECKMARK");48default: return ImageID::invalid();49}50}5152static NoticeLevel GetNoticeLevel(OSDType type) {53switch (type) {54case OSDType::MESSAGE_INFO:55return NoticeLevel::INFO;56case OSDType::MESSAGE_ERROR:57case OSDType::MESSAGE_ERROR_DUMP:58case OSDType::MESSAGE_CENTERED_ERROR:59return NoticeLevel::ERROR;60case OSDType::MESSAGE_WARNING:61case OSDType::MESSAGE_CENTERED_WARNING:62return NoticeLevel::WARN;63case OSDType::MESSAGE_SUCCESS:64return NoticeLevel::SUCCESS;65default:66return NoticeLevel::SUCCESS;67}68}6970// Align only matters here for the ASCII-only flag.71static void MeasureNotice(const UIContext &dc, NoticeLevel level, const std::string &text, const std::string &details, const std::string &iconName, int align, float *width, float *height, float *height1) {72dc.MeasureText(dc.theme->uiFont, 1.0f, 1.0f, text.c_str(), width, height, align);7374*height1 = *height;7576float width2 = 0.0f, height2 = 0.0f;77if (!details.empty()) {78dc.MeasureText(dc.theme->uiFont, extraTextScale, extraTextScale, details.c_str(), &width2, &height2, align);79*width = std::max(*width, width2);80*height += 5.0f + height2;81}8283float iconW = 0.0f;84float iconH = 0.0f;85if (!iconName.empty() && !startsWith(iconName, "I_")) { // Check for atlas image. Bit hacky, but we choose prefixes for icon IDs anyway in a way that this is safe.86// Normal entry but with a cached icon.87int iconWidth, iconHeight;88if (g_iconCache.GetDimensions(iconName, &iconWidth, &iconHeight)) {89*width += 5.0f + iconWidth;90iconW = iconWidth;91iconH = iconHeight;92}93} else {94ImageID iconID = iconName.empty() ? GetOSDIcon(level) : ImageID(iconName.c_str());95if (iconID.isValid()) {96dc.Draw()->GetAtlas()->measureImage(iconID, &iconW, &iconH);97}98}99100iconW += 5.0f;101102*width += iconW + 12.0f;103*height = std::max(*height, iconH + 5.0f);104}105106// Align only matters here for the ASCII-only flag.107static void MeasureOSDEntry(const UIContext &dc, const OnScreenDisplay::Entry &entry, int align, float *width, float *height, float *height1) {108if (entry.type == OSDType::ACHIEVEMENT_UNLOCKED) {109const rc_client_achievement_t *achievement = rc_client_get_achievement_info(Achievements::GetClient(), entry.numericID);110MeasureAchievement(dc, achievement, AchievementRenderStyle::UNLOCKED, width, height);111*width = 550.0f;112*height1 = *height;113} else {114MeasureNotice(dc, GetNoticeLevel(entry.type), entry.text, entry.text2, entry.iconName, align, width, height, height1);115}116}117118static void RenderNotice(UIContext &dc, Bounds bounds, float height1, NoticeLevel level, const std::string &text, const std::string &details, const std::string &iconName, int align, float alpha) {119UI::Drawable background = UI::Drawable(colorAlpha(GetNoticeBackgroundColor(level), alpha));120121uint32_t foreGround = whiteAlpha(alpha);122123dc.DrawRectDropShadow(bounds, 12.0f, 0.7f * alpha);124dc.FillRect(background, bounds);125126float iconW = 0.0f;127float iconH = 0.0f;128if (!iconName.empty() && !startsWith(iconName, "I_")) {129dc.Flush();130// Normal entry but with a cached icon.131Draw::Texture *texture = g_iconCache.BindIconTexture(&dc, iconName);132if (texture) {133iconW = texture->Width();134iconH = texture->Height();135dc.Draw()->DrawTexRect(Bounds(bounds.x + 2.5f, bounds.y + 2.5f, iconW, iconH), 0.0f, 0.0f, 1.0f, 1.0f, foreGround);136dc.Flush();137dc.RebindTexture();138}139dc.Begin();140} else {141ImageID iconID = iconName.empty() ? GetOSDIcon(level) : ImageID(iconName.c_str());142if (iconID.isValid()) {143// Atlas icon.144dc.Draw()->GetAtlas()->measureImage(iconID, &iconW, &iconH);145if (!iconName.empty()) {146Bounds iconBounds = Bounds(bounds.x + 2.5f, bounds.y + 2.5f, iconW, iconH);147// If it's not a preset OSD icon, give it some background to blend in. The RA icon for example148// easily melts into the orange of warnings otherwise.149dc.FillRect(UI::Drawable(0x50000000), iconBounds.Expand(2.0f));150}151dc.DrawImageVGradient(iconID, foreGround, foreGround, Bounds(bounds.x + 2.5f, bounds.y + 2.5f, iconW, iconH));152}153}154155// Make room156bounds.x += iconW + 5.0f;157bounds.w -= iconW + 5.0f;158159dc.DrawTextShadowRect(text.c_str(), bounds.Inset(0.0f, 1.0f, 0.0f, 0.0f), foreGround, (align & FLAG_DYNAMIC_ASCII));160161if (!details.empty()) {162Bounds bottomTextBounds = bounds.Inset(3.0f, height1 + 5.0f, 3.0f, 3.0f);163UI::Drawable backgroundDark = UI::Drawable(colorAlpha(darkenColor(GetNoticeBackgroundColor(level)), alpha));164dc.FillRect(backgroundDark, bottomTextBounds);165dc.SetFontScale(extraTextScale, extraTextScale);166dc.DrawTextRect(details, bottomTextBounds, foreGround, (align & FLAG_DYNAMIC_ASCII) | ALIGN_LEFT);167}168dc.SetFontScale(1.0f, 1.0f);169}170171static void RenderOSDEntry(UIContext &dc, const OnScreenDisplay::Entry &entry, Bounds bounds, float height1, int align, float alpha) {172if (entry.type == OSDType::ACHIEVEMENT_UNLOCKED) {173const rc_client_achievement_t * achievement = rc_client_get_achievement_info(Achievements::GetClient(), entry.numericID);174if (achievement) {175RenderAchievement(dc, achievement, AchievementRenderStyle::UNLOCKED, bounds, alpha, entry.startTime, time_now_d(), false);176}177return;178} else {179RenderNotice(dc, bounds, height1, GetNoticeLevel(entry.type), entry.text, entry.text2, entry.iconName, align, alpha);180}181}182183static void MeasureOSDProgressBar(const UIContext &dc, const OnScreenDisplay::Entry &bar, float *width, float *height) {184*height = 36;185*width = 450.0f;186}187188static void RenderOSDProgressBar(UIContext &dc, const OnScreenDisplay::Entry &entry, Bounds bounds, int align, float alpha) {189uint32_t foreGround = whiteAlpha(alpha);190191dc.DrawRectDropShadow(bounds, 12.0f, 0.7f * alpha);192193uint32_t backgroundColor = colorAlpha(0x806050, alpha);194uint32_t progressBackgroundColor = colorAlpha(0xa08070, alpha);195196if (entry.maxValue > entry.minValue) {197// Normal progress bar198199UI::Drawable background = UI::Drawable(backgroundColor);200UI::Drawable progressBackground = UI::Drawable(progressBackgroundColor);201202float ratio = (float)(entry.progress - entry.minValue) / (float)entry.maxValue;203204Bounds boundLeft = bounds;205Bounds boundRight = bounds;206207boundLeft.w *= ratio;208boundRight.x += ratio * boundRight.w;209boundRight.w *= (1.0f - ratio);210211dc.FillRect(progressBackground, boundLeft);212dc.FillRect(background, boundRight);213} else {214// Indeterminate spinner215float alpha = cos(time_now_d() * 5.0) * 0.5f + 0.5f;216uint32_t pulse = colorBlend(backgroundColor, progressBackgroundColor, alpha);217UI::Drawable background = UI::Drawable(pulse);218dc.FillRect(background, bounds);219}220221dc.SetFontStyle(dc.theme->uiFont);222dc.SetFontScale(1.0f, 1.0f);223224dc.DrawTextShadowRect(entry.text.c_str(), bounds, colorAlpha(0xFFFFFFFF, alpha), (align & FLAG_DYNAMIC_ASCII) | ALIGN_CENTER);225}226227static void MeasureLeaderboardTracker(UIContext &dc, const std::string &text, float *width, float *height) {228dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text.c_str(), width, height);229*width += 16.0f;230*height += 10.0f;231}232233static void RenderLeaderboardTracker(UIContext &dc, const Bounds &bounds, const std::string &text, float alpha) {234// TODO: Awful color.235uint32_t backgroundColor = colorAlpha(0x806050, alpha);236UI::Drawable background = UI::Drawable(backgroundColor);237dc.DrawRectDropShadow(bounds, 12.0f, 0.7f * alpha);238dc.FillRect(background, bounds);239dc.SetFontStyle(dc.theme->uiFont);240dc.SetFontScale(1.0f, 1.0f);241dc.DrawTextShadowRect(text.c_str(), bounds.Inset(5.0f, 5.0f), colorAlpha(0xFFFFFFFF, alpha), ALIGN_VCENTER | ALIGN_HCENTER);242}243244void OnScreenMessagesView::Draw(UIContext &dc) {245if (!g_Config.bShowOnScreenMessages || g_TakeScreenshot) {246return;247}248249dc.Flush();250251double now = time_now_d();252253const float padding = 5.0f;254255const float fadeinCoef = 1.0f / OnScreenDisplay::FadeinTime();256const float fadeoutCoef = 1.0f / OnScreenDisplay::FadeoutTime();257258float sidebarAlpha = g_OSD.SidebarAlpha();259260struct LayoutEdge {261float height;262float maxWidth;263float alpha;264};265266struct MeasuredEntry {267float w;268float h;269float h1;270float alpha;271int align;272int align2;273AchievementRenderStyle style;274};275276// Grab all the entries. Makes a copy so we can release the lock ASAP.277const std::vector<OnScreenDisplay::Entry> entries = g_OSD.Entries();278279std::vector<MeasuredEntry> measuredEntries;280measuredEntries.resize(entries.size());281282// Indexed by the enum ScreenEdgePosition.283LayoutEdge edges[(size_t)ScreenEdgePosition::VALUE_COUNT]{};284for (size_t i = 0; i < (size_t)ScreenEdgePosition::VALUE_COUNT; i++) {285edges[i].alpha = sidebarAlpha;286}287edges[(size_t)ScreenEdgePosition::TOP_CENTER].alpha = 1.0f;288289ScreenEdgePosition typeEdges[(size_t)OSDType::VALUE_COUNT]{};290// Default to top.291for (int i = 0; i < (size_t)OSDType::VALUE_COUNT; i++) {292typeEdges[i] = ScreenEdgePosition::TOP_CENTER;293}294295typeEdges[(size_t)OSDType::ACHIEVEMENT_CHALLENGE_INDICATOR] = (ScreenEdgePosition)g_Config.iAchievementsChallengePos;296typeEdges[(size_t)OSDType::ACHIEVEMENT_PROGRESS] = (ScreenEdgePosition)g_Config.iAchievementsProgressPos;297typeEdges[(size_t)OSDType::LEADERBOARD_TRACKER] = (ScreenEdgePosition)g_Config.iAchievementsLeaderboardTrackerPos;298typeEdges[(size_t)OSDType::LEADERBOARD_STARTED_FAILED] = (ScreenEdgePosition)g_Config.iAchievementsLeaderboardStartedOrFailedPos;299typeEdges[(size_t)OSDType::LEADERBOARD_SUBMITTED] = (ScreenEdgePosition)g_Config.iAchievementsLeaderboardSubmittedPos;300typeEdges[(size_t)OSDType::ACHIEVEMENT_UNLOCKED] = (ScreenEdgePosition)g_Config.iAchievementsUnlockedPos;301typeEdges[(size_t)OSDType::MESSAGE_CENTERED_WARNING] = ScreenEdgePosition::CENTER;302typeEdges[(size_t)OSDType::MESSAGE_CENTERED_ERROR] = ScreenEdgePosition::CENTER;303304dc.SetFontScale(1.0f, 1.0f);305306// First pass: Measure all the sides.307for (size_t i = 0; i < entries.size(); i++) {308const auto &entry = entries[i];309auto &measuredEntry = measuredEntries[i];310311ScreenEdgePosition pos = typeEdges[(size_t)entry.type];312if (pos == ScreenEdgePosition::VALUE_COUNT || pos == (ScreenEdgePosition)-1) {313// NONE.314continue;315}316317measuredEntry.align = 0;318measuredEntry.align2 = 0;319// If we have newlines, we may be looking at ASCII debug output. But let's verify.320if (entry.text.find('\n') != std::string::npos) {321if (!UTF8StringHasNonASCII(entry.text.c_str()))322measuredEntry.align |= FLAG_DYNAMIC_ASCII;323}324if (entry.text2.find('\n') != std::string::npos) {325if (!UTF8StringHasNonASCII(entry.text2.c_str()))326measuredEntry.align2 |= FLAG_DYNAMIC_ASCII;327}328329switch (entry.type) {330case OSDType::ACHIEVEMENT_PROGRESS:331{332const rc_client_achievement_t *achievement = rc_client_get_achievement_info(Achievements::GetClient(), entry.numericID);333if (!achievement)334continue;335measuredEntry.style = AchievementRenderStyle::PROGRESS_INDICATOR;336MeasureAchievement(dc, achievement, measuredEntry.style, &measuredEntry.w, &measuredEntry.h);337break;338}339case OSDType::ACHIEVEMENT_CHALLENGE_INDICATOR:340{341const rc_client_achievement_t *achievement = rc_client_get_achievement_info(Achievements::GetClient(), entry.numericID);342if (!achievement)343continue;344measuredEntry.style = AchievementRenderStyle::CHALLENGE_INDICATOR;345MeasureAchievement(dc, achievement, measuredEntry.style, &measuredEntry.w, &measuredEntry.h);346break;347}348case OSDType::LEADERBOARD_TRACKER:349{350MeasureLeaderboardTracker(dc, entry.text, &measuredEntry.w, &measuredEntry.h);351break;352}353case OSDType::ACHIEVEMENT_UNLOCKED:354{355const rc_client_achievement_t *achievement = rc_client_get_achievement_info(Achievements::GetClient(), entry.numericID);356if (!achievement)357continue;358measuredEntry.style = AchievementRenderStyle::UNLOCKED;359MeasureAchievement(dc, achievement, AchievementRenderStyle::UNLOCKED, &measuredEntry.w, &measuredEntry.h);360measuredEntry.h1 = measuredEntry.h;361measuredEntry.w = 550.0f;362break;363}364case OSDType::PROGRESS_BAR:365MeasureOSDProgressBar(dc, entry, &measuredEntry.w, &measuredEntry.h);366break;367default:368MeasureOSDEntry(dc, entry, measuredEntry.align, &measuredEntry.w, &measuredEntry.h, &measuredEntry.h1);369break;370}371372float enterAlpha = saturatef((float)(now - entry.startTime) * fadeoutCoef);373float leaveAlpha = saturatef((float)(entry.endTime - now) * fadeoutCoef);374float alpha = std::min(enterAlpha, leaveAlpha);375measuredEntry.alpha = alpha;376377edges[(size_t)pos].height += (measuredEntry.h + 4.0f) * alpha;378edges[(size_t)pos].maxWidth = std::max(edges[(size_t)pos].maxWidth, measuredEntry.w);379}380381std::vector<ClickZone> clickZones;382383// Now, perform layout for all 8 edges.384for (size_t i = 0; i < (size_t)ScreenEdgePosition::VALUE_COUNT; i++) {385if (edges[i].height == 0.0f) {386// Nothing on this side, ignore it entirely.387continue;388}389390// First, compute the start position.391float y = padding;392int horizAdj = 0;393int vertAdj = 0;394switch ((ScreenEdgePosition)i) {395case ScreenEdgePosition::TOP_LEFT: horizAdj = -1; vertAdj = -1; break;396case ScreenEdgePosition::CENTER_LEFT: horizAdj = -1; break;397case ScreenEdgePosition::BOTTOM_LEFT: horizAdj = -1; vertAdj = 1; break;398case ScreenEdgePosition::TOP_RIGHT: horizAdj = 1; vertAdj = -1; break;399case ScreenEdgePosition::CENTER_RIGHT: horizAdj = 1; break;400case ScreenEdgePosition::BOTTOM_RIGHT: horizAdj = 1; vertAdj = 1; break;401case ScreenEdgePosition::TOP_CENTER: vertAdj = -1; break;402case ScreenEdgePosition::BOTTOM_CENTER: vertAdj = 1; break;403case ScreenEdgePosition::CENTER: break;404default: break;405}406407if (vertAdj == 0) {408// Center vertically409y = (bounds_.h - edges[i].height) * 0.5f;410} else if (vertAdj == 1) {411y = (bounds_.h - edges[i].height);412}413414// Then, loop through the entries and those belonging here, get rendered here.415for (size_t j = 0; j < (size_t)entries.size(); j++) {416auto &entry = entries[j];417if (typeEdges[(size_t)entry.type] != (ScreenEdgePosition)i) { // yes, i418continue;419}420auto &measuredEntry = measuredEntries[j];421float alpha = measuredEntry.alpha * edges[i].alpha;422423Bounds b(padding, y, measuredEntry.w, measuredEntry.h);424425if (horizAdj == 0) {426// Centered427b.x = (bounds_.w - b.w) * 0.5f;428} else if (horizAdj == 1) {429// Right-aligned430b.x = bounds_.w - (b.w + padding);431}432433switch (entry.type) {434case OSDType::ACHIEVEMENT_PROGRESS:435case OSDType::ACHIEVEMENT_CHALLENGE_INDICATOR:436{437const rc_client_achievement_t *achievement = rc_client_get_achievement_info(Achievements::GetClient(), entry.numericID);438RenderAchievement(dc, achievement, measuredEntry.style, b, alpha, entry.startTime, now, false);439break;440}441case OSDType::LEADERBOARD_TRACKER:442RenderLeaderboardTracker(dc, b, entry.text, alpha);443break;444case OSDType::PROGRESS_BAR:445RenderOSDProgressBar(dc, entry, b, 0, alpha);446break;447default:448{449// Scale down if height doesn't fit.450float scale = 1.0f;451if (measuredEntry.h > bounds_.h - y) {452// Scale down!453scale = std::max(0.15f, (bounds_.h - y) / measuredEntry.h);454dc.SetFontScale(scale, scale);455b.w *= scale;456b.h *= scale;457}458459float alpha = Clamp((float)(entry.endTime - now) * 4.0f, 0.0f, 1.0f);460RenderOSDEntry(dc, entry, b, measuredEntry.h1, measuredEntry.align, alpha);461462switch (entry.type) {463case OSDType::MESSAGE_INFO:464case OSDType::MESSAGE_SUCCESS:465case OSDType::MESSAGE_WARNING:466case OSDType::MESSAGE_ERROR:467case OSDType::MESSAGE_CENTERED_ERROR:468case OSDType::MESSAGE_CENTERED_WARNING:469case OSDType::MESSAGE_ERROR_DUMP:470case OSDType::MESSAGE_FILE_LINK:471case OSDType::ACHIEVEMENT_UNLOCKED:472// Save the location of the popup, for easy dismissal.473clickZones.push_back(ClickZone{ (int)j, b });474break;475default:476break;477}478break;479}480}481482483y += (measuredEntry.h + 4.0f) * measuredEntry.alpha;484}485}486487std::lock_guard<std::mutex> lock(clickMutex_);488clickZones_ = clickZones;489}490491std::string OnScreenMessagesView::DescribeText() const {492std::stringstream ss;493const auto &entries = g_OSD.Entries();494for (auto iter = entries.begin(); iter != entries.end(); ++iter) {495if (iter != entries.begin()) {496ss << "\n";497}498ss << iter->text;499}500return ss.str();501}502503// Asynchronous!504bool OnScreenMessagesView::Dismiss(float x, float y) {505bool dismissed = false;506std::lock_guard<std::mutex> lock(clickMutex_);507double now = time_now_d();508for (auto &zone : clickZones_) {509if (zone.bounds.Contains(x, y)) {510g_OSD.ClickEntry(zone.index, now);511dismissed = true;512}513}514return dismissed;515}516517bool OSDOverlayScreen::UnsyncTouch(const TouchInput &touch) {518// Don't really need to forward.519// UIScreen::UnsyncTouch(touch);520if ((touch.flags & TOUCH_DOWN) && osmView_) {521return osmView_->Dismiss(touch.x, touch.y);522} else {523return false;524}525}526527void OSDOverlayScreen::CreateViews() {528root_ = new UI::AnchorLayout();529root_->SetTag("OSDOverlayScreen");530osmView_ = root_->Add(new OnScreenMessagesView(new UI::AnchorLayoutParams(0.0f, 0.0f, 0.0f, 0.0f)));531}532533void OSDOverlayScreen::DrawForeground(UIContext &ui) {534DebugOverlay debugOverlay = (DebugOverlay)g_Config.iDebugOverlay;535536// Special case control for now, since it uses the control mapper that's owned by EmuScreen.537if (debugOverlay != DebugOverlay::OFF && debugOverlay != DebugOverlay::CONTROL) {538UIContext *uiContext = screenManager()->getUIContext();539DrawDebugOverlay(uiContext, uiContext->GetLayoutBounds(), debugOverlay);540}541}542543void OSDOverlayScreen::update() {544// Partial version of UIScreen::update() but doesn't do event processing to avoid duplicate event processing.545bool vertical = UseVerticalLayout();546if (vertical != lastVertical_) {547RecreateViews();548lastVertical_ = vertical;549}550551DoRecreateViews();552}553554void NoticeView::GetContentDimensionsBySpec(const UIContext &dc, UI::MeasureSpec horiz, UI::MeasureSpec vert, float &w, float &h) const {555Bounds bounds(0, 0, layoutParams_->width, layoutParams_->height);556if (bounds.w < 0) {557// If there's no size, let's grow as big as we want.558bounds.w = horiz.size;559}560if (bounds.h < 0) {561bounds.h = vert.size;562}563ApplyBoundsBySpec(bounds, horiz, vert);564MeasureNotice(dc, level_, text_, detailsText_, iconName_, 0, &w, &h, &height1_);565// Layout hack! Some weird problems with the layout that I can't figure out right now..566if (squishy_) {567w = 50.0;568}569}570571void NoticeView::Draw(UIContext &dc) {572dc.PushScissor(bounds_);573RenderNotice(dc, bounds_, height1_, level_, text_, detailsText_, iconName_, 0, 1.0f);574dc.PopScissor();575}576577578