Skip to content

Commit 0f6bc62

Browse files
feat: adapt monochrome controls with low-rate perimeter sampling
1 parent 1af479c commit 0f6bc62

6 files changed

Lines changed: 275 additions & 35 deletions

File tree

‎experiments/glass_control_island.h‎

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <functional>
1515
#include <string>
1616
#include <utility>
17+
#include "../src/glass_control_appearance.h"
1718

1819
namespace GlassControlIsland {
1920

@@ -98,7 +99,7 @@ class Controller {
9899
static_cast<LONG>(std::lround(centerY + radius))};
99100
}
100101
};
101-
Controller() = default;
102+
Controller() {appearanceFade_.Reset(RGB(0,0,0));}
102103
Controller(const Controller&) = delete;
103104
Controller& operator=(const Controller&) = delete;
104105
~Controller() { Destroy(); }
@@ -185,7 +186,8 @@ class Controller {
185186

186187
void Update(const RECT& noteRect, UINT dpi, bool visible, bool topmost,
187188
bool passThrough, bool menuOpen, HWND noteWindow = nullptr, int cornerRadiusPx = 0,
188-
bool folded = false, float foldProgress = 1.0f, bool noteTransition = false) {
189+
bool folded = false, float foldProgress = 1.0f, bool noteTransition = false,
190+
COLORREF backingColour=0,int backingAlpha=0) {
189191
if (!window_)
190192
return;
191193
const UINT newDpi = dpi ? dpi : 96;
@@ -205,7 +207,11 @@ class Controller {
205207
menuOpen_ = effectiveMenuOpen;
206208
// Retain the API for the host, but note/body hover and note capture are
207209
// deliberately no longer expansion sources, including click-through.
208-
(void)noteWindow;
210+
DWORD affinity=WDA_NONE;
211+
if(noteWindow)GetWindowDisplayAffinity(noteWindow,&affinity);
212+
appearanceRequest_.note=noteRect;appearanceRequest_.tint=backingColour;
213+
appearanceRequest_.radius=cornerRadiusPx;
214+
appearanceRequest_.alpha=!folded && affinity==WDA_EXCLUDEFROMCAPTURE?std::clamp(backingAlpha,0,255):0;
209215
noteRect_ = noteRect;
210216
cornerRadiusPx_ = std::max(0, cornerRadiusPx);
211217
const bool previousBelow = below_;
@@ -238,6 +244,7 @@ class Controller {
238244
(flags & ~(SWP_SHOWWINDOW | SWP_HIDEWINDOW)));
239245
}
240246
if (!visible) {
247+
appearanceRequest_.enabled=false;appearanceSampler_.Submit(appearanceRequest_);
241248
KillTimer(window_, kHoverTimer);
242249
KillTimer(window_, kMotionTimer);
243250
motionRunning_ = false;
@@ -272,9 +279,12 @@ class Controller {
272279
PollHover();
273280
if (geometryChanged || appearanceChanged || visibilityChanged || previousBelow != below_)
274281
Render();
282+
PollAppearance();
275283
}
276284

277285
void Destroy() {
286+
appearanceSampler_.Stop();
287+
for(auto& raster:rasters_)raster.Clear();
278288
closePointerDown_ = false;
279289
if (closeWindow_) {
280290
if (GetCapture() == closeWindow_) ReleaseCapture();
@@ -377,6 +387,13 @@ class Controller {
377387
Detail::Spring foldedHover_;
378388
Detail::DotImpulse dotImpulse_;
379389
float settingsTint_ = 0, closeTint_ = 0;
390+
std::array<GlassControlAppearance::Raster,2> rasters_;
391+
GlassControlAppearance::Sampler appearanceSampler_;
392+
GlassControlAppearance::Request appearanceRequest_;
393+
GlassAutoInk::Decision appearanceDecision_;
394+
GlassAutoInk::Fade appearanceFade_;
395+
unsigned appearanceSeen_=0;
396+
float lightAppearance_=0;
380397
float foldProgress_ = 1.0f;
381398
bool noteTransition_ = false, transitionToFold_ = false;
382399
float insideCenterY_ = 0, outsideCenterY_ = 0, foldedCenterY_ = 0;
@@ -665,6 +682,7 @@ class Controller {
665682
void Animate() {
666683
if (!window_ || !visible_) return;
667684
if (reduceMotion_) {
685+
lightAppearance_=GetRValue(appearanceFade_.target)/255.f;
668686
dotImpulse_.Clear();
669687
expansion_.value = expanded_ && ExpansionAllowed() ? 1.0f : 0.0f;
670688
expansion_.velocity = 0;
@@ -692,6 +710,8 @@ class Controller {
692710
// A faster, more pronounced rise and a short elastic settle. Retain
693711
// velocity when the pointer reverses instead of restarting an easing.
694712
bool active = expansion_.Step(expanded_ && ExpansionAllowed() ? 1.0f : 0.0f, seconds, 32.0f, 0.72f);
713+
lightAppearance_=GetRValue(appearanceFade_.Sample(double(now)))/255.f;
714+
active |= appearanceFade_.Running(double(now));
695715
active |= foldedHover_.Step(folded_ && !noteTransition_ && !inputSuppressed_ && hover_ ? 1.0f : 0.0f, seconds);
696716
active |= Detail::Fade(settingsTint_, hover_ && !closeHover_ && !inputSuppressed_ && !noteTransition_ ? 1.0f : 0.0f, seconds);
697717
active |= Detail::Fade(closeTint_, closeHover_ && ExpansionAllowed() ? 1.0f : 0.0f, seconds);
@@ -704,6 +724,28 @@ class Controller {
704724
KillTimer(window_, kMotionTimer);
705725
motionRunning_ = false;
706726
}
727+
PollAppearance();
728+
}
729+
730+
void PollAppearance() {
731+
auto request=appearanceRequest_;
732+
const auto rectangles=VisibleSurfaceRects();request.surface=rectangles[0];
733+
if(rectangles[1].right>rectangles[1].left)UnionRect(&request.surface,&request.surface,&rectangles[1]);
734+
request.padding=std::max(6,Scale(6));request.guard=std::max(2,Scale(3));
735+
request.enabled=visible_ && !highContrast_ && !noteTransition_ && !inputSuppressed_;
736+
appearanceSampler_.Submit(request);
737+
double share=0;
738+
if(appearanceSampler_.Read(appearanceSeen_,share))appearanceDecision_.Update(share,double(GetTickCount64()));
739+
// Keep the chosen X colour throughout red hover, including its retreat.
740+
if(closeHover_ || closePointerDown_ || closeTint_>.001f) {
741+
const int c=int(std::lround(lightAppearance_*255));appearanceFade_.Reset(RGB(c,c,c));return;
742+
}
743+
if(!request.enabled || !appearanceDecision_.valid)return;
744+
const COLORREF target=appearanceDecision_.white?RGB(255,255,255):RGB(0,0,0);
745+
if(!appearanceFade_.initialized || appearanceFade_.target!=target) {
746+
const int c=int(std::lround(lightAppearance_*255));
747+
appearanceFade_.Aim(target,RGB(c,c,c),double(GetTickCount64()));Animate();
748+
}
707749
}
708750

709751
static void RoundPath(Gdiplus::GraphicsPath& path, float x, float y,
@@ -778,22 +820,9 @@ class Controller {
778820
const int height = static_cast<int>(bounds_.bottom - bounds_.top);
779821
if (width <= 0 || height <= 0)
780822
return;
781-
BITMAPINFO info{};
782-
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
783-
info.bmiHeader.biWidth = width;
784-
info.bmiHeader.biHeight = -height;
785-
info.bmiHeader.biPlanes = 1;
786-
info.bmiHeader.biBitCount = 32;
787-
info.bmiHeader.biCompression = BI_RGB;
788-
void* pixels = nullptr;
789-
HDC dc = CreateCompatibleDC(nullptr);
790-
HBITMAP dib = CreateDIBSection(dc, &info, DIB_RGB_COLORS, &pixels, nullptr, 0);
791-
if (!dc || !dib || !pixels) {
792-
if (dib) DeleteObject(dib);
793-
if (dc) DeleteDC(dc);
794-
return;
795-
}
796-
const HGDIOBJ old = SelectObject(dc, dib);
823+
auto& raster=rasters_[close?1:0];
824+
if(!raster.Ensure(width,height))return;
825+
void* pixels=raster.pixels;HDC dc=raster.dc;
797826
{
798827
// GDI+ writes premultiplied BGRA directly, including edge coverage.
799828
// Do not re-premultiply these pixels after drawing.
@@ -839,15 +868,12 @@ class Controller {
839868
graphics.TranslateTransform(38.0f, centerY);
840869
graphics.ScaleTransform(surface.width / 72.0f, 1.0f);
841870
graphics.TranslateTransform(-38.0f, -centerY);
842-
const float materialWeight = std::max(glyphAlpha, folded_ ? foldProgress_ : 0.0f);
843-
Gdiplus::Color background = Mix(Gdiplus::Color(248, 64, 66, 70),
844-
Gdiplus::Color(248, 48, 50, 54), materialWeight);
845-
Gdiplus::Color settingsBackground = Mix(background, Gdiplus::Color(252, 72, 75, 80), settingsTint_);
871+
Gdiplus::Color background = Mix(Gdiplus::Color(255,0,0,0),Gdiplus::Color(255,255,255,255),lightAppearance_);
872+
Gdiplus::Color settingsBackground = background;
846873
Gdiplus::Color closeBackground = Mix(background, Gdiplus::Color(255, 220, 52, 65), closeTint_);
847874
closeBackground = Mix(settingsBackground, closeBackground, separation);
848-
Gdiplus::Color foreground(255, 245, 247, 251);
849-
Gdiplus::Color closeForeground = Mix(Gdiplus::Color(255, 255, 112, 113),
850-
Gdiplus::Color(255, 255, 255, 255), closeTint_);
875+
Gdiplus::Color foreground(255,255-background.GetR(),255-background.GetG(),255-background.GetB());
876+
Gdiplus::Color closeForeground = foreground;
851877
if (highContrast_) {
852878
const COLORREF bg = GetSysColor(COLOR_BTNFACE), fg = GetSysColor(COLOR_BTNTEXT);
853879
settingsBackground = Gdiplus::Color(255, GetRValue(bg), GetGValue(bg), GetBValue(bg));
@@ -871,9 +897,7 @@ class Controller {
871897
Gdiplus::SolidBrush closeFill(closeBackground);
872898
graphics.FillPath(&closeFill, &path);
873899
graphics.Restore(surfaceClip);
874-
Gdiplus::Pen rim(Gdiplus::Color(highContrast_ ? 255 : 52, foreground.GetR(), foreground.GetG(),
875-
foreground.GetB()), 0.65f);
876-
graphics.DrawPath(&rim, &path);
900+
if(highContrast_) {Gdiplus::Pen rim(foreground,0.65f);graphics.DrawPath(&rim,&path);}
877901
if (glyphAlpha > 0) {
878902
Gdiplus::Color crossColor(static_cast<BYTE>(255 * glyphAlpha), closeForeground.GetR(),
879903
closeForeground.GetG(), closeForeground.GetB());
@@ -888,6 +912,10 @@ class Controller {
888912
line.SetStartCap(Gdiplus::LineCapRound);
889913
line.SetEndCap(Gdiplus::LineCapRound);
890914
Gdiplus::SolidBrush knob(glyph);
915+
const auto glyphTransform=graphics.Save();
916+
graphics.TranslateTransform(24,centerY);
917+
graphics.ScaleTransform(1+.06f*settingsTint_,1+.06f*settingsTint_);
918+
graphics.TranslateTransform(-24,-centerY);
891919
// Original vector artwork: three adjustment tracks with alternating
892920
// handles, not a vendor font glyph or an embedded platform asset.
893921
const float rows[] = {centerY - 5.0f, centerY, centerY + 5.0f};
@@ -898,9 +926,9 @@ class Controller {
898926
graphics.FillEllipse(&cutout, handles[row] - 3, rows[row] - 3, 6.0f, 6.0f);
899927
graphics.FillEllipse(&knob, handles[row] - 1.9f, rows[row] - 1.9f, 3.8f, 3.8f);
900928
}
929+
graphics.Restore(glyphTransform);
901930
if (passThrough_) {
902-
Gdiplus::SolidBrush status(highContrast_ ? glyph :
903-
Gdiplus::Color(static_cast<BYTE>(255 * glyphAlpha), 111, 190, 255));
931+
Gdiplus::SolidBrush status(glyph);
904932
graphics.FillEllipse(&status, 36.0f, centerY + 5.0f, 2.5f, 2.5f);
905933
}
906934
}
@@ -920,9 +948,6 @@ class Controller {
920948
SIZE size{width, height};
921949
BLENDFUNCTION blend{AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
922950
UpdateLayeredWindow(target, nullptr, &destination, &size, dc, &source, 0, &blend, ULW_ALPHA);
923-
SelectObject(dc, old);
924-
DeleteObject(dib);
925-
DeleteDC(dc);
926951
}
927952

928953
void FinishPointer(bool click) {
@@ -1124,6 +1149,7 @@ class Controller {
11241149
}
11251150
if (wp == kHoverTimer) {
11261151
PollHover();
1152+
PollAppearance();
11271153
return 0;
11281154
}
11291155
break;

‎experiments/glass_experience.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ void SyncExperienceUI() {
8686
experienceIsland.Update(anchor,GetDpiForWindow(g_window),g_isVisible && (experienceFolded || IsWindowVisible(g_window)) && !IsIconic(g_window),
8787
state.pinned,state.passThrough,experiencePanel.Visible() || experienceCloseDialogOpen || experienceColorDialogOpen,
8888
g_window,ScaleForDpi(g_window,NoteCornerRadius()),experienceFolded || ExperienceAbsorbing(),
89-
experienceAbsorbProgress,ExperienceAbsorbing());
89+
experienceAbsorbProgress,ExperienceAbsorbing(),g_settings.themeColor,
90+
std::max(g_settings.passThrough?0:1,MulDiv(EffectiveOpacityPercent(),255,100)));
9091
// Activation/restoration can raise the note above its independent hint.
9192
// Insert the controls immediately above the note, preserving applications
9293
// already above it rather than raising the controls to the top of the band.

‎src/glass_control_appearance.h‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#pragma once
2+
#include "glass_auto_ink.h"
3+
#include <chrono>
4+
#include <condition_variable>
5+
#include <mutex>
6+
#include <thread>
7+
#include <functional>
8+
9+
namespace GlassControlAppearance {
10+
// Owned by one thread. Retain DIB/DC allocations across animation frames and
11+
// background samples; no screen image is written to disk.
12+
struct Raster {
13+
HDC dc=nullptr;HBITMAP bitmap=nullptr;HGDIOBJ old=nullptr;
14+
void* pixels=nullptr;int width=0,height=0;unsigned allocations=0;
15+
Raster()=default;Raster(const Raster&)=delete;Raster& operator=(const Raster&)=delete;
16+
~Raster(){Clear();}
17+
void Clear(){if(dc && old)SelectObject(dc,old);if(bitmap)DeleteObject(bitmap);if(dc)DeleteDC(dc);dc=nullptr;bitmap=nullptr;old=nullptr;pixels=nullptr;width=height=0;}
18+
bool Ensure(int w,int h) {
19+
if(w==width && h==height && pixels)return true;
20+
if(w<=0 || h<=0 || w>2048 || h>1024)return false;
21+
Clear();BITMAPINFO info{};info.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
22+
info.bmiHeader.biWidth=w;info.bmiHeader.biHeight=-h;info.bmiHeader.biPlanes=1;
23+
info.bmiHeader.biBitCount=32;info.bmiHeader.biCompression=BI_RGB;
24+
dc=CreateCompatibleDC(nullptr);
25+
if(dc)bitmap=CreateDIBSection(dc,&info,DIB_RGB_COLORS,&pixels,nullptr,0);
26+
if(!dc || !bitmap || !pixels){Clear();return false;}
27+
old=SelectObject(dc,bitmap);width=w;height=h;++allocations;return true;
28+
}
29+
};
30+
struct Request {
31+
RECT surface{},note{};int padding=6,guard=2,radius=0,alpha=0;COLORREF tint=0;
32+
bool enabled=false;
33+
};
34+
inline bool Same(const Request& a,const Request& b) {
35+
return EqualRect(&a.surface,&b.surface) && EqualRect(&a.note,&b.note) && a.padding==b.padding &&
36+
a.guard==b.guard && a.radius==b.radius && a.alpha==b.alpha && a.tint==b.tint && a.enabled==b.enabled;
37+
}
38+
inline bool Inside(RECT r,int x,int y){return x>=r.left && x<r.right && y>=r.top && y<r.bottom;}
39+
inline bool InsideNote(const Request& r,int x,int y) {
40+
if(!Inside(r.note,x,y))return false;
41+
float cx=(r.note.left+r.note.right)*.5f,cy=(r.note.top+r.note.bottom)*.5f;
42+
float radius=float(std::min({r.radius,int(r.note.right-r.note.left)/2,int(r.note.bottom-r.note.top)/2}));
43+
float qx=std::max(std::abs(x+.5f-cx)-(r.note.right-r.note.left)*.5f+radius,0.f);
44+
float qy=std::max(std::abs(y+.5f-cy)-(r.note.bottom-r.note.top)*.5f+radius,0.f);
45+
return qx*qx+qy*qy<=radius*radius;
46+
}
47+
inline bool Vote(const Raster& raster,RECT crop,const Request& request,double& darkShare) {
48+
RECT excluded=request.surface;InflateRect(&excluded,request.guard,request.guard);
49+
int dark=0,total=0;
50+
const int step=std::max(1,int(std::sqrt(double(raster.width)*raster.height/512)));
51+
for(int y=step/2;y<raster.height;y+=step)for(int x=step/2;x<raster.width;x+=step) {
52+
const int sx=crop.left+x,sy=crop.top+y;
53+
if(Inside(excluded,sx,sy))continue;
54+
const auto p=static_cast<const BYTE*>(raster.pixels)+(size_t(y)*raster.width+x)*4;
55+
int red=p[2],green=p[1],blue=p[0];
56+
// Capture-excluded notes may be absent from BitBlt. Restore only their
57+
// known overlay tint for perimeter samples inside the rounded note.
58+
if(request.alpha>0 && InsideNote(request,sx,sy)) {
59+
auto mix=[&](int a,int b){return (a*(255-request.alpha)+b*request.alpha+127)/255;};
60+
red=mix(red,GetRValue(request.tint));green=mix(green,GetGValue(request.tint));blue=mix(blue,GetBValue(request.tint));
61+
}
62+
dark+=GlassAutoInk::PrefersWhite(RGB(red,green,blue));++total;
63+
}
64+
if(total<8)return false;
65+
darkShare=double(dark)/total;return true;
66+
}
67+
inline bool Capture(Raster& raster,const Request& request,double& darkShare) {
68+
RECT crop=request.surface;InflateRect(&crop,request.padding,request.padding);
69+
MONITORINFO monitor{sizeof(monitor)};
70+
if(!GetMonitorInfoW(MonitorFromRect(&request.surface,MONITOR_DEFAULTTONEAREST),&monitor) ||
71+
!IntersectRect(&crop,&crop,&monitor.rcMonitor) || !raster.Ensure(crop.right-crop.left,crop.bottom-crop.top))return false;
72+
HDC screen=GetDC(nullptr);if(!screen)return false;
73+
const bool copied=BitBlt(raster.dc,0,0,raster.width,raster.height,screen,crop.left,crop.top,SRCCOPY|CAPTUREBLT)!=FALSE;
74+
if(copied)GdiFlush();ReleaseDC(nullptr,screen);
75+
return copied && Vote(raster,crop,request,darkShare);
76+
}
77+
class Sampler {
78+
std::function<bool(Raster&,const Request&,double&)> capture_;
79+
std::mutex mutex_;std::condition_variable wake_;std::thread worker_;
80+
Request request_{};bool stop_=false;unsigned generation_=0,sequence_=0;
81+
double share_=.5,elapsedMs_=0;unsigned resultGeneration_=0;
82+
void Run() {
83+
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
84+
Raster raster;auto next=std::chrono::steady_clock::now();
85+
std::unique_lock lock(mutex_);
86+
while(!stop_) {
87+
wake_.wait(lock,[&]{return stop_ || request_.enabled;});if(stop_)break;
88+
if(wake_.wait_until(lock,next,[&]{return stop_ || !request_.enabled;}))continue;
89+
const Request request=request_;const unsigned generation=generation_;
90+
lock.unlock();double share=0;const auto began=std::chrono::steady_clock::now();
91+
bool ok=capture_(raster,request,share);
92+
const double ms=std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-began).count();
93+
lock.lock();
94+
if(ok && generation==generation_ && request_.enabled){share_=share;elapsedMs_=ms;resultGeneration_=generation;++sequence_;}
95+
// Expensive/unsupported desktop capture backs off instead of
96+
// spending the normal 4 Hz budget or blocking the UI thread.
97+
next=std::chrono::steady_clock::now()+std::chrono::milliseconds(ok && ms<8?250:1000);
98+
}
99+
}
100+
public:
101+
explicit Sampler(std::function<bool(Raster&,const Request&,double&)> capture=Capture):capture_(std::move(capture)){}
102+
~Sampler(){Stop();}
103+
void Submit(const Request& request) {
104+
std::lock_guard lock(mutex_);
105+
if(Same(request,request_))return;
106+
request_=request;++generation_;
107+
if(request.enabled && !worker_.joinable()){stop_=false;worker_=std::thread([this]{Run();});}
108+
wake_.notify_one();
109+
}
110+
bool Read(unsigned& seen,double& share,double* elapsed=nullptr) {
111+
std::unique_lock lock(mutex_,std::try_to_lock);
112+
if(!lock || sequence_==seen || resultGeneration_!=generation_)return false;
113+
seen=sequence_;share=share_;if(elapsed)*elapsed=elapsedMs_;return true;
114+
}
115+
void Stop() {
116+
{std::lock_guard lock(mutex_);stop_=true;wake_.notify_one();}
117+
if(worker_.joinable())worker_.join();
118+
std::lock_guard lock(mutex_);request_={};++generation_;
119+
}
120+
};
121+
}

0 commit comments

Comments
 (0)