Skip to content

Commit a58204b

Browse files
merge: integrate adaptive glass and interaction improvements
2 parents d8072c9 + 1e03bc4 commit a58204b

15 files changed

Lines changed: 1046 additions & 83 deletions

‎experiments/glass_control_island.h‎

Lines changed: 64 additions & 37 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

@@ -23,8 +24,7 @@ namespace Detail {
2324
// elasticity without changing its anchored edge or its hit-test footprint.
2425
struct Spring {
2526
float value = 0, velocity = 0;
26-
bool Step(float target, float seconds) {
27-
constexpr float frequency = 22.0f, damping = 0.74f;
27+
bool Step(float target, float seconds, float frequency=22.0f, float damping=0.74f) {
2828
const float dt = std::clamp(seconds, 0.0f, 1.0f);
2929
const float decayRate = frequency * damping;
3030
const float dampedFrequency = frequency * std::sqrt(1.0f - damping * damping);
@@ -99,7 +99,7 @@ class Controller {
9999
static_cast<LONG>(std::lround(centerY + radius))};
100100
}
101101
};
102-
Controller() = default;
102+
Controller() {appearanceFade_.Reset(RGB(0,0,0));}
103103
Controller(const Controller&) = delete;
104104
Controller& operator=(const Controller&) = delete;
105105
~Controller() { Destroy(); }
@@ -186,7 +186,8 @@ class Controller {
186186

187187
void Update(const RECT& noteRect, UINT dpi, bool visible, bool topmost,
188188
bool passThrough, bool menuOpen, HWND noteWindow = nullptr, int cornerRadiusPx = 0,
189-
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) {
190191
if (!window_)
191192
return;
192193
const UINT newDpi = dpi ? dpi : 96;
@@ -206,7 +207,11 @@ class Controller {
206207
menuOpen_ = effectiveMenuOpen;
207208
// Retain the API for the host, but note/body hover and note capture are
208209
// deliberately no longer expansion sources, including click-through.
209-
(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;
210215
noteRect_ = noteRect;
211216
cornerRadiusPx_ = std::max(0, cornerRadiusPx);
212217
const bool previousBelow = below_;
@@ -239,6 +244,7 @@ class Controller {
239244
(flags & ~(SWP_SHOWWINDOW | SWP_HIDEWINDOW)));
240245
}
241246
if (!visible) {
247+
appearanceRequest_.enabled=false;appearanceSampler_.Submit(appearanceRequest_);
242248
KillTimer(window_, kHoverTimer);
243249
KillTimer(window_, kMotionTimer);
244250
motionRunning_ = false;
@@ -273,9 +279,12 @@ class Controller {
273279
PollHover();
274280
if (geometryChanged || appearanceChanged || visibilityChanged || previousBelow != below_)
275281
Render();
282+
PollAppearance();
276283
}
277284

278285
void Destroy() {
286+
appearanceSampler_.Stop();
287+
for(auto& raster:rasters_)raster.Clear();
279288
closePointerDown_ = false;
280289
if (closeWindow_) {
281290
if (GetCapture() == closeWindow_) ReleaseCapture();
@@ -378,6 +387,13 @@ class Controller {
378387
Detail::Spring foldedHover_;
379388
Detail::DotImpulse dotImpulse_;
380389
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;
381397
float foldProgress_ = 1.0f;
382398
bool noteTransition_ = false, transitionToFold_ = false;
383399
float insideCenterY_ = 0, outsideCenterY_ = 0, foldedCenterY_ = 0;
@@ -666,6 +682,7 @@ class Controller {
666682
void Animate() {
667683
if (!window_ || !visible_) return;
668684
if (reduceMotion_) {
685+
lightAppearance_=GetRValue(appearanceFade_.target)/255.f;
669686
dotImpulse_.Clear();
670687
expansion_.value = expanded_ && ExpansionAllowed() ? 1.0f : 0.0f;
671688
expansion_.velocity = 0;
@@ -690,7 +707,11 @@ class Controller {
690707
const ULONGLONG now = GetTickCount64();
691708
const float seconds = static_cast<float>(now - lastMotionTick_) / 1000.0f;
692709
lastMotionTick_ = now;
693-
bool active = expansion_.Step(expanded_ && ExpansionAllowed() ? 1.0f : 0.0f, seconds);
710+
// A faster, more pronounced rise and a short elastic settle. Retain
711+
// velocity when the pointer reverses instead of restarting an easing.
712+
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));
694715
active |= foldedHover_.Step(folded_ && !noteTransition_ && !inputSuppressed_ && hover_ ? 1.0f : 0.0f, seconds);
695716
active |= Detail::Fade(settingsTint_, hover_ && !closeHover_ && !inputSuppressed_ && !noteTransition_ ? 1.0f : 0.0f, seconds);
696717
active |= Detail::Fade(closeTint_, closeHover_ && ExpansionAllowed() ? 1.0f : 0.0f, seconds);
@@ -703,6 +724,28 @@ class Controller {
703724
KillTimer(window_, kMotionTimer);
704725
motionRunning_ = false;
705726
}
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+
}
706749
}
707750

708751
static void RoundPath(Gdiplus::GraphicsPath& path, float x, float y,
@@ -777,22 +820,9 @@ class Controller {
777820
const int height = static_cast<int>(bounds_.bottom - bounds_.top);
778821
if (width <= 0 || height <= 0)
779822
return;
780-
BITMAPINFO info{};
781-
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
782-
info.bmiHeader.biWidth = width;
783-
info.bmiHeader.biHeight = -height;
784-
info.bmiHeader.biPlanes = 1;
785-
info.bmiHeader.biBitCount = 32;
786-
info.bmiHeader.biCompression = BI_RGB;
787-
void* pixels = nullptr;
788-
HDC dc = CreateCompatibleDC(nullptr);
789-
HBITMAP dib = CreateDIBSection(dc, &info, DIB_RGB_COLORS, &pixels, nullptr, 0);
790-
if (!dc || !dib || !pixels) {
791-
if (dib) DeleteObject(dib);
792-
if (dc) DeleteDC(dc);
793-
return;
794-
}
795-
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;
796826
{
797827
// GDI+ writes premultiplied BGRA directly, including edge coverage.
798828
// Do not re-premultiply these pixels after drawing.
@@ -838,15 +868,12 @@ class Controller {
838868
graphics.TranslateTransform(38.0f, centerY);
839869
graphics.ScaleTransform(surface.width / 72.0f, 1.0f);
840870
graphics.TranslateTransform(-38.0f, -centerY);
841-
const float materialWeight = std::max(glyphAlpha, folded_ ? foldProgress_ : 0.0f);
842-
Gdiplus::Color background = Mix(Gdiplus::Color(248, 64, 66, 70),
843-
Gdiplus::Color(248, 48, 50, 54), materialWeight);
844-
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;
845873
Gdiplus::Color closeBackground = Mix(background, Gdiplus::Color(255, 220, 52, 65), closeTint_);
846874
closeBackground = Mix(settingsBackground, closeBackground, separation);
847-
Gdiplus::Color foreground(255, 245, 247, 251);
848-
Gdiplus::Color closeForeground = Mix(Gdiplus::Color(255, 255, 112, 113),
849-
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;
850877
if (highContrast_) {
851878
const COLORREF bg = GetSysColor(COLOR_BTNFACE), fg = GetSysColor(COLOR_BTNTEXT);
852879
settingsBackground = Gdiplus::Color(255, GetRValue(bg), GetGValue(bg), GetBValue(bg));
@@ -870,9 +897,7 @@ class Controller {
870897
Gdiplus::SolidBrush closeFill(closeBackground);
871898
graphics.FillPath(&closeFill, &path);
872899
graphics.Restore(surfaceClip);
873-
Gdiplus::Pen rim(Gdiplus::Color(highContrast_ ? 255 : 52, foreground.GetR(), foreground.GetG(),
874-
foreground.GetB()), 0.65f);
875-
graphics.DrawPath(&rim, &path);
900+
if(highContrast_) {Gdiplus::Pen rim(foreground,0.65f);graphics.DrawPath(&rim,&path);}
876901
if (glyphAlpha > 0) {
877902
Gdiplus::Color crossColor(static_cast<BYTE>(255 * glyphAlpha), closeForeground.GetR(),
878903
closeForeground.GetG(), closeForeground.GetB());
@@ -887,6 +912,10 @@ class Controller {
887912
line.SetStartCap(Gdiplus::LineCapRound);
888913
line.SetEndCap(Gdiplus::LineCapRound);
889914
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);
890919
// Original vector artwork: three adjustment tracks with alternating
891920
// handles, not a vendor font glyph or an embedded platform asset.
892921
const float rows[] = {centerY - 5.0f, centerY, centerY + 5.0f};
@@ -897,9 +926,9 @@ class Controller {
897926
graphics.FillEllipse(&cutout, handles[row] - 3, rows[row] - 3, 6.0f, 6.0f);
898927
graphics.FillEllipse(&knob, handles[row] - 1.9f, rows[row] - 1.9f, 3.8f, 3.8f);
899928
}
929+
graphics.Restore(glyphTransform);
900930
if (passThrough_) {
901-
Gdiplus::SolidBrush status(highContrast_ ? glyph :
902-
Gdiplus::Color(static_cast<BYTE>(255 * glyphAlpha), 111, 190, 255));
931+
Gdiplus::SolidBrush status(glyph);
903932
graphics.FillEllipse(&status, 36.0f, centerY + 5.0f, 2.5f, 2.5f);
904933
}
905934
}
@@ -919,9 +948,6 @@ class Controller {
919948
SIZE size{width, height};
920949
BLENDFUNCTION blend{AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
921950
UpdateLayeredWindow(target, nullptr, &destination, &size, dc, &source, 0, &blend, ULW_ALPHA);
922-
SelectObject(dc, old);
923-
DeleteObject(dib);
924-
DeleteDC(dc);
925951
}
926952

927953
void FinishPointer(bool click) {
@@ -1123,6 +1149,7 @@ class Controller {
11231149
}
11241150
if (wp == kHoverTimer) {
11251151
PollHover();
1152+
PollAppearance();
11261153
return 0;
11271154
}
11281155
break;

‎experiments/glass_experience.h‎

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "glass_close_behavior.h"
55
#include "glass_experience_policy.h"
66
#include "glass_absorb_motion.h"
7+
#include "../src/glass_window_stacking.h"
78
#include <functional>
89

910
namespace {
@@ -85,21 +86,15 @@ void SyncExperienceUI() {
8586
experienceIsland.Update(anchor,GetDpiForWindow(g_window),g_isVisible && (experienceFolded || IsWindowVisible(g_window)) && !IsIconic(g_window),
8687
state.pinned,state.passThrough,experiencePanel.Visible() || experienceCloseDialogOpen || experienceColorDialogOpen,
8788
g_window,ScaleForDpi(g_window,NoteCornerRadius()),experienceFolded || ExperienceAbsorbing(),
88-
experienceAbsorbProgress,ExperienceAbsorbing());
89+
experienceAbsorbProgress,ExperienceAbsorbing(),g_settings.themeColor,
90+
std::max(g_settings.passThrough?0:1,MulDiv(EffectiveOpacityPercent(),255,100)));
8991
// Activation/restoration can raise the note above its independent hint.
9092
// Insert the controls immediately above the note, preserving applications
9193
// already above it rather than raising the controls to the top of the band.
9294
if(IsWindowVisible(g_window) && IsWindowVisible(experienceIsland.Window())) {
93-
int remaining=64;
94-
for(HWND above=GetWindow(experienceIsland.Window(),GW_HWNDPREV);above && --remaining;
95-
above=GetWindow(above,GW_HWNDPREV))if(above==g_window) {
96-
HWND insert=GetWindow(g_window,GW_HWNDPREV);
97-
if(!insert || (!state.pinned && (GetWindowLongPtrW(insert,GWL_EXSTYLE)&WS_EX_TOPMOST)))insert=HWND_TOP;
98-
SetWindowPos(experienceIsland.Window(),insert,0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE);
99-
if(IsWindowVisible(experienceIsland.CloseWindow()))SetWindowPos(experienceIsland.CloseWindow(),insert,
100-
0,0,0,0,SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE);
101-
break;
102-
}
95+
GlassWindowStacking::PlaceAbove(experienceIsland.Window(),g_window,state.pinned);
96+
if(IsWindowVisible(experienceIsland.CloseWindow()))
97+
GlassWindowStacking::PlaceAbove(experienceIsland.CloseWindow(),experienceIsland.Window(),state.pinned);
10398
}
10499
SyncExperienceCapture();
105100
experiencePanel.UpdateState(state);
@@ -265,7 +260,8 @@ void ExpandExperienceNote() {
265260
experienceAbsorbInputBlocked=true;experienceAbsorbReleased=0;
266261
experienceEndpointPresented=false;experienceMotionGeometryMismatches=0;experienceAbsorbProgress=1;experienceAbsorbAlpha=1;
267262
experienceIsland.SetInputSuppressed(true);
268-
SaveExperienceRestoreSize();SaveSettings();
263+
// Persist the final restore state at completion, not on the click path.
264+
g_backdrop.SetPresentationMotion(true);
269265
// Establish the real start at the currently drawn (possibly hovered) dot.
270266
// Geometry is never enlarged after the curve computes its pixel bounds.
271267
SyncExperienceUI();
@@ -337,6 +333,7 @@ void MaybeAbsorbExperienceResize() {
337333
experienceNativeSizing=false;return;
338334
}
339335
experienceNativeSizing=false;experiencePanel.Close(false);KillTimer(g_window,kCaretTimer);
336+
g_backdrop.SetPresentationMotion(true);
340337
// Take over this gesture before moving/hiding the grip. Its queued up or
341338
// capture-lost notifications may still arrive, but cannot restart resizing.
342339
g_pointerDown=false;g_pointerDragged=false;
@@ -372,7 +369,7 @@ void TickExperienceAbsorb() {
372369
// the dot/text. Never skip from the last larger rectangle to hidden.
373370
if(frame.progress>=1)experienceEndpointPresented=true;
374371
} else if(!restoring) {
375-
ShowWindow(g_window,SW_HIDE);g_backdrop.Enable(g_window,false);
372+
ShowWindow(g_window,SW_HIDE);g_backdrop.SuspendForFold();
376373
g_backdrop.SetPresentationOpacity(1.0f);
377374
experienceAbsorbing=false;experienceFolded=true;experienceAbsorbAlpha=1;
378375
// Keep saved/restore geometry at the original thin-card anchor, not
@@ -383,6 +380,7 @@ void TickExperienceAbsorb() {
383380
SaveSettings();SaveExperienceRestoreSize();
384381
} else {
385382
experienceRestoring=false;experienceAbsorbAlpha=1;
383+
g_backdrop.SetPresentationMotion(false);
386384
g_backdrop.SetPresentationOpacity(1.0f);
387385
UpdateLayout(g_window);SaveSettings();SaveExperienceRestoreSize();SaveGlassLabPreferences();
388386
}
@@ -496,6 +494,9 @@ void CreateExperienceUI() {
496494
}
497495
void DestroyExperienceUI() {
498496
KillTimer(g_window,73);KillTimer(g_window,74);KillTimer(g_window,75);KillTimer(g_window,76);
497+
// Restore persistence is deferred off the click path; closing mid-motion
498+
// must still save the intended final state before controls are destroyed.
499+
if(experienceReady)SaveExperienceRestoreSize();
499500
SaveExperienceMaterial();experienceReady=false;
500501
// The popup borrows the note's dispatcher queue; destroy it first.
501502
experiencePanel.Destroy();experienceIsland.Destroy();

‎src/glass_auto_ink.h‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#pragma once
2+
#include <windows.h>
3+
#include <algorithm>
4+
#include <array>
5+
#include <cmath>
6+
7+
namespace GlassAutoInk {
8+
constexpr COLORREF Dark=RGB(30,32,36),Light=RGB(248,249,250);
9+
inline double Luminance(COLORREF colour) {
10+
static const auto linear=[] {
11+
std::array<double,256> table{};
12+
for(int i=0;i<256;i++){double c=i/255.;table[i]=c<=.04045?c/12.92:std::pow((c+.055)/1.055,2.4);}
13+
return table;
14+
}();
15+
return .2126*linear[GetRValue(colour)]+.7152*linear[GetGValue(colour)]+.0722*linear[GetBValue(colour)];
16+
}
17+
inline bool PrefersWhite(COLORREF colour) {
18+
static const double boundary=std::sqrt((Luminance(Dark)+.05)*(Luminance(Light)+.05))-.05;
19+
return Luminance(colour)<boundary;
20+
}
21+
// Vote per area sample AFTER the actual overlay's byte-space alpha blend.
22+
// Averaging colours first would incorrectly turn a black/white split into grey.
23+
inline double WhiteShare(const void* data,unsigned pitch,int width,int height,COLORREF tint,int alpha) {
24+
unsigned white=0;
25+
alpha=std::clamp(alpha,0,255);
26+
for(int y=0;y<height;y++)for(int x=0;x<width;x++) {
27+
const auto p=static_cast<const unsigned char*>(data)+size_t(y)*pitch+x*4;
28+
auto blend=[alpha](int b,int t){return (b*(255-alpha)+t*alpha+127)/255;};
29+
white+=PrefersWhite(RGB(blend(p[2],GetRValue(tint)),blend(p[1],GetGValue(tint)),blend(p[0],GetBValue(tint))));
30+
}
31+
return width>0 && height>0 ? double(white)/(double(width)*height) : .5;
32+
}
33+
struct Decision {
34+
bool white=false,valid=false,pending=false,candidate=false;
35+
double candidateAt=0;
36+
void Reset(bool fallbackWhite=false){*this={};white=fallbackWhite;}
37+
void Update(double share,double now) {
38+
if(!valid){if(share>.55)white=true;else if(share<.45)white=false;valid=true;return;}
39+
bool next=share>.55?true:share<.45?false:white;
40+
if(next==white){pending=false;return;}
41+
if(share>=.85 || share<=.15){white=next;pending=false;return;}
42+
if(!pending || candidate!=next){pending=true;candidate=next;candidateAt=now;return;}
43+
if(now-candidateAt>=90){white=next;pending=false;}
44+
}
45+
};
46+
struct Fade {
47+
static constexpr double duration=180;
48+
COLORREF from=Dark,target=Dark;
49+
double started=0;
50+
bool initialized=false;
51+
void Reset(COLORREF colour){from=target=colour;initialized=true;started=0;}
52+
bool Running(double now) const {return from!=target && now-started<duration;}
53+
COLORREF Sample(double now) const {
54+
double t=std::clamp((now-started)/duration,0.,1.);
55+
t=t*t*(3-2*t); // smooth departure and arrival, no long grey plateau
56+
auto channel=[t](int a,int b){return int(std::lround(a+(b-a)*t));};
57+
return RGB(channel(GetRValue(from),GetRValue(target)),channel(GetGValue(from),GetGValue(target)),channel(GetBValue(from),GetBValue(target)));
58+
}
59+
void Aim(COLORREF colour,COLORREF current,double now) {
60+
if(!initialized)Reset(current);
61+
if(colour==target)return;
62+
from=Sample(now);target=colour;started=now;
63+
}
64+
};
65+
}

0 commit comments

Comments
 (0)