UI: settings menu restyle, physical-px text pipeline, menu blur hooks
This commit is contained in:
parent
c807e433a7
commit
6c8e885bce
12 changed files with 773 additions and 215 deletions
|
|
@ -68,4 +68,24 @@ bool ShouldSuppressPassAtPitch(uint32_t surface_pitch);
|
|||
// only suppressed scene passes (the native renderer shadows itself).
|
||||
bool ShouldSuppressExemptDepthOnlyDraws();
|
||||
|
||||
// ---- Guest-output post-processor ------------------------------------------
|
||||
// Host effect over the EMULATED guest-output path (e.g. the settings-menu
|
||||
// backdrop blur): invoked by the command processors at the very end of the
|
||||
// emulated gamma/FXAA refresh, after the guest output image has been fully
|
||||
// written, with the same context contract as the renderer callback (the
|
||||
// image arrives in kGuestOutput state and must be returned to it). When the
|
||||
// native renderer handled the frame this is NOT invoked; the renderer
|
||||
// applies its own effects inline. Only invoked while the request flag is set
|
||||
// (a cheap gate so the common no-effect frame skips the RHI frame setup);
|
||||
// the post-processor clears the flag itself once its effect has fully
|
||||
// decayed.
|
||||
using NativeGuestOutputPostProcessor = void (*)(const NativeGuestOutputRenderContext& context,
|
||||
void* user_data);
|
||||
void SetNativeGuestOutputPostProcessor(NativeGuestOutputPostProcessor post_processor,
|
||||
void* user_data);
|
||||
bool HasNativeGuestOutputPostProcessor();
|
||||
void InvokeNativeGuestOutputPostProcessor(const NativeGuestOutputRenderContext& context);
|
||||
void RequestNativeGuestOutputPostProcess(bool requested);
|
||||
bool IsNativeGuestOutputPostProcessRequested();
|
||||
|
||||
} // namespace rex::graphics
|
||||
|
|
|
|||
|
|
@ -13,5 +13,6 @@ namespace rex::ui {
|
|||
|
||||
const char* GetInterRegularCompressedBase85();
|
||||
const char* GetInterSemiBoldCompressedBase85();
|
||||
const char* GetInterBoldCompressedBase85();
|
||||
|
||||
} // namespace rex::ui
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
|
|||
// default font (PushFont(nullptr, size)).
|
||||
ImFont* ui_font() const { return ui_font_; }
|
||||
ImFont* ui_font_semibold() const { return ui_font_semibold_; }
|
||||
ImFont* ui_font_bold() const { return ui_font_bold_; }
|
||||
// Coverage-thinned variants for DARK text on LIGHT backgrounds (see
|
||||
// SetupFonts); same metrics as the plain fonts.
|
||||
ImFont* ui_font_on_light() const { return ui_font_on_light_; }
|
||||
ImFont* ui_font_semibold_on_light() const { return ui_font_semibold_on_light_; }
|
||||
|
||||
void AddDialog(ImGuiDialog* dialog);
|
||||
void RemoveDialog(ImGuiDialog* dialog);
|
||||
|
|
@ -114,6 +119,9 @@ class ImGuiDrawer : public WindowInputListener, public UIDrawer {
|
|||
// System UI fonts (see ui_font()). Owned by the ImGui font atlas.
|
||||
ImFont* ui_font_ = nullptr;
|
||||
ImFont* ui_font_semibold_ = nullptr;
|
||||
ImFont* ui_font_bold_ = nullptr;
|
||||
ImFont* ui_font_on_light_ = nullptr;
|
||||
ImFont* ui_font_semibold_on_light_ = nullptr;
|
||||
|
||||
// All currently-attached dialogs that get drawn.
|
||||
std::vector<ImGuiDialog*> dialogs_;
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@ class SimpleSettingsDialog final : public ImGuiDialog {
|
|||
float highlight_anim_y_ = -1.0f; // smoothed selection highlight position
|
||||
float rail_anim_y_ = -1.0f;
|
||||
float content_scroll_ = 0.0f;
|
||||
// Swallow the first frame's cursor delta after Show: the pre-open cursor
|
||||
// position (or a cursor-mode warp) otherwise reads as mouse motion and
|
||||
// steals focus from the rail to whatever row it lands on.
|
||||
bool just_shown_ = false;
|
||||
float mouse_x_ = -1.0f; // last mouse position, to detect real motion
|
||||
float mouse_y_ = -1.0f;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2631,6 +2631,31 @@ void D3D12CommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, uint32_t frontbu
|
|||
// presenter so it can submit its own commands for displaying it to the
|
||||
// queue.
|
||||
SubmitBarriers();
|
||||
|
||||
// Host post-processor over the emulated output (settings-menu
|
||||
// backdrop blur): runs after the gamma/FXAA pass has fully written
|
||||
// the image (barriers above put it in the presenter's internal
|
||||
// state). Frames the native renderer handled returned above; it
|
||||
// applies its own effects inline.
|
||||
if (IsNativeGuestOutputPostProcessRequested() && HasNativeGuestOutputPostProcessor()) {
|
||||
NativeGuestOutputRenderContext native_context;
|
||||
native_context.backend = NativeGuestOutputBackend::kD3D12;
|
||||
native_context.guest_output_width = guest_output_width;
|
||||
native_context.guest_output_height = guest_output_height;
|
||||
native_context.display_width = display_width;
|
||||
native_context.display_height = display_height;
|
||||
if (native_rhi_device_ == nullptr) {
|
||||
native_rhi_device_ = CreateNativeRhiDevice(this);
|
||||
}
|
||||
native_context.device = native_rhi_device_;
|
||||
native_context.cmd = NativeRhiBeginFrame(
|
||||
native_rhi_device_, guest_output_resource,
|
||||
ui::d3d12::D3D12Presenter::kGuestOutputFormat,
|
||||
ui::d3d12::D3D12Presenter::kGuestOutputInternalState, guest_output_width,
|
||||
guest_output_height, &native_context.guest_output);
|
||||
InvokeNativeGuestOutputPostProcessor(native_context);
|
||||
}
|
||||
|
||||
EndSubmission(true);
|
||||
return true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ std::atomic<void*> g_renderer_user_data{nullptr};
|
|||
// True while the registered renderer actually replaced the last presented
|
||||
// frame (false when it yields: menus, early-outs, no renderer).
|
||||
std::atomic<bool> g_native_output_active{false};
|
||||
std::atomic<NativeGuestOutputPostProcessor> g_post_processor{nullptr};
|
||||
std::atomic<void*> g_post_processor_user_data{nullptr};
|
||||
std::atomic<bool> g_post_process_requested{false};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
|
@ -79,6 +82,33 @@ bool HasNativeGuestOutputRenderer() {
|
|||
return g_renderer.load(std::memory_order_acquire) != nullptr;
|
||||
}
|
||||
|
||||
void SetNativeGuestOutputPostProcessor(NativeGuestOutputPostProcessor post_processor,
|
||||
void* user_data) {
|
||||
g_post_processor_user_data.store(user_data, std::memory_order_release);
|
||||
g_post_processor.store(post_processor, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool HasNativeGuestOutputPostProcessor() {
|
||||
return g_post_processor.load(std::memory_order_acquire) != nullptr;
|
||||
}
|
||||
|
||||
void InvokeNativeGuestOutputPostProcessor(const NativeGuestOutputRenderContext& context) {
|
||||
NativeGuestOutputPostProcessor post_processor =
|
||||
g_post_processor.load(std::memory_order_acquire);
|
||||
if (post_processor == nullptr) {
|
||||
return;
|
||||
}
|
||||
post_processor(context, g_post_processor_user_data.load(std::memory_order_acquire));
|
||||
}
|
||||
|
||||
void RequestNativeGuestOutputPostProcess(bool requested) {
|
||||
g_post_process_requested.store(requested, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool IsNativeGuestOutputPostProcessRequested() {
|
||||
return g_post_process_requested.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
bool IsNativeGuestOutputActive() {
|
||||
return g_native_output_active.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
#include <rex/logging.h>
|
||||
#include <rex/math.h>
|
||||
#include <rex/platform.h>
|
||||
#include <rex/graphics/gpu_clock_telemetry.h>
|
||||
#include <rex/graphics/native_guest_renderer.h>
|
||||
#include <rex/graphics/util/draw.h>
|
||||
#include <rex/graphics/flags.h>
|
||||
|
|
@ -163,6 +164,12 @@ REXCVAR_DEFINE_BOOL(vulkan_gpu_timestamp_buckets, false, "GPU/Vulkan",
|
|||
"with timestamp queries and report them in the FPS overlay")
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
REXCVAR_DEFINE_INT32(vulkan_gpu_clock_log_interval_frames, 120, "GPU/Vulkan",
|
||||
"How often (in guest frames) to log the GPU clock/P-state telemetry line; "
|
||||
"P-state changes and large clock swings log immediately; 0 disables")
|
||||
.range(0, 36000)
|
||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||
|
||||
// Skate 3 ultrawide cvars. These mirror the Direct3D 12 backend's definitions
|
||||
// so the Hor+ classifier behaves identically on Vulkan; the D3D12 translation
|
||||
// unit is not compiled on this platform, so without these the queries below
|
||||
|
|
@ -202,6 +209,51 @@ const char* ReadbackResolveModeName(ReadbackResolveMode mode) {
|
|||
return "unknown";
|
||||
}
|
||||
|
||||
// Periodic GPU clock/P-state telemetry. All timestamp buckets inflating
|
||||
// together with unchanged interval counts means the same workload is
|
||||
// executing slower - this line attributes that to core/memory clocks the
|
||||
// driver kept parked (the sticky post-map-change slow state and the slow
|
||||
// first seconds of gameplay). Logs on P-state changes and large SM clock
|
||||
// swings immediately, and unconditionally every interval as a heartbeat.
|
||||
void LogGpuClockTelemetry() {
|
||||
const int32_t interval = REXCVAR_GET(vulkan_gpu_clock_log_interval_frames);
|
||||
if (interval <= 0) {
|
||||
return;
|
||||
}
|
||||
constexpr uint32_t kSampleEveryFrames = 15;
|
||||
static uint32_t frames_until_sample = 0;
|
||||
static uint32_t frames_since_log = 0;
|
||||
static uint32_t last_logged_pstate = UINT32_MAX;
|
||||
static uint32_t last_logged_sm_mhz = 0;
|
||||
++frames_since_log;
|
||||
if (frames_until_sample > 1) {
|
||||
--frames_until_sample;
|
||||
return;
|
||||
}
|
||||
frames_until_sample = kSampleEveryFrames;
|
||||
const GpuClockSample sample = SampleGpuClocks();
|
||||
if (!sample.valid) {
|
||||
// Never loaded or failed - stop paying for the attempt.
|
||||
REXCVAR_SET(vulkan_gpu_clock_log_interval_frames, 0);
|
||||
return;
|
||||
}
|
||||
const uint32_t sm_delta =
|
||||
sample.sm_mhz > last_logged_sm_mhz ? sample.sm_mhz - last_logged_sm_mhz
|
||||
: last_logged_sm_mhz - sample.sm_mhz;
|
||||
const bool state_changed = sample.performance_state != last_logged_pstate ||
|
||||
sm_delta * 5 > last_logged_sm_mhz; // >20% swing.
|
||||
if (!state_changed && frames_since_log < uint32_t(interval)) {
|
||||
return;
|
||||
}
|
||||
REXGPU_INFO("Vulkan GPU clocks: sm={}MHz mem={}MHz pstate=P{} util={}% memutil={}% "
|
||||
"throttle={:#x}",
|
||||
sample.sm_mhz, sample.mem_mhz, sample.performance_state, sample.gpu_util_percent,
|
||||
sample.mem_util_percent, sample.throttle_reasons);
|
||||
frames_since_log = 0;
|
||||
last_logged_pstate = sample.performance_state;
|
||||
last_logged_sm_mhz = sample.sm_mhz;
|
||||
}
|
||||
|
||||
bool IsGameplayStateActive(const system::KernelState* kernel_state) {
|
||||
if (!kernel_state) {
|
||||
return false;
|
||||
|
|
@ -2736,6 +2788,8 @@ void VulkanCommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, uint32_t frontb
|
|||
return;
|
||||
}
|
||||
|
||||
LogGpuClockTelemetry();
|
||||
|
||||
// In case the swap command is the only one in the frame.
|
||||
if (!BeginSubmission(true)) {
|
||||
REXGPU_ERROR("XELOG_GPU PRESENT: BeginSubmission FAILED");
|
||||
|
|
@ -3473,6 +3527,31 @@ void VulkanCommandProcessor::IssueSwap(uint32_t frontbuffer_ptr, uint32_t frontb
|
|||
ui::vulkan::VulkanPresenter::kGuestOutputInternalLayout);
|
||||
}
|
||||
|
||||
// Host post-processor over the emulated output (settings-menu
|
||||
// backdrop blur): runs after the gamma/FXAA pass has fully written
|
||||
// the image (the pending release barrier above is flushed by the
|
||||
// RHI frame begin). Frames the native renderer handled returned
|
||||
// above; it applies its own effects inline.
|
||||
if (IsNativeGuestOutputPostProcessRequested() && HasNativeGuestOutputPostProcessor()) {
|
||||
NativeGuestOutputRenderContext native_context;
|
||||
native_context.backend = NativeGuestOutputBackend::kVulkan;
|
||||
native_context.guest_output_width = guest_output_width;
|
||||
native_context.guest_output_height = guest_output_height;
|
||||
native_context.display_width = display_width;
|
||||
native_context.display_height = display_height;
|
||||
if (native_rhi_device_ == nullptr) {
|
||||
native_rhi_device_ = CreateNativeRhiDevice(this);
|
||||
}
|
||||
native_context.device = native_rhi_device_;
|
||||
// ever_written = true: the pass above just wrote the image, so it
|
||||
// is in the presenter's internal layout once barriers flush.
|
||||
native_context.cmd = NativeRhiBeginFrame(
|
||||
native_rhi_device_, vulkan_context.image(), vulkan_context.image_view(), true,
|
||||
guest_output_width, guest_output_height, &native_context.guest_output);
|
||||
InvokeNativeGuestOutputPostProcessor(native_context);
|
||||
NativeRhiEndFrame(native_rhi_device_);
|
||||
}
|
||||
|
||||
// Need to submit all the commands before giving the image back to the
|
||||
// presenter so it can submit its own commands for displaying it to the
|
||||
// queue, and also need to submit the release barrier.
|
||||
|
|
|
|||
|
|
@ -370,6 +370,131 @@ static const char kInterSemiBold_compressed_data_base85[17270+1] =
|
|||
"'`vQNxLF&#i^v8/MXZY#P;PQ')>MMFDiu(/G#####m8AtBC(t/";
|
||||
|
||||
|
||||
// Exported using binary_to_compressed_c.exe -base85 "InterMenu-Bold.ttf" kInterBold
|
||||
static const char kInterBold_compressed_data_base85[16985+1] =
|
||||
"7])#######02xYI'/###[),##1xL$#Q6>##TZ;995BEL%?Ver6Rf>11uRdL<(jlS;-_*87_qEn/3ExF>'aGI_u*m<--@^01uZn42Y)*q),2MY5xj*R/aNV=B-b;1EkR3$6u,>>#Hb(*H"
|
||||
"MY`=-%U^C-8BRm/#-0%JR`p-PgLPH2DQT`3G-d<Bh&o@H%kP]4GIVX-TT$=(9u0j(tU^C-W8Bk0<_[FHL[Pg>@kP]4vu8$$43JuBd3fCRGXI)44#691+K[^I7RJ=#GZp%48Mfn0+>00F"
|
||||
"lq:T3WKIV64R_B#,%S+HWUU7#Y#iH-5OD21V9Xc2FR=;dW2>8%CxMd%aCZV-jnlV-,/vS/#K)w$_iQC#`J#j#MG72H<2T7`A2?X.Q5Zp8nP.WtV?VcuFJO0N9nPmS$<)=GS1b(N*wAvP"
|
||||
"LI?>#0c><%xjt1K81pR*iepR*gb1`s',TSSa;vofI2/T*PTp:mu.OJi>H)S*[a8x9RsVcM0)wQj:^'%M8TW$#jE9D3&0fX-BijfLQTZO(Re#,M(FR(<$)3fLR*Io.'02Ap@$Z_$3da4S"
|
||||
"QdaLu$3P+V8]Oig&5>##YH:;$hdP[.Nke%#@N[U0H&B.*Ynn8%0x]fL)_i8.'HSF4+87<.2UnhMX/IA.GZRkr'M-/>p^-Gm3GLR#ag6QN#QSdq<+IPEwu;^ibqXR+.6+5V._$XE:Uex@"
|
||||
"2>o'@0X&##-^@)vY4Dg$'*+&#oD,W.KJ=l([eX@$B2Z8/gG+jLJLihL_sgd4T(r;$TY7ouYFiQ#x#HP#PaF[u6m`AL-'oQZn$c@#ax;ku0TPQ#bn@X#EAx>pxr[huT0vvtOabXqe*P:v"
|
||||
"2'Lo@=.__#S`($#*Mc##KLM0vR(x+MG^%dDk$@9&6]###&vmanuYWe)>Jc>#Z'GT.VvmbrnQ6V#nEYluBD=Ze95r'i(&Mg]SL.2o4,MQ#`KV[t%Sei.YH:;$+M#@.bE9D3qHUa3S:EVm"
|
||||
"EH(/LZ#=M^M9*uu>?p@OccEVMf]qPScaW5',^6pf?81;Hj2ZSf6O1T*o/`%Xu8%#QEq>jhBE4;H*+Q9i&B1pLfO<^Od<C.$Uf[d#P,l<M*I[/twYFw^ADN1p^?trmY3IpKGAPn(KG>c4"
|
||||
"ZH7g)@]Cv#)0_=csjxu5Lc:/([/P7egi/HZ'.p>VD((c`7Mdku&s(R#YO.X#?BLKlGKC,vS*DsuC_=cn4GlRn.0/rL`c5vG^CDW-2dXL#]'gc276,)3gk%SIfs9EXScC+2$AJ(NR3mA#"
|
||||
"ZLL@-77ggL<Ah._uwTMg$;OMg&ILrZejESR+xHi08####U(kpuj&krLtPQ)M3)oK%8f''#^;vof1%>Q1M>?>>b2ZSf7`):2D^Sw9DT.S*rGTf:D5B>#TJM1pKT,_JdV###NBd)4MfXI)"
|
||||
"*)TF4LkQF7u,d]4WJSX#qIr`?e6q^u[f^Wl[MPLFnIL)*;8`CrdM,W-w^Ce6UP###0,E:/uR>c4t$TfL_J2K(mmGT.hAAr:^?JVH4-dV6hfQr?lwEpu2OH;<mNx:ZM3%l].%>quf+t^-"
|
||||
"*Vp?9ip3@9RH`hL`C#,MxcT.1:<]Y-c_5L,_^@L,-$l]#]1f]44n@X-Y_vNFfCI8%#9#g1m`l]#6Un`%B6_-(?J8L(pYNNps:S4fGp)&+A7`Es/X=f(S&H]#YdMMg=IOqS$,C@[E+*;Z"
|
||||
"d'/Da1;DJA1c$84$b/Da%AwCC%u255;t,WS7#,sQUpK,*H+GB#7HT7/#_kEI>%TfL5)C+*HJ:(>1_d9Ai1M?#Z%Alu00eQ:=rbgLZ0gK:?C91kh5-&MHebc8lv%NNE(Mx=idWr$V8iG%"
|
||||
"&f''#A####SuaT#M7C/#.^@)v<1Bm$o*V$#2;J('&uXe)@]Cv#J29f3?;Rv$fT=x#nj#Yc$kxu5@9^Y5XJ>uc@Ln)h)X<EhgaLd2ubX=rw4d(^xmPIqZNlIqbkrEI/Jg;%xGuG-dq6a."
|
||||
">oPN#f@;.Mru@u-;Wd*N%J4l$9g)bNRhL:#[PoQaRhUSSa;vof5ODx9a2ZSf&q-T*^<vr6Yp?JN_.2>l]`ql&8c###f7K,3#DXI)t29Z-PA**GQfh69edsvK`i[FrD]*7NeDUMg[/wcR"
|
||||
"G/.A+Wcu1Gup'##.^@)v)0Bm$sB%%#%px4(1+RX-urkV-DwE^6_,E.3>5#9'om.7iSrd#K0nv`EjD%3[(nnmZe&Rm:xO/ouJ$#fqY$WG)#CoHJ/Ue;-&76&%HHG3bDKu3b1R+naqiXV-"
|
||||
"7#dNb;E8f3m%hGNkNIY#Dw%L#c7]lu,4pU#wJIuLn0p_sTOHX4Hr[G)rerP&EOl/#$'D(v(B5)McqtjLRFqV.B(C+*D%sM0i+39/ND#G4:d^O9cp:Z>iL0+*r/l]#R4)Z#j.&tuiZ220"
|
||||
"rK+BJ`iBmWG^7MuBp>[O[3ik%jg%vKm2=@K5?hLg;MGWGKq[l;@l'k13bo*eR-:AM7<=6;V&$Y-.ABDKfFRfL?7KnLMfcgL5Qe##7r2]-6n@X-EE#&b)miOfN9eXu$GBJhx2qGNjab>>"
|
||||
"SVL/)n^Uv-%o-lLsp6u%^9s;$#Otmu1agc2;koLYF[RjuNBZqr9rU7uKLwOYT/s4u)PT$OR(4GM62rPSx#'/1*c68%bnOD*BnuS*D=)Giu.OJi1w(S*M55x99JhxF<jZEe-V<T%aG###"
|
||||
"$DT/)Cx]C4KgSX-`(wCu^1dUp1:49/sTnV#%^qV$@?'(GM^^&TF@_f#J%g0M=I*^-X8:-m%$ee6T.^C4:G*^-0,mb%'7ZV-D1NT/9%`v#U]p=G6EjV*ENPO01xXV60#nK)w@YXZ6MPxF"
|
||||
"LvS])*c([,:]OwB-@X?o]RmDD$H8S.WCWfGR5b%kI=u@t;#w#A/Pt7%(*Yg.[PUV$M&w]/nE9D3^hlG*o&,a4Y_:ENZ[7J3TG*A+_N$r32tWV7YMnr?Pg#j'hMbv3x$v%5,7%AtIi:%="
|
||||
"CfVSKHua(3W#)#5D1.G4JwqUdPG4Z$#cDE-V:X3.5p4)O.[9N(UtqbiMBm=,U&VL1C>+/C57WG2MgKc2/n&&65qIlfM=B1ga3(H2m0dj#(^Q(#*Mc##xB9U#MO:h.BH:;$U>dD-'Q@h%"
|
||||
"8^@A4G$Ts$iF3]-E+Rs$u.'J3$+rf#%s#81@%YauV]=Iu.dV52ShLF#`bM>>ITH%$-_+6ufT<i4&sx=#j:^l87P>p.^,su,PU$##PNv)4MtP%.`w]79c=A=%+n-H3AS'f)XXj=.3:,-3"
|
||||
"rQ;Z>@4[s$iL0+*R=d?h[mbFQTEc(?7c^Lpbsb+`S>`hu3J&-WK&wt(C?%a=Sl:@YnL8D8<Ol`4_w94;elNVVHB9Lu*NuqBDK.#JV0o<$O,qv7A2b(NL>i8.BT'a4IjT&5vW_S*%-oFi"
|
||||
"XcM=-kY$=.&31-g)smofYcM=-*cD&.as'XO6e3bNS*@B;T++,M+N_]tlUDJ1E[,87KcCW-15CQ(Tv/o8-c5g)h,r]$=tJs$=]d8/jZ_,)a0<j9#j'B#iiOjL%hp:dqikImp?^,>K>fv."
|
||||
"E]*Cl>8J8F./aLu*+n1GTr%A#Vf0xt:Xwj7FhsMZcpfp:nD)s$**Y5J6'N6*;P^WUD-g&63vN3LsmW/uMG[4OUqbnS:=,G>'lFU?k33<JLFjaE-w^Y#<_kLpd1TMg[Clr-h:Y:/N6fX-"
|
||||
"Df*F3twC.3u7XI)CDLQ9Px%?5GBxVK?]MZu3.St%dfn^AcbKaIl1h]J[Z;>KNaY>@1vSMg'Ld9A9T9>#GWM:vAQAhRmXv)Td/2RE1ib)QogYqUdfvYutZYC.Hoh;HOcF'#vdu'v(o9F$"
|
||||
":aaj$V,.&4(n%J3v<'Y$C></%$,F.3FtJs$G3:JYp-CqrJ4C5&?M%E)2bC#K5<',Gn'[=-l]^XQ,WOiZ,Ms.dmaNSC%:wSawSrt[D+>>#Ed1&M)`@5/e<%##ZAO(j`2,F%=]2eHnFl]O"
|
||||
"Daj)N?/C+*2xs^oXk5Q'Br/K<3v',+j$Q#KLMBWK:PD?#7t&+([fgK#ZN(-HM'79)fkFf_eXi.LE[B_#0++U]11NPqoI>^^oXhV`4i=(.bB0sLB#K%#FPUV$'@ho-NBp2Dx'-&%6%joI"
|
||||
"(>`qM:BQ]pba$W$9UNFGvNf30biWo7+$psH5t;6*euZo[iiwrQ[A:7>iVsv77%Xl#J&-qM`gQT-0Sfb-`:J?pEordm#q7fhvdM=-]0+r.P`39#^l$T*ffCD<u.OJi,DLq;v0CG)A)s]+"
|
||||
"X;@D*9<hc)4ctM(9,B+4HjE.3C4Lp7h:Ha3j`$No.EsRcKkXBHhhk'4mj:A$A9&`N1Vqr>Rk;r%Vhgp7U_%##vZYbuUWGJ$]FX&#DeG@.auvs.BFEp.2=Ss$`fD?[gSA+4pZ'u$<3ie&"
|
||||
"H+;>Gqwpo.WP:@GIjJR*.'d[Kf1E^f#foQ<m[QP/[Yx/H^-()IagKi9X@a32rFT@fC'irqp8#JLUC^K`,*;gd6=JPYiY9^_IDF5#O'[0#@Hnv9,SbA#jJ7Z-mQ;wg:#)C&]9^:/pbv>#"
|
||||
"O-'U#JeL(K#kK.hsxm#J$NY4JZX-i^YueUcEf,8R(sL,vnnWrL/Lo0%pd''#q####GZKEq?M#F@RXXM$/6>##)LEn<W:3/1$9LCW#,Y:vs6YY#T@W+N)id##<f>3#jkl%lUSM>ZA<mR*"
|
||||
"U*DS73+G&#oE6^#/(Au-(Y2^Mv,@tLgTOrNb_d##'fZt.HqpOf'>)FIfD###'M4I)O?Z)4I5^+4[nqakAv40;;0^_/9d9(pO@R:vhR4X_2M)Y_BY.h$2w^dM'j^L;Y%cNbs?3AFcUO$B"
|
||||
"X=oFVFbl`$(e,V#?d[@u+L:GB:'`qr.c76/SQ>n$LrJkL.+p+M9M%pecGSV-:*5H*JjE.37dG,*h1qAO>VK4LtDYK1WWp^;&IJYT*tSBD&uUduU;IL<wh6mi6FveLpd2QuK-aHACo,Gu"
|
||||
"4GcU=SeMHuEnI3%3+r`EGr0hP4ctM(UAaE-b4VA0s5G,2(1U`gcWX5h[OF5AB-H'8`O04LM>ZPJce?5/%*&##QKvLg]_0WfdUTvL8Xs'%.6t29TV'?H#8`qMMrle$bivAJ]ZQ&JKa6H2"
|
||||
"_21rZ_;@rQPxucR$rcGRoPtG%SY=^+;akY?g&iih<9).Mg`iX-W[sq)ab[PSh.;x0xVNj`@O&-%['fU.P`39#9MAu-<E6^Rb<kBMOTg8%u;C-*E4$##(16g)ZVQQ/`r29/<Jo-)bU79."
|
||||
"/<XQ'V>$6/#P]Q<s;<g9g_k]uD0uF`ZP66/=7OBuBSwZ9jM])=fCg]O<P=B#1S4Q'OlN<9nDP)4;/lRE/qjhuf.-+<,iUZRaHs?##k)K93l'^u[mFS1BopvR_5H6u+Gkc%'Wi.LC;S>,"
|
||||
"P[JJ(/M51M7oxF4[4pL(6'1T%/ewK#WxQYQL3=L),l0ON[&_@D74rbrsP&UDQiB>#]VmOo.ifKYk=)W-t)CUBI=WT/*C'Y.:d^O93Qi/:k>.d)El:Z##PU@t.61l0#@OEQNS_R#YVT+p"
|
||||
"J=++)/di6C).hS#2m2A*9Ls3U`vZc,+10(W8BS:2m@_/3lJLn/g]I1X;TQt$3D1b;W$pG3F[hE[lf@lD(Mp;-OSF4%((e)O?@S#.,9nENZ)%0Q''mrC]>6shmxd>CXw=cH=BJKFcA-H$"
|
||||
"a6^BC*oj-HV7q6&t/<w^<@ke>IZTsGMM1iF7fxCM*oJwu;r/m$$AhhL@]Lk)IGEF3/86J*u;Tv-^gx9.iNAX-_>pCg>bsP(gQsT1_QW^T^hF5A>U8^DwYg+(pn.k#a8D5&plF>#A6H+r"
|
||||
"[YI5A6]###`I*^-;5$Up(^pZ-Xl:Z#R3qT#%[^vAu0h^u>k2[Z/*do#japRD1HPf#2vBenpQBuuiQ5w.m-@5/=5hd+wwSfLbpj.L,Bjxt>K(E+.NTe;#D,H3[jTv-5(aE[:-VN(Ir1tI"
|
||||
"RatJ#MIqQK5=PjL<#$$01g]##t,ffGL%foR<Q]Xo`bcB6n]%vNVjK,Ndid##Gqr4#$a#S*)HAGDu.OJiD2;qVj>ou,*XUVZn&=p&3osT%Vsb+`r>7870Nqj(,boh_mQ_.C@0xb`N291<"
|
||||
"M(sl8sjV9i^)Jv$Z3RA-W?qi;`)E^4CxkQ#YtKI2FmfI*X(%1CCb:f)uc]H#k[=muV&bR/1XZK)Vb%du8_bHK+JQH#-+vZAunOsh+xXatj(YA:>R/;^>XO4MfptjLGSLsL7,Mm(g:a$["
|
||||
"oq+>%a>+eu'TJQCS<[(+8]PI#_r7P#rUr*KT6@c*I*og#4#QV[37V+4x6LPuCBO(h.%2f3tkX^Mp@Qu>gPr'AU#5J*3YGc4qvcT%;Ah8.)0j>9xLVA&2W7H#V^b=-/tid*#1<`u9.w6Y"
|
||||
";u.VHb0l@5o_C2Bd6,_Q=RJ>:.@Cu,[ln$#BAxu,-Mpv$gd''#Ls`Y$%>Fr)Wt#r)t3$Pfo:0L,jd9SIvPdA,`][m/bE9D3Z;2]-:eS@#<COUI5*YOoId.%tN&)n#?O;9d*e(gq%/5##"
|
||||
".W+41b'F9#HXI%#ihtM(T8gC%Gt@X-qVqF,Pl4jL?FKL]fU'6s+L8oeq]%JN/P8Ok=PeLp@D(aL>R]fuiJbA#)2fSL5KU/1;Hs)%h9F&#Q,>>#>wGl$H@OK1eO39/Xe_a4iVp.*M':au"
|
||||
"6Smt$H<]f_b^2AMH8WBpoaQ>NXoc3($_6cu7X`,B&`OlZE+sXcn1,cFC8a#K+Wl1NLe:g^3J'pN8?QAp8ggC>5=#nODPL-WIu(Ot,>Qi>J5NsNil%v#Znfx4])1jT^q-eQ.l$##RCRW."
|
||||
"B(C+*1$c@7hof+46*l]#vB&_S=F%],RTF=$*tqHM#N+_/C[Rs$eKx[#dEuXlZ]V<%%/.m`@]*oQ6#nhJZ*MO%UHK9KUa#fqXwl1FbIF>AqFma'xh0hF'7(S*8hQ_KsF^j^q&jlaYexv&"
|
||||
"17-@,'@Yma,EYQ#B?1q:*p?A#c>pu&V]gK<wZj%=94:,DmE$#,F7$##T@S_#ND#G4nTjKYge*[T1RHZ$KsHd)6,r29>)PnH+s&HI2?3]uZ21]7t/u?#LU(j]R&@&@mLF5tN@TOS?S]`'"
|
||||
"[,pO:Fg8iBj8FL3I0pZY]iclSF#[<L,.>>#9[3eHZ`6eH=8`$';ORX-8gfX-2E9N(VRG)4/)8N0[H1;?Bs?;6^#nqu$pB^uxnWpu$Ia`u?vV1^_'E+`A/Cf1JEsluR+BkLv]NigcjQ&#"
|
||||
"5p1M3,Wd;#)F9D3G#TF4uKZ)4/h;E4D;6T.0)xD**VC_%f$:u$^>8+4MfXI)S^Ng=)Q%0UD*NmJ_iZR#1OfRnmdd7Db_K`NU:Zv#@@k:mm'dbW`e*l;k#H[Uk1fE_SU@0GT%3u<bdb(5"
|
||||
"<Pr3#.^@)vw6@W$kh1$#?PUV$epq0(#&_e$VBq,3Ke4`#*Trc)SZw4]$/@uc$i+&+%8su5#<5rmbAmPO>f-W)xKv(EqLlXR$h<lS&w7]Fxc3dr&l:ZuWv7tuD_Kk.-6YY#m`tV$;c?X-"
|
||||
"nDvJ(7e;suRKY:ZE4PMglj9>ck2-:nc]sDFmVPMTqIs&,66@+49j/_%8*RalnOAx$h6eX-r6B/(3cq<C:b=%k$%rvU'w7dXmYsrHTJ)-C>KC*Hwr);iR_=`Lph08P7(SFMY>p)vdC6&M"
|
||||
"%:Xr$;$v&>KqF$.%:;W-/ghU83sIvPlXB+*VRr;$W:Dnt@SBD3;9ILFQ*tNOr2WFIvXai#8uTYP-Ub,n_R:QW`3L:vDW=`LT6-D@Gn@YFPHwqC-&'VFE=YYKXnnd)ZiGKcA05##8QUV$"
|
||||
"^%F9#<(V$#mOwC#HH/i)4ctM(:3(B#eLZ]FoB5$`fLRm&H(%p%C.F9u>nv_s&sot#:tQ%'='0s@&b72U&D=RqKxsF0xXQp.80fX-W(5X&w6G#%#:U#q3^MM0,Lq,GRDU]8YF$ipGFwKu"
|
||||
"F1Xm/'@bwg7]#c)27C-?i5VDIM.%L208ptuP7]TKf<'dnZBmX$%c>n$@8iX%)DW8C`_J@>*e0^#*$+l<EI^&=iL0+*Z>`lG@Sa^R(%FKRIqZZu0IeG?8L)*3h[A$gt2S[X';slFk+]W8"
|
||||
"L$$FYZlVVti7fnGdTM@@bnEIBvUl9PtBJtq;=,YT#4-R:d6F3t=[%##]ihv7/%gG3ZjH)4^HRo[iU`+2tvR7RPZN#uM[I+2&5>##s0ST$Vvx/.0qvC>x:Gd3QGg+40=3<--<uc%/lq$^"
|
||||
"_=;n_=m7d#dj1uuNvXG@KQwBon63^Nogf`Qb&lXu]E?fQtv'2PD9V^J3<`#K2.q-JAU'u'#kQHJsh?RD*7c;_iN*eDV$Q5aNms>B>x1wBG$$/Uc6sfFcTocGI?5qVe0IJh1Qwu,#[]/."
|
||||
"^o,98:E0j(1T,t-^P;;?)^:E4.S39/O/WN;fG,wJ4FVx'0FStHa7gE&vlNpMGTD4taWisXbn?+r*:X>9p0?R:/$O8**5am<cW^%kLmg5je6wANrG>bY+E$st(53o#&%Qs:utKq9Hon=T"
|
||||
"x^j%=kpBv>0g88.5cNY5xaaD<?;u`4&^`H2T5xF4Pm(u$0/vS/C[@@#bMKG.nin]4o0KT%mLx[#Zx-lIqFrc=/]&EIJSKC@cOjY-0TZck>#X/=K+U=lx-C#IpWPn<sG)X8TfL.*ItdoM"
|
||||
"s2a*YXJ2ACAMKQDOil`u[q1X:JmXbuYGpA6DqLP8B/?)6K0h%bS=%M3)Kq$7e^]Y#f/mLp_&NMgF47n'9[ww#mr0s-lf>d$ubXI)euc@#Vo`7<1=71<qO%Gi$>q7%6Cu5;#'=Q;P6i+M"
|
||||
"lZArQV5(sHrU`/)vv;f6o5c@#$I%:NhNArQ_si>$KTq1MRbqP1e6]T$(IQ;#lvK'#rX2eHijq&$co&W&hm?d)T-OG5.';_>k-Z2E00TG$3#*,MC2Toj-ff*?xEQ3X.b<kFCn4v*]<A>/"
|
||||
"M*`0<u&]DQT3^v]`)di6Gw5l*7&-7aYAs1Bu-3j0ItkwKgCGv$r:;T.ke#Q'MNuw$=>_)4V,jhLXU>hL64&Gr@kGq8(f,7:hW=;-[h['M@J(905#GSR^uoA#g]0b%luo=YAbg;-/("
|
||||
"klVb3J%E]-BHqc)s@9F*lE->Y'SBF#x_Rh4`@gbuh#]h#;l$,D-TBvT059l]4ir_s-s#bg>$E>#.op?9k$@@9TYOl(Ke4`#.'R,*q7XI)nfWd=UIb1D$[TI;2'<[Kwj^R#2;xJL_VPF#"
|
||||
"2xvfuo?'6XjM98ganWB-mpWB-TpWB-lk/S38BF:.3U<O^GDg4dWxOm8mDpHmP?uiC8pWG<5LA+4k_Y)4.r@*d*KmrutV1mJA#t?MJ)uhuw@tKPNl`$'G]R_#=U^:/:L^_-q(e'&kcMD3"
|
||||
"vl@d)^=9mA9[Tp7@]WF3Q9V=^cLVJFq%rF?k;epF3MEjCqKE_.<;J4ogGFXMRI4F$I[ufCeai6W3hHcC_Qh1Di(?(+DR,iihpl'SSS#lL,XOjLf7W.3hmxD4Kh*iMpCZ)4Mx]fL:;_F*"
|
||||
"FrF,M910[#3_%^.[$VTC;tnNF^,oTB+>UZBqhrguQ]FkERV;a-0_pE@H,&rC2sS?0S4/I#&DFJCDh6[KR/5##1_<A%_:78#+o.nLcQqV.XYIh(I.X@$MH+<-eDl<'q&A+4:R=,%]h`S."
|
||||
"xsGJ(1YJ,3X5Du$nHdO(%'7ftf2TI$lM*pu8s+V#6ouhiGtxdL%D6^BGtJ^+,8pna>RF%M;6[S&Vt>UA/wE+iK.vju,a6K#qsH37[DXFr#lYdH0N>fuSQ.AXqq#v,K+&eL6W$D_]uF*V"
|
||||
"t8_w@W/nGRW=(e4kY?Q2Eu(rb@_<kuIv7lu1EFS3rU$<7::?8.<JT,IUb'kkof###pPNMg^7pu,W@T/)(Vg/)5RRLM/@F%$8VE%$ZajJMuwSfLxYx4]vcVn6:pHg/j8$K/GePW-))1I$"
|
||||
"s^NsejZ<Wec9`G#SS#ci$lKxO(7LV#dE0(&hp>.m7f4ru_@VG#e'NY0CxIp$Y.W<#QDpkLaad8/i>169w,bc4$=_C4*^B.*:>`:%m&Cu$NW^:%(f)Q/F5h#eb,?4*Yug3aa#K4ORi>,4"
|
||||
"vAwV;^5M9_+F^%T@:I>#SqRiPxO5=EB2%,H@3WMb4p-])bRDhPes$BC'V(@RfZL]6l^=jP'aW?De;<eOhM]89DAWfJQVIGHW>uGdD->>#`FGV-Dv;a*`S_$'wKkA#EA=V-VfYxkC=.o%"
|
||||
"o9m--WgEV->aN5]Y%.O(bhE5&lKV5]$'SsQ*-)NhC2u=lCMSk+Y8###4n@X-hbtM(Zk:Zus0.m/flq$%[Zub#Bb8F->R):1@;FV-`uPxkR2n?tImK8/L.89$e_]_MBRfX-6K$CuN0-Fq"
|
||||
"BoRfLSDTT$=6Su-QV@+MFtr=-F9Su-VlN_Na3tO#a&q#$Gm'hL*PA7&1h'u$iRMS?JXR`QhPsi>t-qf(EQ-(&5mw%RMZQt.2nqfCQ0DwgrA*.)c;;@_1/?X(DF7oR,YVl+int%Xb[B3_"
|
||||
"f:.L,&3sfC%lSL5rdK#$N_YV-]Exu-PlRfLY7&,Vm>&:/jL(##q]GiT.C)##l+LkFw.=cM#uGWML.[N#em`=-#b`=-&s@u-_w,]M5'jfuBno=#cOc##8U[stMFuG-;GuG-wMj>0ltlV-"
|
||||
",RPD_LQ9bIL)>>#fDqJ#0t+`MA]Jstb78R0h%&t#mqA*#4Z$8@eZlH3?DXI)ulIS:cM;nLl,WkBGLB6Mi_$mL'>f8@ovd8/wi`9MgR](W[%2F%FG[(W;3IkOoMs>$Je).-5=c&#<Hu%N"
|
||||
"*_rc)$[IP/BJAeHb-vE7HX(4+cSWD*E*a]FK/l?-B[125fN>/$;]j)#Mrgo.GKkA#eNvru&FgM#K<fJ0n*'Gi$mt10pjYKj3`?/$b#Y?-*3:w-'U2uLUetD#V;>lN#7lsLCDHYukjd8/"
|
||||
"RU>eZoS+JhpYj/;Ra_ip2qx%Y2qx%Ym_$XLN-b4#5uIL$jNc##mOwC#KXR@#/`G]t$%v1^J3eX#sapquRkI7na0l9Mj$]w0h'xw0F^9[0Ur%9.GHSr$$3]+;Pde+i)O$v,Lwi?#_<O]u"
|
||||
"`5YY#%BGrm@UW`a[@%T.qH7g),UaJ1$'h:ud^f4nS]>]kug,XLeMQ6#6UqN$-g1$#%$AX-if*F360fX-nuv_u$,egu$s?iKF20=q$H,SIc69sL$I95#7TEP$@mWX%m7QP/^%3D#=xG>#"
|
||||
"5[uUZ5D7R#3vnx_#urx=5BoP#M4K['87F['^1+nu7#%P`&B%VZ[cGD/NmE=$D:&vL^9W$#kkgk._kJu.CUxr%L>c)Ohc45]'#k@t)Bf9:%0At9rNUQ9hA=Se$S9J#9@l&uWntT:ws`<9"
|
||||
"Ia&qr<V8>5`-#jKx`Tm'iZ)FNMGF:.j^Qu5]vQgu$5W+MPKj-$LIODo$&5>#brxXu.D:*MkK1PSt/&,i#]x%+7*Da<:jE.3'`B+*;b5V/@rh8.d.<9/?;Rv$7H3sZr`em<9Bw)4V0f5%"
|
||||
",W@qKMCTu.I[R>#ADf95_e>E=]h'R6V:b,<**,##sPB>$?,M$#,pia-hOL-Q^s7Hu%rsL#3RY.q(jRR4$&>uuec*9#&5>##%0fX-(KI8uQ=4)MxW?'$Ou[h$]<4gLuQDD3n0M8.JrDD3"
|
||||
"51,=n`CI4$(%IKuXf=muBd2q/U=p.$((F9#xI1_%^=3Q/gY5J*>ln]4`W^:%;):`j2EeH#/>8MBM37%)rJS:v0sZ[#mo^.Vr=VgY4vw[u%t(DK=/&kubI:;$5'lr?U+wrmYC18.9oNY5"
|
||||
"Gcfw-h?7f3twC.3m]WI)It9w.`<>V/=A6gDQucG*NdQT%J`tYgoP[PVg1/vgpS[PVVEsibEGL1IFGIGHuH)A@VM[,;PJ@KDR%/d,J3iu?%UePVt+cw0ouO5TV<nSMBN=%66I%n#xu>J4"
|
||||
"WPFT#]kS60K>uu#a^T4ol#qon`65s7Ab$gL990C4Ob7e43T&vPAV=D<]#mG*YL^c&F$0[#`r29/n-&ku'TbDIYK'.V^M-.M>3cOo-HC9QhtnVMbqhXu&@.;Cq7s/h-BW&?]=c]7nO%h&"
|
||||
"`$+eGJYf[PH=0C-Ubq1M7e'^#)/a6GT$:H5>Vdc>(Mw:A<fN7`%/5##[l*x#?cK:#C5c;-Y1h-'i%EK;$C3D#a.t'B@)fWB';XmV$0?&@U#q`?jUSfL6%A6MAr%qL)VoS#7c.u-)>woL"
|
||||
"9fX`#]1r_$004&#O,>>#YBB9.vBo8%Y<AH3AS'f)_Tnj<GNS,;#?Mj9]N_KYA1n`H_/ua>PpE8f18TSFW;eiLu$T4*Zv4-=*F6]KG>jO3vAGo/EiRr/iiw,6xjI7emTgKW#%oV'#BQa3"
|
||||
"](/(#6rN88It]]Y7Ma3k-nB*<RI=W6gs^kbCnpXNDnWOE2+wsBXq(tB.Wh4FXJ,kbBF=%M8,D:_mD>.PmJPIPtE3D<lL,m8_rQM'U.tM(1ZClMA0vM(-a[m><JJGHRbl=uNpMo7,>67N"
|
||||
"FtD+2^)HW-XApQ$?Ud;#JuJ*#mJc8/XkC+*I5^+4u%ws..W8f3Og;[.[ZY8&9`^F*jR'f)KIw8%Q6;jT#DSC4Bhp:/H`wv$L#[)*8Moi`0Ng.($ex'?ZVP@tYT#oEmg.)m;j^Q-xCx0<"
|
||||
"_LP;9eY5@J(OAJCa>c((`?P%$CC`Tp`Bu[$[)2au&PMw19n=^6*YrbEikgQDiC,]QK1p-]1hj3;-NIm:pHhjUn(wd$P.+4>4*+d#DOJ_4M&R,,1HA_4T5@g+>hD]F3TH>5>9=8%k&1P("
|
||||
",jv;%v.F>5kY:-gN-K=c8*>Kch*vL1YgplJ8TPAb=r###x;eR_GD4jLCU-1MhE0k$tZMJ#Rpjf<omVHkPcIlcOlM4=7SrlJLj@J1=3ae37Vu,Q_kXnB$IRY%P#Ia.QM-,$,o,D-V5f^."
|
||||
"d1NT/BST@%76jgLfE&J3g<nX]-6r$%nLA'[*x`W#C,'K<:?SOE*h4'6I2N+C=O<3c+iaGH[WcYGK:5%.x_sp/5G&r/ju-w-]UkDP/;QA%7.:Q;tJe)#>r'/#X3^5:@+0Z->U]w'u*$Pf"
|
||||
"MZMMg^q1hPmHxnL)*I7$lueR&P-5Oe:Zl1$#$[o#$2IEKD'D'#m5YY#p#B*#nu9H':U'U%%wWS.Hv<Q1HFwqXP2*i-7-RD#Z6QG)KKVK2&wC$Hk21@/PK`'7(rC>#gn#`s%Bg3=qL$##"
|
||||
"?3`b*Y<Nv6+GMU%E*ZI)^RwO=7?9PfsV%L2:xUA?vcGg3lWhE#uwi+PG*R0?Cup-&gES3)otA60OL,80h`:?-FoN`P-li>&RabqEY05##16`P-9D,n%#ufi':Ne+4FF+F3>HF:.omx9."
|
||||
"36HF#+BiWBQVw8[cRXJ1c'44G#ZTS7P,B$#tQW`<AxMAbxSOP/V1>_8d81F%x^MJC*c68%T=oG)ANf>-)4Gv-YOfVM26KnL>ZLY$24RA-)wEB-`''$.Ex&kL[qQ##w(oP%$cv;#/93jL"
|
||||
"R7](#lPUV$rZv)48Rb,%1AXI)lQ%;Q^XZ>#7R9)3oM4gL*k]@#?n)w$5m,v:A;WqL(r8_Q+5V%'r9N##d/fuGbC&79%lb@$FRl)3Q0Mu?f6(%@YKK>SfvA>S/BuDOGaCE3C?VMghH4Ab"
|
||||
"94SI4cAS.<87e_j7Fh#v&F=M$cvh$%wb8ZfA7AcVmhJ88_`%qf#)P:v+sN`s9j@^+`g4L<I3^h$9[io$j*m<-jxgS/n;2PAd=)/1_4B,j$,>>#/V5%$IA*.)'hAoOdkBQ$Gom_uk%4GN"
|
||||
"I$650[IT/)dV@T#jTrh,x@'DNI0QP0;.=`#somk$cwh$MMeGi$7Dt&%v3UhLulix-`GUv-4IuD4*^B.*+2pb4el:Z#>21W90Z1^#CqV%A%^7$.c%81$(lg`37G,nKwcD7B-$U:vhVxZ$"
|
||||
"]'F9#M$kd;&tFg.:&+##]?q4.uWQIM`,ck%&>rM:iFhB#^eKD?NT')?.W^b%Xe_D$@^SA6FXSA65)9gL$(1I$<(T2%5b.?Pmb75/mEvv8.b[E$DP:Z71ZjfLZRCP854QDET*SS%6/<Q/"
|
||||
"XADD3,5vS/h-2xt-QnV#xb(uP$81?#*%DO%%15##7Dfl#`JJ5#+e#T/XDvs:-e)4;q^d/;jbH030h2@$&@B;:k@F99Gr_BH4,x9)Q,lmLZ$],8wjp/)lur'4^?,T%uQ;a4SvvA4ig'u$"
|
||||
"8HuT2USKX@uDR5A$jCgu7f[Y-j8XF1#K9i<Teh+MD`Op%Rvvfq>*9j9au-w%3`7n&>LNAJJp[&JXE7Q3hYK+4aaPp%dsxHPdv+IPJwwh2]&4.3SCc15V,wO$eB`$#=H?D*ep<=Z$dSWR"
|
||||
"cc35oMR'8/L>gQ$'CxRN7iIwDD(vq1>RB^@ou#-3b%xlAjrWk+cr2]-Fm#g1B_nieqx8;-(1I202B*1#@A+.#fY-@'>:th#fAX)W&Q?(#V7p*#1tI-#&Lg2#B%>Y#]D6C#gnuF#ATOI#"
|
||||
"%r&M#mTTU#YP]t#3.rZ#w1N`#@ake#I$kp#h<7w#fvg#$@]A&$qBr($W7:3$&aK>$[N7D$M5hF$(rAI$YD;o$k%W[$vY'b$3D_j$at&m$8TMo$f,i$%hhH0%`+(R%bS0B%d4WD%>q1G%"
|
||||
"3[$o%uiVW%PU:Z%*6b]%dXAa%XPbe%r7x1&JETq%hH[w%>#$$&n_S&&sBW-&6H^6&Zd:C&XmuJ&hZWP&`>2S&L<[W&fhvc&Ql'j&8Hpm&@K($'mS53'7e+>'>[JE'FR=Q'T^UZ'-2O*("
|
||||
"+qOu'HUdG(:5T6(t6/A(DxgF(`?hN(x6t&)Uq@p(*'Rs(ubVw(VTC$)ERm()9a:G)rOk.)L6E1)#ml3)Y'17)F&0:)vUM<)hMn@)J4HC)FM@H)FNsL)&2KU)f:0^)i?8a)qQQg)Rg`@*"
|
||||
"RR71*^Ui8*dR9F*_uJQ*$C+U*rRn`*Mgfe*<4Fi*tlst*>xYv*_-Ax*?An@+KxgKM4hUa4'B_=HAudO^))-%u$cGc*dF?>#'G:;$Ig^-B3$vs']G?8%Y9w0#r=5^RaLRm/I>oPu=U_f%"
|
||||
"1Dh<.(/5##)=dZ$][:_$;NCUS(WPg12Ww/;F'PPo_w).$AB.DNDHX/$,X-DN%t%F.u10DN@iu-$ML4m#*nqAN:]6J$pAl^$n?EY.8=YF%f'l?->]P8.];t&%8x,Z$nKx>-Pn,D-9kA$0"
|
||||
"fQ[`$vkU_$rSjvPDoT=#,O]nL-`e;#c=W'M,oL5/=A6G$OkEWSA<a/:da<T&+]&gLX`02^PIv-$$EJ)=$=J_&2(q#$ltaX$F9X7/L<<h#ClWrL*%Vp.HKxi0@Wp_&SvG_&Co$.$rbUXM"
|
||||
"H,qE%JLsH$]?(.$?]f-6^NQ-HHZN)=pNo92mXr(<GZ)DN(RKG)KkwE7/7ZAO#i*R/iNvu,lQvu,YO(Q0-@3F%r1GtRP/;s?vE58eb^$W6&aEwgd;XY,Muw4AAQ$5ALs_o@K$*AFawTiB"
|
||||
"LG8JC_Ydw'1cx4Apmw+;Xf'F.2_dZ$,u:c#Zmh=MsI@5JM=m-$Z)Y;$RMp-$bol-$*vp-$r2U`a5Ko-$*^FW-<Rwh2Wc<a*tlw]+;PX%tvm@M^2wS_&cQ:7#_0###[gj-$mJ[8##)v5N"
|
||||
"V/H6#4IeqLG)e+#;QDmLRgH+#^@FV.%8A5#A?pV-nkxQE&i0^#NT,hL(7voLE,W_$G*uoLE,W_$fP1xLAF@&M9lL%M+mx#Mu#axL1;Y$M-#5$MaUTvL(L3/iK?*##QG+29A8)GV+wu1B"
|
||||
"#ZVxXG>cuGkCsx4T>/58L/O9iDs%5A?gJ]F6I#2BM3_lAC&p=GM)P`<B_-p95kr+;Ng8G;2tN`<>GU`<'5u+;[l>f_g:x=Yw<&hV.%E5&JV)GVR=AloN(EAYkIdfVj@HJVvH4aX`,+##"
|
||||
"xOP8.QjCG).[V8&[bniLbLfS8,owlB[Bv92^RlE$-0k=$a/`($=R?(#+c68%J<%%#Q@g1t0H'c)eA;YP1Ei:#$_Yb#:CP##`4a@$[)A>-vfNj0]E](apX(##0ohv#5IC^#*w,tL0_CaN"
|
||||
"I'7f=eq'GV4GQ8/6c#<%V]l)M8xSfLN_;;$1jm&MmJG&#gI3#.SxefLc^/%#V(4GMQUd>#NU#<-P>Ib3V>$(#0rH0#EIFV-?T=>-6mC#vc?cS08,3)#=aeM-L8tbN$*9nL=o_P/2]'##"
|
||||
".=iIK)4jJ)bGTK2WSElf?tixF6oWh#%:P9#D'Z3#(<L/#q;w0#GYN1#doF6#tE24#[K;4#YWM4#u,85#D#*#08(02#-Yu##bEYf%rCcf(JgCG)RM<A+Zrsx+_4TY,k9IP/ud*20#'bi0"
|
||||
"+dYc232;D38S7A4BT8SRe,f1^Nph7eDX,87REDP8[/=J:dSt+;nLM]=xw.>>&:fu>2?ZlA<j;MBA58JCK(L`ETO-AFZ$EYGBHWrQdmi4](WMcMhp=SI+a#8Iu7?c`<b'edI]r6#+^+6#"
|
||||
"SOn8#9Qo5#NVL7#VC+.#1e`4#[J:7#4'/5#]%.8#tSE1#,AP##,5>##,Mc##6f1$#@(V$#D4i$#R_R%#RR@%#n5=&#mWt&#lEX&#IC]5#t2h'#+?$(#3dZ(#P*]-#Avv(#D>N)#D8E)#"
|
||||
"N]&*#[%T*#[oA*#n[P+#nO>+#E7D,#4[%-#3I`,#.Jf5#>H4.#NTF.#Q#(/#)st.#WqG3#d?)4#`3m3#W:4gLnL?##_RFgLwS#oL1F%q..`($#YT__&OrQS%GkC#$@P`PB>h-?$5OTxX"
|
||||
"1@6Z$s$k^oST@v$)&%jq+/G]FW&9sI@Lf?Kk]4GD(+p>-FY)kbv:v?0e&hx=B$:NC7n;J:-co3+P_`'/U+;kFlY2R3mJ8JCr`v9)S%co7x)0F%Apv9)+7auG9bK88IdMdEg'X`3#4*R<"
|
||||
"UpYc2q#n+DNHn>H4Y3>5[*E,39:]PBqGZMCrr^G375#E=o+Uc;YY6#6?MpM1&EcdO`P;=-hi?2MMA]qLH`4rLbiu0OusMa-U?*1#XB:7#SGNvPPL-##$)>>#alRfL?ecgLw1K;-..m<-"
|
||||
"pRrt-31SnLCa?+#tSGs-^g$qL0(8qL[$TfL$$YrLqKM/#CTGs-CABsLqp.0#ITGs-gX;uLZAm6#3UGs-&Dt$Mg4/8#>UGs-7ug%Mv9c9#KCg;-.e&g1Iww%#1;gl8`Yx+#^14,M'(7:M"
|
||||
"%@c(N7v('#'/=GM8gtaEhpf.#iJLW.#####+`6o%vLY.#";
|
||||
|
||||
const char* GetInterRegularCompressedBase85() {
|
||||
return kInterRegular_compressed_data_base85;
|
||||
}
|
||||
|
|
@ -378,4 +503,8 @@ const char* GetInterSemiBoldCompressedBase85() {
|
|||
return kInterSemiBold_compressed_data_base85;
|
||||
}
|
||||
|
||||
const char* GetInterBoldCompressedBase85() {
|
||||
return kInterBold_compressed_data_base85;
|
||||
}
|
||||
|
||||
} // namespace rex::ui
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
#include <rex/ui/windowed_app_context.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <misc/freetype/imgui_freetype.h>
|
||||
|
||||
namespace rex {
|
||||
namespace ui {
|
||||
|
|
@ -370,10 +371,38 @@ void ImGuiDrawer::SetupFonts() {
|
|||
// loaded before them - fonts cached here would dangle.
|
||||
{
|
||||
ImFontConfig config;
|
||||
// FreeType rasterization WITHOUT hinting: light hinting's blue-zone
|
||||
// snapping rounded the x-height down at the menu sizes and read as
|
||||
// vertically squished (playtest). Unhinted FreeType keeps the design
|
||||
// proportions exactly while still rasterizing curves cleaner than stb.
|
||||
// Advances stay fractional (browser letter spacing); the Oversample
|
||||
// fields are ignored by the FreeType loader.
|
||||
config.OversampleH = 1;
|
||||
config.OversampleV = 1;
|
||||
config.PixelSnapH = false;
|
||||
config.RasterizerMultiply = 1.0f;
|
||||
config.FontLoaderFlags = ImGuiFreeTypeBuilderFlags_NoHinting;
|
||||
ui_font_ = io.Fonts->AddFontFromMemoryCompressedBase85TTF(
|
||||
GetInterRegularCompressedBase85(), 18.0f, &config);
|
||||
ui_font_semibold_ = io.Fonts->AddFontFromMemoryCompressedBase85TTF(
|
||||
GetInterSemiBoldCompressedBase85(), 18.0f, &config);
|
||||
ui_font_bold_ = io.Fonts->AddFontFromMemoryCompressedBase85TTF(
|
||||
GetInterBoldCompressedBase85(), 18.0f, &config);
|
||||
// DARK-ON-LIGHT variants: naive sRGB alpha blending renders dark text on
|
||||
// light panels measurably fatter and softer than the browser (stems 4.94
|
||||
// vs 4.70 px, vertical edge gradient 73 vs 92) - the
|
||||
// browser's text engine gamma-adjusts coverage per polarity. A coverage
|
||||
// multiply < 1 approximates that for dark-on-light; light-on-dark keeps
|
||||
// the plain 1.0 fonts above. Metrics are identical across variants.
|
||||
ImFontConfig config_on_light = config;
|
||||
// 1.0 = currently identical to the plain fonts: the 0.8 coverage-thinning
|
||||
// experiment read as "lighter, not sharper" in playtest. Kept as the
|
||||
// per-polarity tuning knob.
|
||||
config_on_light.RasterizerMultiply = 1.0f;
|
||||
ui_font_on_light_ = io.Fonts->AddFontFromMemoryCompressedBase85TTF(
|
||||
GetInterRegularCompressedBase85(), 18.0f, &config_on_light);
|
||||
ui_font_semibold_on_light_ = io.Fonts->AddFontFromMemoryCompressedBase85TTF(
|
||||
GetInterSemiBoldCompressedBase85(), 18.0f, &config_on_light);
|
||||
}
|
||||
auto add_first_available = [&io](const std::vector<std::string>& paths) -> ImFont* {
|
||||
for (const std::string& path : paths) {
|
||||
|
|
@ -450,6 +479,15 @@ void ImGuiDrawer::SetupFonts() {
|
|||
if (!ui_font_semibold_) {
|
||||
ui_font_semibold_ = ui_font_;
|
||||
}
|
||||
if (!ui_font_bold_) {
|
||||
ui_font_bold_ = ui_font_semibold_;
|
||||
}
|
||||
if (!ui_font_on_light_) {
|
||||
ui_font_on_light_ = ui_font_;
|
||||
}
|
||||
if (!ui_font_semibold_on_light_) {
|
||||
ui_font_semibold_on_light_ = ui_font_semibold_;
|
||||
}
|
||||
REXLOG_INFO("imgui fonts: drawer={} atlas={} count={} ui={} uisb={}", (void*)this,
|
||||
(void*)io.Fonts, io.Fonts->Fonts.Size, (void*)ui_font_, (void*)ui_font_semibold_);
|
||||
}
|
||||
|
|
@ -533,13 +571,21 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) {
|
|||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// Retina: glyphs must rasterize at display pixel density to survive the
|
||||
// logical->physical magnification in RenderDrawLists. Per-frame because the
|
||||
// window can move between displays with different densities.
|
||||
float fb_scale = float(window_->GetDpi()) / float(window_->GetMediumDpi());
|
||||
io.DisplayFramebufferScale = ImVec2(fb_scale, fb_scale);
|
||||
#endif
|
||||
// The UI coordinate space is PHYSICAL pixels, 1:1 with the render target -
|
||||
// no logical->physical magnification anywhere in the UI path. Under a
|
||||
// fractional OS scale (e.g. Windows 150%) a logical coordinate space cannot
|
||||
// be blur-free: the GPU stretch lands half the pixel-snapped positions
|
||||
// between physical pixels, and glyph texels can never all align. Glyphs now
|
||||
// rasterize at their physical size directly, so density compensation
|
||||
// (DisplayFramebufferScale, the old Retina fix) must stay 1.
|
||||
io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f);
|
||||
// Widget-based dialogs (message boxes, wizards, fps overlay) author their
|
||||
// font sizes in logical units - FontScaleDpi keeps their on-screen size
|
||||
// across DPI scales while text rasterizes at physical resolution. Explicit
|
||||
// ImDrawList::AddText sizes (the settings overlay) are unaffected by it.
|
||||
// Per-frame because the window can move between monitors with different
|
||||
// scales.
|
||||
ImGui::GetStyle().FontScaleDpi = float(window_->GetDpi()) / float(window_->GetMediumDpi());
|
||||
|
||||
uint64_t current_frame_time_ticks = rex::chrono::Clock::QueryHostTickCount();
|
||||
io.DeltaTime =
|
||||
|
|
@ -551,9 +597,8 @@ void ImGuiDrawer::Draw(UIDrawContext& ui_draw_context) {
|
|||
}
|
||||
last_frame_time_ticks_ = current_frame_time_ticks;
|
||||
|
||||
float physical_to_logical = float(window_->GetMediumDpi()) / float(window_->GetDpi());
|
||||
io.DisplaySize.x = window_->GetActualPhysicalWidth() * physical_to_logical;
|
||||
io.DisplaySize.y = window_->GetActualPhysicalHeight() * physical_to_logical;
|
||||
io.DisplaySize.x = float(window_->GetActualPhysicalWidth());
|
||||
io.DisplaySize.y = float(window_->GetActualPhysicalHeight());
|
||||
|
||||
ImGui::NewFrame();
|
||||
|
||||
|
|
@ -859,8 +904,9 @@ void ImGuiDrawer::OnKey(KeyEvent& e, bool is_down) {
|
|||
|
||||
void ImGuiDrawer::UpdateMousePosition(float x, float y) {
|
||||
auto& io = GetIO();
|
||||
float physical_to_logical = float(window_->GetMediumDpi()) / float(window_->GetDpi());
|
||||
io.AddMousePosEvent(x * physical_to_logical, y * physical_to_logical);
|
||||
// MouseEvents already carry physical pixels (WindowPointToPhysical) - the
|
||||
// UI coordinate space is physical too, no conversion.
|
||||
io.AddMousePosEvent(x, y);
|
||||
}
|
||||
|
||||
void ImGuiDrawer::SwitchToPhysicalMouseAndUpdateMousePosition(const MouseEvent& e) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <imgui.h>
|
||||
|
|
@ -78,21 +79,41 @@ constexpr float kRepeatDelay = 0.42f;
|
|||
constexpr float kRepeatRate = 0.11f;
|
||||
|
||||
// ---- Palette -------------------------------------------------------------
|
||||
// Frosted-panel color scheme (iterated against
|
||||
// real gameplay captures): teal frosted side panels with white text over the
|
||||
// blurred scene, WHITE setting rows with near-black text, black focused row
|
||||
// with a lime ring, lime section/description bars, magenta steppers/slider.
|
||||
|
||||
constexpr ImU32 kColSelFill = IM_COL32(243, 246, 247, 255);
|
||||
constexpr ImU32 kColSelText = IM_COL32(13, 16, 18, 255);
|
||||
constexpr ImU32 kColPanel = IM_COL32(15, 19, 22, 216);
|
||||
constexpr ImU32 kColPanelHover = IM_COL32(30, 37, 41, 230);
|
||||
constexpr ImU32 kColPanelBorder = IM_COL32(255, 255, 255, 14);
|
||||
constexpr ImU32 kColText = IM_COL32(228, 233, 235, 255);
|
||||
constexpr ImU32 kColTextDim = IM_COL32(148, 157, 161, 255);
|
||||
constexpr ImU32 kColTextFaint = IM_COL32(105, 114, 118, 255);
|
||||
constexpr ImU32 kColAccent = IM_COL32(62, 219, 190, 255);
|
||||
constexpr ImU32 kColAccentDark = IM_COL32(9, 24, 21, 255);
|
||||
constexpr ImU32 kColSelFill = IM_COL32(10, 12, 12, 255);
|
||||
constexpr ImU32 kColSelText = IM_COL32(250, 252, 252, 255);
|
||||
// Rows are OPAQUE (no scene bleed-through) but tuned to the tone the
|
||||
// raw white would render (~235 vs 252) so they don't read as
|
||||
// too bright or too contrasty.
|
||||
constexpr ImU32 kColPanel = IM_COL32(235, 236, 236, 255);
|
||||
constexpr ImU32 kColPanelHover = IM_COL32(248, 250, 250, 255);
|
||||
constexpr ImU32 kColPanelBorder = IM_COL32(0, 0, 0, 26);
|
||||
constexpr ImU32 kColRowText = IM_COL32(15, 17, 18, 255);
|
||||
constexpr ImU32 kColRailPanel = IM_COL32(14, 56, 53, 140);
|
||||
constexpr ImU32 kColRailPanelHover = IM_COL32(24, 90, 86, 166);
|
||||
constexpr ImU32 kColRailBorder = IM_COL32(255, 255, 255, 26);
|
||||
constexpr ImU32 kColText = IM_COL32(235, 242, 241, 255);
|
||||
constexpr ImU32 kColTextDim = IM_COL32(224, 235, 234, 191);
|
||||
constexpr ImU32 kColTextFaint = IM_COL32(150, 158, 158, 255);
|
||||
constexpr ImU32 kColAccent = IM_COL32(213, 235, 10, 255);
|
||||
constexpr ImU32 kColAccentDark = IM_COL32(13, 15, 5, 255);
|
||||
constexpr ImU32 kColInteract = IM_COL32(230, 0, 120, 255);
|
||||
constexpr ImU32 kColInteractHover = IM_COL32(255, 25, 145, 255);
|
||||
constexpr ImU32 kColDanger = IM_COL32(233, 88, 76, 255);
|
||||
constexpr ImU32 kColWarn = IM_COL32(245, 197, 87, 255);
|
||||
constexpr ImU32 kColDescPanel = IM_COL32(11, 15, 17, 220);
|
||||
constexpr ImU32 kColRule = IM_COL32(255, 255, 255, 26);
|
||||
constexpr ImU32 kColDescPanel = IM_COL32(11, 46, 43, 140);
|
||||
constexpr ImU32 kColChevDisabled = IM_COL32(0, 0, 0, 56);
|
||||
// The disabled chevron on the FOCUSED (black) row needs a light variant -
|
||||
// black-alpha disappears there.
|
||||
constexpr ImU32 kColChevDisabledOnDark = IM_COL32(255, 255, 255, 64);
|
||||
constexpr ImU32 kColLegendChip = IM_COL32(238, 240, 240, 255);
|
||||
constexpr ImU32 kColLegendText = IM_COL32(15, 18, 20, 255);
|
||||
constexpr ImU32 kColLegendLabel = IM_COL32(228, 236, 235, 255);
|
||||
constexpr ImU32 kColWhite = IM_COL32(255, 255, 255, 255);
|
||||
|
||||
bool HasCvar(std::string_view name) {
|
||||
return rex::cvar::GetFlagInfo(name) != nullptr;
|
||||
|
|
@ -296,6 +317,13 @@ void SetTearingCvars(bool value) {
|
|||
|
||||
// ---- Small draw helpers ----------------------------------------------------
|
||||
|
||||
// Snap to whole pixels. Fractional origins under bilinear filtering smear
|
||||
// glyph edges and turn 1px rules into soft 2px lines - browsers pixel-snap
|
||||
// text and borders, so snapping to whole pixels keeps them crisp.
|
||||
float Snap(float value) {
|
||||
return std::floor(value + 0.5f);
|
||||
}
|
||||
|
||||
std::string TruncateToWidth(ImFont* font, float size, float max_width, std::string text) {
|
||||
if (font->CalcTextSizeA(size, FLT_MAX, 0.0f, text.c_str()).x <= max_width) {
|
||||
return text;
|
||||
|
|
@ -310,24 +338,157 @@ std::string TruncateToWidth(ImFont* font, float size, float max_width, std::stri
|
|||
void AddTextCentered(ImDrawList* dl, ImFont* font, float size, ImVec2 center, ImU32 col,
|
||||
const char* text) {
|
||||
ImVec2 extent = font->CalcTextSizeA(size, FLT_MAX, 0.0f, text);
|
||||
dl->AddText(font, size, ImVec2(center.x - extent.x * 0.5f, center.y - extent.y * 0.5f), col,
|
||||
dl->AddText(font, size,
|
||||
ImVec2(Snap(center.x - extent.x * 0.5f), Snap(center.y - extent.y * 0.5f)), col,
|
||||
text);
|
||||
}
|
||||
|
||||
// Optical centering for short glyph labels (pad button circles, key chips):
|
||||
// AddTextCentered centers the line box, but a capital's visual ink sits
|
||||
// asymmetrically inside it (descender slack below, bearings on the sides), so
|
||||
// the letter lands ~1px down-right of a small circle's center. Vertical
|
||||
// centers the CAP-HEIGHT BAND taken from 'H'
|
||||
// rather than each glyph's own ink: per-glyph raster boxes round
|
||||
// independently (A has a pointed-apex overshoot, Y doesn't), which lands
|
||||
// adjacent buttons on different subpixel rows - the eye catches that
|
||||
// inconsistency more than any absolute offset. One shared band = identical
|
||||
// placement for every label, overshoots hanging evenly, which is also what
|
||||
// the browser's hinted rasterizer converges to. Horizontal centers the ink
|
||||
// for single glyphs (circles) or the advance box when ink_x is false -
|
||||
// multi-glyph chip labels match the browser's text-box centering that way.
|
||||
// ASCII labels only.
|
||||
void AddTextCenteredCap(ImDrawList* dl, ImFont* font, float size, ImVec2 center, ImU32 col,
|
||||
const char* text, bool ink_x) {
|
||||
ImFontBaked* baked = font->GetFontBaked(size);
|
||||
float pen = 0.0f;
|
||||
float min_x = FLT_MAX, max_x = -FLT_MAX;
|
||||
for (const char* p = text; *p; ++p) {
|
||||
const ImFontGlyph* glyph = baked->FindGlyph(ImWchar(uint8_t(*p)));
|
||||
if (!glyph) {
|
||||
continue;
|
||||
}
|
||||
if (glyph->Visible) {
|
||||
min_x = std::min(min_x, pen + glyph->X0);
|
||||
max_x = std::max(max_x, pen + glyph->X1);
|
||||
}
|
||||
pen += glyph->AdvanceX;
|
||||
}
|
||||
if (min_x > max_x) {
|
||||
return; // no visible ink
|
||||
}
|
||||
float band_top = 0.0f;
|
||||
float band_bottom = size;
|
||||
if (const ImFontGlyph* cap = baked->FindGlyphNoFallback(ImWchar('H'))) {
|
||||
band_top = cap->Y0;
|
||||
band_bottom = cap->Y1;
|
||||
}
|
||||
const float x = ink_x ? center.x - (min_x + max_x) * 0.5f : center.x - pen * 0.5f;
|
||||
// Round-half-DOWN for the vertical position: the cap band regularly has a
|
||||
// half-integer center (e.g. 18px band in a 52px circle), and Snap's
|
||||
// round-half-up resolved every tie downward - measured exactly 1px low
|
||||
// versus the browser, which resolves the same tie upward (the typographic
|
||||
// convention: when a glyph can't center exactly, err high).
|
||||
const float y = center.y - (band_top + band_bottom) * 0.5f;
|
||||
dl->AddText(font, size, ImVec2(Snap(x), std::ceil(y - 0.5f)), col, text);
|
||||
}
|
||||
|
||||
void AddTextVCentered(ImDrawList* dl, ImFont* font, float size, float x, float center_y,
|
||||
ImU32 col, const char* text) {
|
||||
ImVec2 extent = font->CalcTextSizeA(size, FLT_MAX, 0.0f, text);
|
||||
dl->AddText(font, size, ImVec2(x, center_y - extent.y * 0.5f), col, text);
|
||||
dl->AddText(font, size, ImVec2(Snap(x), Snap(center_y - extent.y * 0.5f)), col, text);
|
||||
}
|
||||
|
||||
// Justified paragraph (browser text-align: justify): wraps words to wrap_w
|
||||
// and stretches the word gaps of every full line so both edges align; the
|
||||
// last line of the paragraph stays ragged-left. Returns the height consumed.
|
||||
float AddTextJustified(ImDrawList* dl, ImFont* font, float size, ImVec2 pos, float wrap_w,
|
||||
ImU32 col, const char* text) {
|
||||
std::vector<std::string_view> words;
|
||||
for (const char* p = text; *p;) {
|
||||
while (*p == ' ') {
|
||||
++p;
|
||||
}
|
||||
const char* start = p;
|
||||
while (*p && *p != ' ') {
|
||||
++p;
|
||||
}
|
||||
if (p > start) {
|
||||
words.emplace_back(start, size_t(p - start));
|
||||
}
|
||||
}
|
||||
if (words.empty()) {
|
||||
return 0.0f;
|
||||
}
|
||||
const float space_w = font->CalcTextSizeA(size, FLT_MAX, 0.0f, " ").x;
|
||||
auto word_w = [&](std::string_view w) {
|
||||
return font->CalcTextSizeA(size, FLT_MAX, 0.0f, w.data(), w.data() + w.size()).x;
|
||||
};
|
||||
float y = pos.y;
|
||||
size_t i = 0;
|
||||
while (i < words.size()) {
|
||||
float line_w = word_w(words[i]);
|
||||
size_t j = i + 1;
|
||||
while (j < words.size()) {
|
||||
const float w = word_w(words[j]);
|
||||
if (line_w + space_w + w > wrap_w) {
|
||||
break;
|
||||
}
|
||||
line_w += space_w + w;
|
||||
++j;
|
||||
}
|
||||
const bool last_line = j == words.size();
|
||||
float gap = space_w;
|
||||
if (!last_line && j - i > 1) {
|
||||
const float words_only = line_w - space_w * float(j - i - 1);
|
||||
const float stretched = (wrap_w - words_only) / float(j - i - 1);
|
||||
// Conditional justify: only stretch when the gaps stay modest -
|
||||
// beyond ~1.6x a normal space the stretched line reads worse than a
|
||||
// ragged one (the "big spaces" objection to naive justification).
|
||||
if (stretched <= space_w * 1.6f) {
|
||||
gap = stretched;
|
||||
}
|
||||
}
|
||||
float x = pos.x;
|
||||
for (size_t k = i; k < j; ++k) {
|
||||
dl->AddText(font, size, ImVec2(Snap(x), Snap(y)), col, words[k].data(),
|
||||
words[k].data() + words[k].size());
|
||||
x += word_w(words[k]) + gap;
|
||||
}
|
||||
y += size;
|
||||
i = j;
|
||||
}
|
||||
return y - pos.y;
|
||||
}
|
||||
|
||||
// Focused-item highlight: rounded black fill with a lime ring and a thicker
|
||||
// black outer edge, both drawn OUTSIDE the box (stacked
|
||||
// box-shadows). Total ring = 6*s beyond the rect on every side; callers keep
|
||||
// a matching margin in their clip rects.
|
||||
void DrawFocusHighlight(ImDrawList* dl, ImVec2 p_min, ImVec2 p_max, float s) {
|
||||
const float radius = 6.0f * s;
|
||||
dl->AddRectFilled(p_min, p_max, kColSelFill, radius);
|
||||
// Lime band: box edge -> +2*s (stroke centered at +1*s).
|
||||
dl->AddRect(ImVec2(p_min.x - 1.0f * s, p_min.y - 1.0f * s),
|
||||
ImVec2(p_max.x + 1.0f * s, p_max.y + 1.0f * s), kColAccent, radius + 1.0f * s, 0,
|
||||
2.0f * s);
|
||||
// Black outer edge: +2*s -> +6*s (stroke centered at +4*s).
|
||||
dl->AddRect(ImVec2(p_min.x - 4.0f * s, p_min.y - 4.0f * s),
|
||||
ImVec2(p_max.x + 4.0f * s, p_max.y + 4.0f * s), kColSelFill, radius + 4.0f * s, 0,
|
||||
4.0f * s);
|
||||
}
|
||||
|
||||
// Stepper chevron: a triangle in a circle.
|
||||
void DrawChevron(ImDrawList* dl, ImVec2 center, float radius, bool points_left, ImU32 circle_col,
|
||||
ImU32 tri_col, bool filled) {
|
||||
center = ImVec2(Snap(center.x), Snap(center.y));
|
||||
if (filled) {
|
||||
dl->AddCircleFilled(center, radius, circle_col, 24);
|
||||
} else {
|
||||
dl->AddCircle(center, radius, circle_col, 24, 1.5f);
|
||||
}
|
||||
// Tip-anchored triangle (tip reaches further from center than the back
|
||||
// edge). A bbox-centered variant was tried and looked worse in playtest -
|
||||
// keep this geometry.
|
||||
float t = radius * 0.42f;
|
||||
float dir = points_left ? -1.0f : 1.0f;
|
||||
ImVec2 tip(center.x + dir * t, center.y);
|
||||
|
|
@ -447,6 +608,7 @@ void SimpleSettingsDialog::Show() {
|
|||
highlight_anim_y_ = -1.0f;
|
||||
rail_anim_y_ = -1.0f;
|
||||
prev_pad_buttons_ = 0xFFFF; // swallow buttons already held at open
|
||||
just_shown_ = true; // swallow the stale cursor delta too
|
||||
}
|
||||
|
||||
void SimpleSettingsDialog::LoadSettingsFromCvars() {
|
||||
|
|
@ -591,7 +753,12 @@ void SimpleSettingsDialog::BuildRows(std::vector<RowSpec>& rows, int category) {
|
|||
rows.clear();
|
||||
const bool pending = HasSettingsChanges();
|
||||
|
||||
// Section bars only SEPARATE groups - a category's first group
|
||||
// starts directly with its rows, so a leading header is dropped.
|
||||
auto header = [&rows](const char* label) {
|
||||
if (rows.empty()) {
|
||||
return;
|
||||
}
|
||||
RowSpec row;
|
||||
row.kind = RowSpec::kHeader;
|
||||
row.label = label;
|
||||
|
|
@ -600,7 +767,7 @@ void SimpleSettingsDialog::BuildRows(std::vector<RowSpec>& rows, int category) {
|
|||
|
||||
switch (category) {
|
||||
case 0: { // Video
|
||||
header("DISPLAY");
|
||||
header("Display");
|
||||
if (!device_list_.device_names.empty() && HasCvar(device_list_.cvar_name)) {
|
||||
RowSpec row;
|
||||
row.kind = RowSpec::kEnum;
|
||||
|
|
@ -711,7 +878,7 @@ void SimpleSettingsDialog::BuildRows(std::vector<RowSpec>& rows, int category) {
|
|||
rows.push_back(std::move(row));
|
||||
}
|
||||
if (HasFieldOfViewCvar()) {
|
||||
header("GAMEPLAY");
|
||||
header("Gameplay");
|
||||
RowSpec row;
|
||||
row.kind = RowSpec::kSlider;
|
||||
row.label = "Field of View";
|
||||
|
|
@ -735,7 +902,7 @@ void SimpleSettingsDialog::BuildRows(std::vector<RowSpec>& rows, int category) {
|
|||
break;
|
||||
}
|
||||
case 1: { // Controls
|
||||
header("MOUSE & KEYBOARD");
|
||||
header("Mouse & Keyboard");
|
||||
{
|
||||
RowSpec row;
|
||||
row.kind = RowSpec::kEnum;
|
||||
|
|
@ -763,7 +930,7 @@ void SimpleSettingsDialog::BuildRows(std::vector<RowSpec>& rows, int category) {
|
|||
break;
|
||||
}
|
||||
case 2: { // Profile
|
||||
header("LOCAL PROFILE");
|
||||
header("Local Profile");
|
||||
{
|
||||
RowSpec row;
|
||||
row.kind = RowSpec::kEnum;
|
||||
|
|
@ -810,7 +977,7 @@ void SimpleSettingsDialog::BuildRows(std::vector<RowSpec>& rows, int category) {
|
|||
break;
|
||||
}
|
||||
case 3: { // System
|
||||
header("SESSION");
|
||||
header("Session");
|
||||
{
|
||||
RowSpec row;
|
||||
row.kind = RowSpec::kAction;
|
||||
|
|
@ -990,10 +1157,34 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
|
||||
ImFont* font = imgui_drawer()->ui_font() ? imgui_drawer()->ui_font() : ImGui::GetFont();
|
||||
ImFont* bold = imgui_drawer()->ui_font_semibold() ? imgui_drawer()->ui_font_semibold() : font;
|
||||
// Coverage-thinned variants for dark text on light panels (rows, section
|
||||
// bars, legend chips) - matches the browser's per-polarity text gamma.
|
||||
ImFont* bold_ol = imgui_drawer()->ui_font_semibold_on_light()
|
||||
? imgui_drawer()->ui_font_semibold_on_light()
|
||||
: bold;
|
||||
|
||||
// Uniform scale: design metrics are authored for 1080p.
|
||||
const float s = std::clamp(io.DisplaySize.y / 1080.0f, 0.6f, 3.0f);
|
||||
|
||||
// Quantize font sizes so the EM (size * upm / (hhea ascent - descent) -
|
||||
// the browser's font-size) lands on whole pixels. A fractional em renders
|
||||
// measurably wider letter spacing than the browser reference, which snaps
|
||||
// css font sizes to the device grid (+1.3% run width at 4K, 370px vs
|
||||
// 365px on "Vertical Synchronisation").
|
||||
constexpr float kEmPerSize = 2048.0f / 2478.0f; // Inter upm / (asc - desc)
|
||||
auto font_px = [](float size) {
|
||||
return std::round(size * kEmPerSize) / kEmPerSize;
|
||||
};
|
||||
|
||||
if (just_shown_) {
|
||||
// Ignore the pre-open cursor position delta (opening warps/reveals the
|
||||
// cursor): it otherwise reads as motion and steals focus from the rail
|
||||
// to whatever row happens to sit under the cursor.
|
||||
mouse_x_ = io.MousePos.x;
|
||||
mouse_y_ = io.MousePos.y;
|
||||
just_shown_ = false;
|
||||
}
|
||||
|
||||
// ---- Input ----
|
||||
NavIntents in = GatherInput(io);
|
||||
if (editing_text_) {
|
||||
|
|
@ -1067,15 +1258,17 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
row.on_enum_change(value);
|
||||
}
|
||||
};
|
||||
// Range semantics: directional stepping (dpad/arrows/chevrons)
|
||||
// CLAMPS at the ends - the matching chevron greys out there - while
|
||||
// activation (A/Enter/row click) cycles with wrap-around.
|
||||
auto step_row = [&](RowSpec& row, int dir) {
|
||||
if (row.kind == RowSpec::kEnum) {
|
||||
int count = static_cast<int>(row.options.size());
|
||||
if (count <= 0) {
|
||||
return;
|
||||
}
|
||||
int value = ((enum_value(row) % count) + count) % count;
|
||||
value = ((value + dir) % count + count) % count;
|
||||
set_enum_value(row, value);
|
||||
int value = std::clamp(enum_value(row), 0, count - 1);
|
||||
set_enum_value(row, std::clamp(value + dir, 0, count - 1));
|
||||
} else if (row.kind == RowSpec::kSlider && row.value) {
|
||||
*row.value = std::clamp(*row.value + dir * row.step, row.min, row.max);
|
||||
if (row.on_value_change) {
|
||||
|
|
@ -1089,9 +1282,14 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
return;
|
||||
}
|
||||
switch (row.kind) {
|
||||
case RowSpec::kEnum:
|
||||
step_row(row, 1);
|
||||
case RowSpec::kEnum: {
|
||||
int count = static_cast<int>(row.options.size());
|
||||
if (count > 0) {
|
||||
int value = std::clamp(enum_value(row), 0, count - 1);
|
||||
set_enum_value(row, (value + 1) % count);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RowSpec::kAction:
|
||||
if (row.action) {
|
||||
row.action();
|
||||
|
|
@ -1169,33 +1367,39 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
const bool pending = HasSettingsChanges();
|
||||
|
||||
// ---- Layout ----
|
||||
const float margin_x = std::max(56.0f * s, io.DisplaySize.x * 0.05f);
|
||||
const float title_y = io.DisplaySize.y * 0.085f;
|
||||
const float title_size = 42.0f * s;
|
||||
const float columns_y = title_y + 74.0f * s;
|
||||
const float rail_w = 250.0f * s;
|
||||
const float desc_w = 330.0f * s;
|
||||
const float col_gap = 14.0f * s;
|
||||
const float footer_h = 96.0f * s;
|
||||
const float content_bottom = io.DisplaySize.y - footer_h - 18.0f * s;
|
||||
// Positional metrics are snapped to whole pixels (see Snap).
|
||||
const float margin_x = Snap(std::max(56.0f * s, io.DisplaySize.x * 0.05f));
|
||||
const float title_y = Snap(io.DisplaySize.y * 0.13f);
|
||||
const float title_size = font_px(42.0f * s);
|
||||
const float columns_y = Snap(title_y + 74.0f * s);
|
||||
// Rail and description columns are equal-width.
|
||||
const float rail_w = Snap(300.0f * s);
|
||||
const float desc_w = Snap(300.0f * s);
|
||||
const float col_gap = Snap(14.0f * s);
|
||||
const float footer_h = Snap(96.0f * s);
|
||||
const float content_bottom = Snap(io.DisplaySize.y - footer_h - 18.0f * s);
|
||||
float content_w = io.DisplaySize.x - 2.0f * margin_x - rail_w - desc_w - 2.0f * col_gap;
|
||||
content_w = std::clamp(content_w, 300.0f * s, 800.0f * s);
|
||||
content_w = Snap(std::clamp(content_w, 300.0f * s, 800.0f * s));
|
||||
// The column widths are capped, so on wide displays the block would hug the
|
||||
// left margin - center the whole menu instead.
|
||||
const float menu_total_w = rail_w + content_w + desc_w + 2.0f * col_gap;
|
||||
const float rail_x = std::max(margin_x, (io.DisplaySize.x - menu_total_w) * 0.5f);
|
||||
const float rail_x = Snap(std::max(margin_x, (io.DisplaySize.x - menu_total_w) * 0.5f));
|
||||
const float content_x = rail_x + rail_w + col_gap;
|
||||
const float desc_x = content_x + content_w + col_gap;
|
||||
const float menu_right = desc_x + desc_w;
|
||||
|
||||
const float row_h = 52.0f * s;
|
||||
const float header_h = 36.0f * s;
|
||||
const float row_gap = 6.0f * s;
|
||||
const float rail_item_h = 52.0f * s;
|
||||
const float label_size = 19.0f * s;
|
||||
const float value_size = 19.0f * s;
|
||||
const float header_size = 14.5f * s;
|
||||
const float desc_size = 16.5f * s;
|
||||
const float row_h = Snap(52.0f * s);
|
||||
const float header_h = row_h; // section bars match setting rows
|
||||
// Gap is 1 design px tighter than the 6*s focus ring, so the ring slightly
|
||||
// overlaps neighbours - it draws above them.
|
||||
const float row_gap = Snap(5.0f * s);
|
||||
const float rail_item_h = Snap(52.0f * s);
|
||||
const float label_size = font_px(19.0f * s);
|
||||
const float value_size = font_px(19.0f * s);
|
||||
const float desc_size = font_px(18.0f * s);
|
||||
// Description panel height - also anchors Close Game's bottom edge.
|
||||
const float desc_panel_h =
|
||||
Snap(std::min(content_bottom - columns_y, 320.0f * s));
|
||||
|
||||
const ImVec2 mouse = io.MousePos;
|
||||
const bool mouse_moved =
|
||||
|
|
@ -1224,56 +1428,27 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
}
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
// ---- Backdrop scrim: even dim plus a stronger wash behind the columns ----
|
||||
dl->AddRectFilledMultiColor(ImVec2(0.0f, 0.0f), io.DisplaySize, IM_COL32(6, 9, 11, 150),
|
||||
IM_COL32(6, 9, 11, 150), IM_COL32(3, 5, 7, 215),
|
||||
IM_COL32(3, 5, 7, 215));
|
||||
{
|
||||
// Extra wash behind the menu block, fading out symmetrically at both
|
||||
// sides so the (centered) block reads against busy game scenes.
|
||||
const float wash_pad = 110.0f * s;
|
||||
const ImU32 wash = IM_COL32(4, 6, 8, 140);
|
||||
const ImU32 wash_clear = IM_COL32(4, 6, 8, 0);
|
||||
dl->AddRectFilledMultiColor(ImVec2(rail_x - wash_pad, 0.0f),
|
||||
ImVec2(rail_x, io.DisplaySize.y), wash_clear, wash, wash,
|
||||
wash_clear);
|
||||
dl->AddRectFilled(ImVec2(rail_x, 0.0f), ImVec2(menu_right, io.DisplaySize.y), wash);
|
||||
dl->AddRectFilledMultiColor(ImVec2(menu_right, 0.0f),
|
||||
ImVec2(menu_right + wash_pad, io.DisplaySize.y), wash,
|
||||
wash_clear, wash_clear, wash);
|
||||
}
|
||||
// ---- Backdrop: uniform slightly-dark tint over the whole screen. The
|
||||
// native renderer blurs the finished frame while the menu is open
|
||||
// (SetSettingsMenuBlur), so the tint lands on a frosted scene.
|
||||
dl->AddRectFilled(ImVec2(0.0f, 0.0f), io.DisplaySize, IM_COL32(6, 9, 11, 133));
|
||||
|
||||
// ---- Title ----
|
||||
dl->AddRectFilled(ImVec2(rail_x, title_y + 3.0f * s),
|
||||
ImVec2(rail_x + 6.0f * s, title_y + title_size - 3.0f * s), kColAccent);
|
||||
dl->AddText(bold, title_size, ImVec2(rail_x + 18.0f * s, title_y), kColText, "Settings");
|
||||
{
|
||||
const char* cat_name = kCategories[category_].name;
|
||||
char crumb[64];
|
||||
std::snprintf(crumb, sizeof(crumb), "/ %s", cat_name);
|
||||
ImVec2 title_extent = bold->CalcTextSizeA(title_size, FLT_MAX, 0.0f, "Settings");
|
||||
dl->AddText(font, 20.0f * s,
|
||||
ImVec2(rail_x + 18.0f * s + title_extent.x + 16.0f * s,
|
||||
title_y + title_size - 26.0f * s),
|
||||
kColTextDim, crumb);
|
||||
}
|
||||
dl->AddText(bold, title_size, ImVec2(Snap(rail_x), title_y), kColText, "Settings");
|
||||
if (pending) {
|
||||
const char* chip_text = "RESTART REQUIRED TO APPLY";
|
||||
float chip_size = 14.0f * s;
|
||||
float chip_size = font_px(14.0f * s);
|
||||
ImVec2 extent = bold->CalcTextSizeA(chip_size, FLT_MAX, 0.0f, chip_text);
|
||||
float chip_pad = 10.0f * s;
|
||||
float chip_x1 = menu_right;
|
||||
float chip_x0 = chip_x1 - extent.x - 2.0f * chip_pad;
|
||||
float chip_y0 = title_y + title_size * 0.5f - extent.y * 0.5f - 6.0f * s;
|
||||
float chip_y1 = chip_y0 + extent.y + 12.0f * s;
|
||||
float chip_x0 = Snap(chip_x1 - extent.x - 2.0f * chip_pad);
|
||||
float chip_y0 = Snap(title_y + title_size * 0.5f - extent.y * 0.5f - 6.0f * s);
|
||||
float chip_y1 = Snap(chip_y0 + extent.y + 12.0f * s);
|
||||
dl->AddRect(ImVec2(chip_x0, chip_y0), ImVec2(chip_x1, chip_y1), kColWarn, 0.0f, 0,
|
||||
1.5f);
|
||||
dl->AddText(bold, chip_size, ImVec2(chip_x0 + chip_pad, chip_y0 + 6.0f * s), kColWarn,
|
||||
chip_text);
|
||||
dl->AddText(bold, chip_size, ImVec2(Snap(chip_x0 + chip_pad), Snap(chip_y0 + 6.0f * s)),
|
||||
kColWarn, chip_text);
|
||||
}
|
||||
dl->AddRectFilled(ImVec2(rail_x, columns_y - 12.0f * s),
|
||||
ImVec2(menu_right, columns_y - 12.0f * s + 1.0f), kColRule);
|
||||
|
||||
// ---- Left rail ----
|
||||
float rail_focus_target = -1.0f;
|
||||
for (int i = 0; i < category_count; ++i) {
|
||||
|
|
@ -1292,30 +1467,25 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
if (is_current && zone_ == FocusZone::kRail && rail_sel_ == i) {
|
||||
rail_focus_target = y0;
|
||||
}
|
||||
ImU32 bg = is_current ? kColPanelHover : (hovered ? kColPanelHover : kColPanel);
|
||||
// The rounded focus fill covers this item while rail-focused; skip the
|
||||
// square panel underneath so its corners don't poke past the radius.
|
||||
bool rail_focused = zone_ == FocusZone::kRail && rail_sel_ == i;
|
||||
if (!rail_focused) {
|
||||
ImU32 bg = is_current ? kColSelFill : (hovered ? kColRailPanelHover : kColRailPanel);
|
||||
dl->AddRectFilled(ImVec2(rail_x, y0), ImVec2(rail_x + rail_w, y1), bg, 0.0f);
|
||||
dl->AddRect(ImVec2(rail_x, y0), ImVec2(rail_x + rail_w, y1), kColPanelBorder, 0.0f);
|
||||
// Static accent bar only while focus lives in the content column; while
|
||||
// navigating the rail the bar rides the sliding highlight below so both
|
||||
// move at the same rate.
|
||||
if (is_current && zone_ == FocusZone::kContent) {
|
||||
dl->AddRectFilled(ImVec2(rail_x, y0), ImVec2(rail_x + 4.0f * s, y1), kColAccent,
|
||||
0.0f, ImDrawFlags_RoundCornersLeft);
|
||||
dl->AddRect(ImVec2(rail_x, y0), ImVec2(rail_x + rail_w, y1), kColRailBorder, 0.0f);
|
||||
}
|
||||
}
|
||||
// Focused rail item: sliding white fill (with the accent bar attached to
|
||||
// its left edge) drawn over the panels, then labels.
|
||||
// Focused rail item: sliding black fill with the lime focus ring, drawn
|
||||
// over the panels, then labels.
|
||||
if (rail_focus_target >= 0.0f) {
|
||||
if (rail_anim_y_ < 0.0f || std::abs(rail_anim_y_ - rail_focus_target) > 160.0f * s) {
|
||||
rail_anim_y_ = rail_focus_target;
|
||||
}
|
||||
rail_anim_y_ += (rail_focus_target - rail_anim_y_) * std::min(1.0f, io.DeltaTime * 22.0f);
|
||||
dl->AddRectFilled(ImVec2(rail_x, rail_anim_y_),
|
||||
ImVec2(rail_x + rail_w, rail_anim_y_ + rail_item_h), kColSelFill,
|
||||
0.0f);
|
||||
dl->AddRectFilled(ImVec2(rail_x, rail_anim_y_),
|
||||
ImVec2(rail_x + 4.0f * s, rail_anim_y_ + rail_item_h), kColAccent,
|
||||
0.0f, ImDrawFlags_RoundCornersLeft);
|
||||
const float fill_y = Snap(rail_anim_y_);
|
||||
DrawFocusHighlight(dl, ImVec2(rail_x, fill_y),
|
||||
ImVec2(rail_x + rail_w, fill_y + rail_item_h), s);
|
||||
} else {
|
||||
rail_anim_y_ = -1.0f;
|
||||
}
|
||||
|
|
@ -1324,7 +1494,7 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
bool is_current = category_ == i;
|
||||
bool focused = is_current && zone_ == FocusZone::kRail && rail_sel_ == i;
|
||||
ImU32 text_col = focused ? kColSelText : (is_current ? kColText : kColTextDim);
|
||||
AddTextVCentered(dl, focused || is_current ? bold : font, label_size, rail_x + 20.0f * s,
|
||||
AddTextVCentered(dl, bold, label_size, rail_x + 20.0f * s,
|
||||
y0 + rail_item_h * 0.5f, text_col, kCategories[i].name);
|
||||
if (is_current && zone_ == FocusZone::kContent) {
|
||||
// Arrow hinting that focus lives in the content column.
|
||||
|
|
@ -1337,7 +1507,8 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
|
||||
// Close Game entry: below the categories, set apart by an extra gap.
|
||||
{
|
||||
float y0 = columns_y + category_count * (rail_item_h + row_gap) + 26.0f * s;
|
||||
// Bottom edge aligned with the description panel's bottom.
|
||||
float y0 = Snap(columns_y + desc_panel_h - rail_item_h);
|
||||
float y1 = y0 + rail_item_h;
|
||||
bool focused = zone_ == FocusZone::kRail && rail_sel_ == quit_rail_index;
|
||||
bool hovered = mouse_in(rail_x, y0, rail_x + rail_w, y1);
|
||||
|
|
@ -1345,14 +1516,10 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
Hide();
|
||||
close_game_();
|
||||
}
|
||||
ImU32 bg = focused ? kColDanger : (hovered ? kColPanelHover : kColPanel);
|
||||
ImU32 bg = focused ? kColDanger : (hovered ? kColRailPanelHover : kColRailPanel);
|
||||
dl->AddRectFilled(ImVec2(rail_x, y0), ImVec2(rail_x + rail_w, y1), bg, 0.0f);
|
||||
dl->AddRect(ImVec2(rail_x, y0), ImVec2(rail_x + rail_w, y1),
|
||||
focused ? kColDanger : kColPanelBorder, 0.0f);
|
||||
if (!focused) {
|
||||
dl->AddRectFilled(ImVec2(rail_x, y0), ImVec2(rail_x + 4.0f * s, y1), kColDanger,
|
||||
0.0f, ImDrawFlags_RoundCornersLeft);
|
||||
}
|
||||
focused ? kColDanger : kColRailBorder, 0.0f);
|
||||
AddTextVCentered(dl, bold, label_size, rail_x + 20.0f * s, (y0 + y1) * 0.5f,
|
||||
focused ? IM_COL32(255, 250, 249, 255) : kColDanger, "Close Game");
|
||||
}
|
||||
|
|
@ -1389,9 +1556,13 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
}
|
||||
}
|
||||
content_scroll_ = std::clamp(content_scroll_, 0.0f, max_scroll);
|
||||
const float scroll = Snap(content_scroll_);
|
||||
|
||||
dl->PushClipRect(ImVec2(content_x, columns_y - 2.0f), ImVec2(content_x + content_w + 1.0f,
|
||||
content_bottom),
|
||||
// Clip window padded by the focus-ring extent (6*s) so the ring can sit
|
||||
// fully OUTSIDE the focused row without being cut off at the edges.
|
||||
const float ring_pad = 6.0f * s;
|
||||
dl->PushClipRect(ImVec2(content_x - ring_pad, columns_y - ring_pad),
|
||||
ImVec2(content_x + content_w + ring_pad + 1.0f, content_bottom + ring_pad),
|
||||
true);
|
||||
|
||||
// Sliding selection highlight (white fill under the focused row).
|
||||
|
|
@ -1409,14 +1580,16 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
const float value_w = std::min(330.0f * s, content_w * 0.46f);
|
||||
const float chevron_r = 13.0f * s;
|
||||
|
||||
// Backgrounds first (so the sliding highlight can sit above them), then
|
||||
// per-row content.
|
||||
// Backgrounds first, then per-row content in TWO phases around the sliding
|
||||
// highlight: non-focused content below it, the focused row's content above
|
||||
// it - the ring overlaps neighbours (gap is 1 design px tighter than the
|
||||
// ring) and must read as the forefront element.
|
||||
for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
|
||||
const RowSpec& row = rows[i];
|
||||
if (row.kind == RowSpec::kHeader) {
|
||||
continue;
|
||||
}
|
||||
float y0 = columns_y + row_y[i] - content_scroll_;
|
||||
float y0 = columns_y + Snap(row_y[i]) - scroll;
|
||||
float y1 = y0 + row_hgt[i];
|
||||
if (y1 < columns_y || y0 > content_bottom) {
|
||||
continue;
|
||||
|
|
@ -1426,49 +1599,46 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
zone_ = FocusZone::kContent;
|
||||
row_index_ = i;
|
||||
}
|
||||
// The rounded highlight covers the focused row's panel entirely; skip it
|
||||
// so its square corners don't poke past the radius.
|
||||
if (content_focus && row_index_ == i) {
|
||||
continue;
|
||||
}
|
||||
ImU32 bg = hovered && row.enabled ? kColPanelHover : kColPanel;
|
||||
dl->AddRectFilled(ImVec2(content_x, y0), ImVec2(content_x + content_w, y1), bg, 0.0f);
|
||||
dl->AddRect(ImVec2(content_x, y0), ImVec2(content_x + content_w, y1), kColPanelBorder,
|
||||
0.0f);
|
||||
}
|
||||
if (content_focus && highlight_anim_y_ >= 0.0f) {
|
||||
float y0 = columns_y + highlight_anim_y_ - content_scroll_;
|
||||
dl->AddRectFilled(ImVec2(content_x, y0), ImVec2(content_x + content_w, y0 + row_h),
|
||||
kColSelFill, 0.0f);
|
||||
dl->AddRect(ImVec2(content_x - 1.0f, y0 - 1.0f),
|
||||
ImVec2(content_x + content_w + 1.0f, y0 + row_h + 1.0f), kColAccent, 0.0f,
|
||||
0, 2.0f * s);
|
||||
}
|
||||
|
||||
for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
|
||||
auto draw_row_content = [&](int i) {
|
||||
RowSpec& row = rows[i];
|
||||
float y0 = columns_y + row_y[i] - content_scroll_;
|
||||
float y0 = columns_y + Snap(row_y[i]) - scroll;
|
||||
float y1 = y0 + row_hgt[i];
|
||||
if (y1 < columns_y || y0 > content_bottom) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
float cy = (y0 + y1) * 0.5f;
|
||||
|
||||
if (row.kind == RowSpec::kHeader) {
|
||||
dl->AddText(bold, header_size, ImVec2(content_x + 2.0f * s, y0 + 8.0f * s), kColAccent,
|
||||
// Section header: solid accent bar spanning the content
|
||||
// column, dark bold text, label column-aligned with the row labels.
|
||||
dl->AddRectFilled(ImVec2(content_x, y0), ImVec2(content_x + content_w, y1), kColAccent,
|
||||
0.0f);
|
||||
AddTextVCentered(dl, bold_ol, label_size, content_x + 18.0f * s, cy, kColAccentDark,
|
||||
row.label);
|
||||
ImVec2 extent = bold->CalcTextSizeA(header_size, FLT_MAX, 0.0f, row.label);
|
||||
dl->AddRectFilled(ImVec2(content_x + extent.x + 14.0f * s, y0 + 8.0f * s + extent.y * 0.5f),
|
||||
ImVec2(content_x + content_w, y0 + 8.0f * s + extent.y * 0.5f + 1.0f),
|
||||
kColRule);
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
const bool focused = content_focus && row_index_ == i;
|
||||
const bool hovered = mouse_in(content_x, y0, content_x + content_w, y1);
|
||||
ImFont* row_bold = focused ? bold : bold_ol; // polarity-matched weight
|
||||
ImU32 label_col = focused ? kColSelText
|
||||
: (!row.enabled ? kColTextFaint
|
||||
: (row.danger ? kColDanger : kColText));
|
||||
ImU32 value_col = focused ? kColSelText : (row.enabled ? kColText : kColTextFaint);
|
||||
ImU32 dim_col = focused ? IM_COL32(60, 70, 74, 255) : kColTextDim;
|
||||
: (row.danger ? kColDanger : kColRowText));
|
||||
ImU32 value_col = focused ? kColSelText : (row.enabled ? kColRowText : kColTextFaint);
|
||||
|
||||
// Label.
|
||||
AddTextVCentered(dl, font, label_size, content_x + 18.0f * s, cy, label_col, row.label);
|
||||
AddTextVCentered(dl, row_bold, label_size, content_x + 18.0f * s, cy, label_col, row.label);
|
||||
|
||||
// Value area on the right side of the row.
|
||||
float vx1 = content_x + content_w - 12.0f * s;
|
||||
|
|
@ -1480,26 +1650,31 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
int value = count > 0 ? std::clamp(enum_value(row), 0, count - 1) : 0;
|
||||
ImVec2 left_center(vx0 + chevron_r, cy);
|
||||
ImVec2 right_center(vx1 - chevron_r, cy);
|
||||
bool left_hover = row.enabled &&
|
||||
// Magenta filled stepper circles with white triangles;
|
||||
// a circle greys out (and stops responding) at its end of the range.
|
||||
const bool can_left = row.enabled && value > 0;
|
||||
const bool can_right = row.enabled && value < count - 1;
|
||||
bool left_hover = can_left &&
|
||||
mouse_in(left_center.x - chevron_r - 4.0f, cy - chevron_r - 4.0f,
|
||||
left_center.x + chevron_r + 4.0f, cy + chevron_r + 4.0f);
|
||||
bool right_hover = row.enabled &&
|
||||
bool right_hover = can_right &&
|
||||
mouse_in(right_center.x - chevron_r - 4.0f, cy - chevron_r - 4.0f,
|
||||
right_center.x + chevron_r + 4.0f, cy + chevron_r + 4.0f);
|
||||
ImU32 chev_circle = focused ? kColAccent : IM_COL32(255, 255, 255, 36);
|
||||
ImU32 chev_tri = focused ? kColAccentDark : kColTextDim;
|
||||
const ImU32 chev_disabled = focused ? kColChevDisabledOnDark : kColChevDisabled;
|
||||
DrawChevron(dl, left_center, chevron_r, true,
|
||||
left_hover ? kColAccent : chev_circle,
|
||||
left_hover ? kColAccentDark : chev_tri, focused || left_hover);
|
||||
can_left ? (left_hover ? kColInteractHover : kColInteract)
|
||||
: chev_disabled,
|
||||
kColWhite, true);
|
||||
DrawChevron(dl, right_center, chevron_r, false,
|
||||
right_hover ? kColAccent : chev_circle,
|
||||
right_hover ? kColAccentDark : chev_tri, focused || right_hover);
|
||||
can_right ? (right_hover ? kColInteractHover : kColInteract)
|
||||
: chev_disabled,
|
||||
kColWhite, true);
|
||||
if (count > 0) {
|
||||
float text_max_w = (right_center.x - chevron_r) - (left_center.x + chevron_r) -
|
||||
16.0f * s;
|
||||
std::string text =
|
||||
TruncateToWidth(bold, value_size, text_max_w, row.options[value]);
|
||||
AddTextCentered(dl, bold, value_size,
|
||||
TruncateToWidth(row_bold, value_size, text_max_w, row.options[value]);
|
||||
AddTextCentered(dl, row_bold, value_size,
|
||||
ImVec2((left_center.x + right_center.x) * 0.5f, cy), value_col,
|
||||
text.c_str());
|
||||
}
|
||||
|
|
@ -1509,7 +1684,7 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
} else if (right_hover) {
|
||||
step_row(row, 1);
|
||||
} else if (hovered) {
|
||||
step_row(row, 1);
|
||||
activate_row(row); // row-body click cycles with wrap-around
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -1520,18 +1695,17 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
float track_x1 = vx1 - number_w;
|
||||
float t = row.max > row.min ? (*row.value - row.min) / (row.max - row.min) : 0.0f;
|
||||
float knob_x = track_x0 + t * (track_x1 - track_x0);
|
||||
ImU32 track_col = focused ? IM_COL32(0, 0, 0, 56) : IM_COL32(255, 255, 255, 40);
|
||||
ImU32 track_col = focused ? IM_COL32(255, 255, 255, 77) : IM_COL32(0, 0, 0, 56);
|
||||
dl->AddRectFilled(ImVec2(track_x0, cy - 2.0f * s), ImVec2(track_x1, cy + 2.0f * s),
|
||||
track_col, 0.0f);
|
||||
dl->AddRectFilled(ImVec2(track_x0, cy - 2.0f * s), ImVec2(knob_x, cy + 2.0f * s),
|
||||
kColAccent, 0.0f);
|
||||
dl->AddCircleFilled(ImVec2(knob_x, cy), 8.0f * s,
|
||||
focused ? kColSelText : IM_COL32(240, 244, 245, 255), 24);
|
||||
kColInteract, 0.0f);
|
||||
dl->AddCircleFilled(ImVec2(Snap(knob_x), Snap(cy)), 8.0f * s, kColInteract, 24);
|
||||
char value_text[32];
|
||||
std::snprintf(value_text, sizeof(value_text), row.fmt, double(*row.value));
|
||||
ImVec2 extent = bold->CalcTextSizeA(value_size, FLT_MAX, 0.0f, value_text);
|
||||
dl->AddText(bold, value_size, ImVec2(vx1 - extent.x, cy - extent.y * 0.5f), value_col,
|
||||
value_text);
|
||||
ImVec2 extent = row_bold->CalcTextSizeA(value_size, FLT_MAX, 0.0f, value_text);
|
||||
dl->AddText(row_bold, value_size, ImVec2(Snap(vx1 - extent.x), Snap(cy - extent.y * 0.5f)),
|
||||
value_col, value_text);
|
||||
// Mouse drag on the track.
|
||||
ImGui::SetCursorScreenPos(ImVec2(track_x0 - 10.0f * s, cy - 14.0f * s));
|
||||
char slider_id[48];
|
||||
|
|
@ -1560,10 +1734,8 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
break;
|
||||
}
|
||||
case RowSpec::kAction: {
|
||||
const char* hint = row.enabled ? "Select" : "-";
|
||||
ImVec2 extent = font->CalcTextSizeA(15.0f * s, FLT_MAX, 0.0f, hint);
|
||||
dl->AddText(font, 15.0f * s, ImVec2(vx1 - extent.x, cy - extent.y * 0.5f), dim_col,
|
||||
hint);
|
||||
// No right-side 'Select' / '-' hint: the footer legend covers
|
||||
// activation, and disabled rows read from their dim label alone.
|
||||
if (clicked && hovered && row.enabled && !row_activated_this_frame) {
|
||||
activate_row(row);
|
||||
row_activated_this_frame = true;
|
||||
|
|
@ -1575,10 +1747,13 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
if (editing_this) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(vx0 + 6.0f * s, cy - 14.0f * s));
|
||||
ImGui::SetNextItemWidth(vx1 - vx0 - 12.0f * s);
|
||||
ImGui::PushFont(font, value_size);
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0.12f));
|
||||
// value_size is already physical; divide out the global widget-font
|
||||
// DPI scale (it exists for logical-unit dialogs, see ImGuiDrawer).
|
||||
ImGui::PushFont(font, value_size / std::max(0.01f, ImGui::GetStyle().FontScaleDpi));
|
||||
// Edit field sits on the BLACK focused row: light text, light frame.
|
||||
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(1, 1, 1, 0.15f));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImVec4(0.05f, 0.06f, 0.07f, 1.0f));
|
||||
ImVec4(0.98f, 0.99f, 0.99f, 1.0f));
|
||||
if (text_edit_focus_pending_) {
|
||||
ImGui::SetKeyboardFocusHere();
|
||||
text_edit_focus_pending_ = false;
|
||||
|
|
@ -1594,8 +1769,8 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
ImGui::PopFont();
|
||||
} else {
|
||||
const char* text = row.text_buf && row.text_buf[0] ? row.text_buf : "-";
|
||||
ImVec2 extent = bold->CalcTextSizeA(value_size, FLT_MAX, 0.0f, text);
|
||||
dl->AddText(bold, value_size, ImVec2(vx1 - extent.x, cy - extent.y * 0.5f),
|
||||
ImVec2 extent = row_bold->CalcTextSizeA(value_size, FLT_MAX, 0.0f, text);
|
||||
dl->AddText(row_bold, value_size, ImVec2(Snap(vx1 - extent.x), Snap(cy - extent.y * 0.5f)),
|
||||
value_col, text);
|
||||
if (clicked && hovered && row.enabled) {
|
||||
zone_ = FocusZone::kContent;
|
||||
|
|
@ -1609,6 +1784,22 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
// Phase 1: everything except the focused row.
|
||||
for (int i = 0; i < static_cast<int>(rows.size()); ++i) {
|
||||
if (!(content_focus && row_index_ == i)) {
|
||||
draw_row_content(i);
|
||||
}
|
||||
}
|
||||
// Phase 2: the sliding highlight above neighbours...
|
||||
if (content_focus && highlight_anim_y_ >= 0.0f) {
|
||||
float y0 = columns_y + Snap(highlight_anim_y_) - scroll;
|
||||
DrawFocusHighlight(dl, ImVec2(content_x, y0), ImVec2(content_x + content_w, y0 + row_h),
|
||||
s);
|
||||
}
|
||||
// ...phase 3: the focused row's own content above the highlight.
|
||||
if (content_focus && row_index_ >= 0 && row_index_ < static_cast<int>(rows.size())) {
|
||||
draw_row_content(row_index_);
|
||||
}
|
||||
dl->PopClipRect();
|
||||
|
||||
|
|
@ -1626,17 +1817,16 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
|
||||
// ---- Description panel ----
|
||||
{
|
||||
float panel_h = std::min(content_view_h, 320.0f * s);
|
||||
float header_bar_h = 38.0f * s;
|
||||
const float panel_h = desc_panel_h;
|
||||
const float header_bar_h = row_h; // header bar matches a settings row
|
||||
dl->AddRectFilled(ImVec2(desc_x, columns_y), ImVec2(desc_x + desc_w, columns_y + header_bar_h),
|
||||
kColAccent, 0.0f, ImDrawFlags_RoundCornersTop);
|
||||
AddTextVCentered(dl, bold, 16.0f * s, desc_x + 14.0f * s, columns_y + header_bar_h * 0.5f,
|
||||
kColAccent, 0.0f);
|
||||
AddTextVCentered(dl, bold_ol, label_size, desc_x + 18.0f * s, columns_y + header_bar_h * 0.5f,
|
||||
kColAccentDark, "Description");
|
||||
dl->AddRectFilled(ImVec2(desc_x, columns_y + header_bar_h),
|
||||
ImVec2(desc_x + desc_w, columns_y + panel_h), kColDescPanel, 0.0f,
|
||||
ImDrawFlags_RoundCornersBottom);
|
||||
ImVec2(desc_x + desc_w, columns_y + panel_h), kColDescPanel, 0.0f);
|
||||
dl->AddRect(ImVec2(desc_x, columns_y), ImVec2(desc_x + desc_w, columns_y + panel_h),
|
||||
kColPanelBorder, 0.0f);
|
||||
kColRailBorder, 0.0f);
|
||||
|
||||
const char* desc_text = kCategories[category_].desc;
|
||||
if (zone_ == FocusZone::kRail && rail_sel_ == quit_rail_index) {
|
||||
|
|
@ -1655,37 +1845,34 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
extra_text = &row.desc_extra;
|
||||
}
|
||||
}
|
||||
float text_x = desc_x + 14.0f * s;
|
||||
float text_y = columns_y + header_bar_h + 14.0f * s;
|
||||
float text_x = Snap(desc_x + 14.0f * s);
|
||||
float text_y = Snap(columns_y + header_bar_h + 14.0f * s);
|
||||
float wrap_w = desc_w - 28.0f * s;
|
||||
dl->AddText(font, desc_size, ImVec2(text_x, text_y), kColText, desc_text, nullptr, wrap_w);
|
||||
ImVec2 used = font->CalcTextSizeA(desc_size, FLT_MAX, wrap_w, desc_text);
|
||||
text_y += used.y + 10.0f * s;
|
||||
text_y = Snap(text_y +
|
||||
AddTextJustified(dl, font, desc_size, ImVec2(text_x, text_y), wrap_w,
|
||||
kColText, desc_text) +
|
||||
10.0f * s);
|
||||
if (extra_text) {
|
||||
dl->AddText(font, desc_size, ImVec2(text_x, text_y), kColTextDim, extra_text->c_str(),
|
||||
nullptr, wrap_w);
|
||||
ImVec2 extra_used =
|
||||
font->CalcTextSizeA(desc_size, FLT_MAX, wrap_w, extra_text->c_str());
|
||||
text_y += extra_used.y + 10.0f * s;
|
||||
text_y = Snap(text_y +
|
||||
AddTextJustified(dl, font, desc_size, ImVec2(text_x, text_y), wrap_w,
|
||||
kColTextDim, extra_text->c_str()) +
|
||||
10.0f * s);
|
||||
}
|
||||
if (note_text) {
|
||||
dl->AddText(bold, desc_size, ImVec2(text_x, text_y), kColWarn, note_text, nullptr,
|
||||
wrap_w);
|
||||
ImVec2 note_used = font->CalcTextSizeA(desc_size, FLT_MAX, wrap_w, note_text);
|
||||
text_y += note_used.y + 10.0f * s;
|
||||
text_y = Snap(text_y +
|
||||
AddTextJustified(dl, bold, desc_size, ImVec2(text_x, text_y), wrap_w,
|
||||
kColWarn, note_text) +
|
||||
10.0f * s);
|
||||
}
|
||||
if (pending) {
|
||||
dl->AddText(font, desc_size, ImVec2(text_x, text_y), kColWarn,
|
||||
"Changes are applied after a restart. Use Apply & Restart when ready.",
|
||||
nullptr, wrap_w);
|
||||
AddTextJustified(dl, font, desc_size, ImVec2(text_x, text_y), wrap_w, kColWarn,
|
||||
"Changes are applied after a restart. Use Apply & Restart when ready.");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Footer button legend ----
|
||||
{
|
||||
float legend_y = io.DisplaySize.y - footer_h + 14.0f * s;
|
||||
dl->AddRectFilled(ImVec2(rail_x, legend_y - 12.0f * s),
|
||||
ImVec2(menu_right, legend_y - 12.0f * s + 1.0f), kColRule);
|
||||
float legend_y = Snap(io.DisplaySize.y - footer_h + 14.0f * s);
|
||||
std::vector<LegendGlyph> glyphs;
|
||||
if (pad_active_) {
|
||||
glyphs.push_back({"A", "Select", true});
|
||||
|
|
@ -1705,29 +1892,30 @@ void SimpleSettingsDialog::OnDraw(ImGuiIO& io) {
|
|||
}
|
||||
}
|
||||
float x = rail_x;
|
||||
float glyph_size = 15.0f * s;
|
||||
float label_text_size = 16.0f * s;
|
||||
float chip_h = 26.0f * s;
|
||||
float glyph_size = font_px(15.0f * s);
|
||||
float label_text_size = font_px(16.0f * s);
|
||||
float chip_h = Snap(26.0f * s);
|
||||
for (const LegendGlyph& glyph : glyphs) {
|
||||
ImVec2 glyph_extent = bold->CalcTextSizeA(glyph_size, FLT_MAX, 0.0f, glyph.glyph);
|
||||
float cy = legend_y + chip_h * 0.5f;
|
||||
x = Snap(x);
|
||||
if (glyph.circle) {
|
||||
float r = chip_h * 0.5f;
|
||||
dl->AddCircleFilled(ImVec2(x + r, cy), r, IM_COL32(232, 236, 237, 235), 28);
|
||||
AddTextCentered(dl, bold, glyph_size, ImVec2(x + r, cy), IM_COL32(15, 18, 20, 255),
|
||||
glyph.glyph);
|
||||
dl->AddCircleFilled(ImVec2(x + r, cy), r, kColLegendChip, 28);
|
||||
AddTextCenteredCap(dl, bold_ol, glyph_size, ImVec2(x + r, cy), kColLegendText,
|
||||
glyph.glyph, /*ink_x=*/true);
|
||||
x += 2.0f * r + 8.0f * s;
|
||||
} else {
|
||||
float chip_w = glyph_extent.x + 18.0f * s;
|
||||
float chip_w = Snap(glyph_extent.x + 18.0f * s);
|
||||
dl->AddRectFilled(ImVec2(x, legend_y), ImVec2(x + chip_w, legend_y + chip_h),
|
||||
IM_COL32(232, 236, 237, 235), 4.0f * s);
|
||||
AddTextCentered(dl, bold, glyph_size, ImVec2(x + chip_w * 0.5f, cy),
|
||||
IM_COL32(15, 18, 20, 255), glyph.glyph);
|
||||
kColLegendChip, 4.0f * s);
|
||||
AddTextCenteredCap(dl, bold_ol, glyph_size, ImVec2(x + chip_w * 0.5f, cy),
|
||||
kColLegendText, glyph.glyph, /*ink_x=*/false);
|
||||
x += chip_w + 8.0f * s;
|
||||
}
|
||||
ImVec2 label_extent = font->CalcTextSizeA(label_text_size, FLT_MAX, 0.0f, glyph.label);
|
||||
dl->AddText(font, label_text_size, ImVec2(x, cy - label_extent.y * 0.5f), kColTextDim,
|
||||
glyph.label);
|
||||
ImVec2 label_extent = bold->CalcTextSizeA(label_text_size, FLT_MAX, 0.0f, glyph.label);
|
||||
dl->AddText(bold, label_text_size, ImVec2(Snap(x), Snap(cy - label_extent.y * 0.5f)),
|
||||
kColLegendLabel, glyph.label);
|
||||
x += label_extent.x + 26.0f * s;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
thirdparty/CMakeLists.txt
vendored
28
thirdparty/CMakeLists.txt
vendored
|
|
@ -225,6 +225,29 @@ endif()
|
|||
#=============================================================================
|
||||
include(${PROJECT_SOURCE_DIR}/cmake/rexglue_fidelityfx.cmake)
|
||||
|
||||
#=============================================================================
|
||||
# FreeType - font rasterizer for imgui (fetched on demand, all optional deps
|
||||
# disabled: pure static rasterizer). Enables hinted glyph rendering - the
|
||||
# stb_truetype loader cannot grid-fit horizontal strokes, which measurably
|
||||
# softens UI text versus browser rendering.
|
||||
#=============================================================================
|
||||
include(FetchContent)
|
||||
set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_PNG ON CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_HARFBUZZ ON CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_BROTLI ON CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(freetype
|
||||
GIT_REPOSITORY https://github.com/freetype/freetype.git
|
||||
GIT_TAG VER-2-13-3
|
||||
GIT_SHALLOW TRUE)
|
||||
FetchContent_MakeAvailable(freetype)
|
||||
# Drop the version .rc: it's DLL metadata only, and rc.exe has no include
|
||||
# paths under the clang/ninja toolchain (fatal RC1015 on windows.h).
|
||||
get_target_property(_ft_sources freetype SOURCES)
|
||||
list(FILTER _ft_sources EXCLUDE REGEX "\\.rc$")
|
||||
set_property(TARGET freetype PROPERTY SOURCES ${_ft_sources})
|
||||
|
||||
#=============================================================================
|
||||
# imgui - Immediate mode GUI (no CMakeLists.txt, manual target)
|
||||
#=============================================================================
|
||||
|
|
@ -235,11 +258,16 @@ add_library(imgui OBJECT
|
|||
${IMGUI_DIR}/imgui_draw.cpp
|
||||
${IMGUI_DIR}/imgui_tables.cpp
|
||||
${IMGUI_DIR}/imgui_widgets.cpp
|
||||
${IMGUI_DIR}/misc/freetype/imgui_freetype.cpp
|
||||
)
|
||||
add_library(imgui::imgui ALIAS imgui)
|
||||
target_include_directories(imgui PUBLIC
|
||||
$<BUILD_INTERFACE:${IMGUI_DIR}>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
|
||||
# PUBLIC: the define selects the default font loader in imgui.h-level state;
|
||||
# every TU including imgui.h must agree.
|
||||
target_compile_definitions(imgui PUBLIC IMGUI_ENABLE_FREETYPE)
|
||||
target_link_libraries(imgui PUBLIC freetype)
|
||||
target_compile_options(imgui PRIVATE -w)
|
||||
|
||||
#=============================================================================
|
||||
|
|
|
|||
2
thirdparty/imgui
vendored
2
thirdparty/imgui
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 6d910d5487d11ca567b61c7824b0c78c569d62f0
|
||||
Subproject commit 9fd429ca504dfb8aa7257876abe69b74c4e01cae
|
||||
Loading…
Add table
Reference in a new issue