Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,56 @@ All notable changes to ConfigurO are recorded here. The updater reads the
heading for the running version to decide what to show, so keep the
`## [x.y]` format.

## [3.2]

### Fixes
- **Every outlined surface in the app was drawn blurred.** Cards, buttons,
inputs, tags, toggles, checkboxes, table rows, the sidebar rule and the
window's own edge all reach the screen through one hairline helper, and that
helper inset its path by a whole pixel before stroking it. The app paints
under `PixelOffsetMode.HighQuality`, which puts sample points on pixel
*corners*, so a 1px pen running along an integer coordinate straddles two
rows and GDI+ resolves it by painting both at roughly two-thirds strength.
Measured on Windows: a `#3F424D` edge on `#161826` came back as two rows of
`#2B2D3A` instead of one row of `#3F424D`. The stroke now sits on a
half-pixel centreline, with the corner radius pulled in to match so its outer
edge still follows the fill beneath it. Verified in the shipping binary at
150% scale — the edge is one column, at the exact token colour.
This could not be seen where the interface was being reviewed: libgdiplus
does not reproduce the split, so on Linux the borders had always looked
correct.
- **Aero Snap, Snap Layouts, the Alt+Space window menu and the minimise
animation all did nothing.** The shell is a borderless window, and
`FormBorderStyle.None` strips `WS_THICKFRAME`, `WS_MINIMIZEBOX`,
`WS_MAXIMIZEBOX` and `WS_SYSMENU` — the style bits Windows reads to decide
what kind of window it is looking at. Measured, the window was reporting
`0x16010000`: no thick frame, no system menu. They are restored in
`CreateParams`; the window still draws all of its own chrome, and maximise
stays inside the work area because `WM_GETMINMAXINFO` was already answered
correctly. The rounded corners, shadow and DWM border were never affected —
Windows 11 gives those to any top-level window.

### Interface
- **The mouse wheel moved content in 54px jumps.** Scrolling now eases toward a
target at roughly 66fps instead of teleporting once per notch, and a fast
flick compounds into one long glide rather than each notch restarting from
wherever the last one reached. The panel composites its children so moving
them does not tear. Dragging the scrollbar thumb stays 1:1 — that is direct
manipulation and should not lag behind the pointer.
- **Hover and press now arrive and leave instead of switching on.** Every
interactive surface in the app repainted its hover state instantly; only the
toggle knob had ever animated. That curve — 150ms, ease-out — is now a
reusable value in `NAnim`, and `NControl` and `NPanel` drive one for hover
and one for press, so painters read a float rather than a bool. Buttons,
selectable cards and text inputs use it: a button fades its fill up from
nothing and deepens rather than jumping when pressed, and a card edge mixes
toward the accent rather than flicking between two colours as the pointer
crosses a grid. Anyone who has turned animation off in Windows gets none of
it — `SPI_GETCLIENTAREAANIMATION` is honoured.
- The wheel router no longer keeps its own copy of the scroll distance, so a
routed wheel and a focused one can no longer disagree about how far a notch
goes.

## [3.1]

### Fixes
Expand Down
3 changes: 2 additions & 1 deletion src/ConfigurO/ConfigurO.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
Expand Down Expand Up @@ -108,6 +108,7 @@
<Compile Include="Forms\MainForm.cs" />
<Compile Include="Nocturne\DwmChrome.cs" />
<Compile Include="Nocturne\I18n.cs" />
<Compile Include="Nocturne\NAnim.cs" />
<Compile Include="Nocturne\NocturneBrand.cs" />
<Compile Include="Nocturne\NocturneDraw.cs" />
<Compile Include="Nocturne\NocturneFonts.cs" />
Expand Down
7 changes: 2 additions & 5 deletions src/ConfigurO/Controls/MoonCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ protected override void OnPaint(PaintEventArgs e)
else
{
Color edge = _hover ? NocturneTheme.Accent : NocturneTheme.Neutral600;
using (Pen pen = new Pen(edge)) g.DrawPath(pen, p);
NocturneTheme.DrawRounded(g, box, radius, edge);
}
}

Expand All @@ -101,10 +101,7 @@ protected override void OnPaint(PaintEventArgs e)

if (Focused)
{
Rectangle ring = Rectangle.Inflate(box, NocturneScale.S(2), NocturneScale.S(2));
using (GraphicsPath p = NocturneTheme.RoundedRect(ring, radius + NocturneScale.S(2)))
using (Pen pen = new Pen(NocturneTheme.Accent, NocturneScale.S(2)))
g.DrawPath(pen, p);
NocturneDraw.FocusRing(g, box, radius);
}

if (!string.IsNullOrEmpty(Text))
Expand Down
2 changes: 1 addition & 1 deletion src/ConfigurO/Controls/MoonCheckList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ protected override void OnDrawItem(DrawItemEventArgs e)
}
else
{
using (Pen pen = new Pen(NocturneTheme.Neutral600)) g.DrawPath(pen, p);
NocturneTheme.DrawRounded(g, box, NocturneScale.S(4), NocturneTheme.Neutral600);
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/ConfigurO/Controls/MoonSelect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ protected override void WndProc(ref Message m)
Color edge = Focused ? NocturneTheme.Accent
: _hover ? NocturneTheme.BorderStrong
: NocturneTheme.Divider;
using (GraphicsPath p = NocturneTheme.RoundedRect(r, NocturneTheme.RadiusMd))
using (Pen pen = new Pen(edge)) g.DrawPath(pen, p);
NocturneTheme.DrawRounded(g, r, NocturneTheme.RadiusMd, edge);

NocturneIcons.Draw(g, NocturneIcons.Caret,
Width - NocturneScale.S(24), (Height - NocturneScale.S(16)) / 2,
Expand Down
7 changes: 2 additions & 5 deletions src/ConfigurO/Controls/MoonToggle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,12 @@ protected override void OnPaint(PaintEventArgs e)
using (GraphicsPath p = NocturneTheme.RoundedRect(pill, h / 2))
{
using (SolidBrush b = new SolidBrush(fill)) g.FillPath(b, p);
using (Pen pen = new Pen(edge)) g.DrawPath(pen, p);
NocturneTheme.DrawRounded(g, pill, h / 2, edge);
}

if (Focused)
{
Rectangle ring = Rectangle.Inflate(pill, NocturneScale.S(2), NocturneScale.S(2));
using (GraphicsPath p = NocturneTheme.RoundedRect(ring, ring.Height / 2))
using (Pen pen = new Pen(NocturneTheme.Accent, NocturneScale.S(2)))
g.DrawPath(pen, p);
NocturneDraw.FocusRing(g, pill, pill.Height / 2);
}

float knobX = left + pad + _position * (w - knob - pad * 2);
Expand Down
47 changes: 38 additions & 9 deletions src/ConfigurO/Nocturne/Controls/NButton.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal enum NButtonStyle
/// </summary>
internal sealed class NButton : NControl, IButtonControl
{
bool _hover, _pressed;
bool _pressed;
string _icon;
NButtonStyle _style = NButtonStyle.Secondary;
bool _active;
Expand Down Expand Up @@ -113,8 +113,15 @@ int EffectiveIconSize
get { return IconSize > 0 ? NocturneScale.S(IconSize) : NocturneScale.S(15); }
}

protected override void OnMouseEnter(EventArgs e) { _hover = true; Invalidate(); base.OnMouseEnter(e); }
protected override void OnMouseLeave(EventArgs e) { _hover = _pressed = false; Invalidate(); base.OnMouseLeave(e); }
// Enter is handled entirely by NControl, which eases HoverAmount.
protected override void OnMouseLeave(EventArgs e)
{
// NControl unwinds the easing; this clears the logical flag the
// keyboard path guards on.
_pressed = false;
base.OnMouseLeave(e);
}

protected override void OnMouseDown(MouseEventArgs e)
{
// Clicking is not navigating: a ring here answers a question nobody
Expand All @@ -126,7 +133,7 @@ protected override void OnMouseDown(MouseEventArgs e)
}
_pressed = true; Focus(); Invalidate(); base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseEventArgs e) { _pressed = false; Invalidate(); base.OnMouseUp(e); }
protected override void OnMouseUp(MouseEventArgs e) { _pressed = false; base.OnMouseUp(e); }
protected override void OnGotFocus(EventArgs e) { Invalidate(); base.OnGotFocus(e); }
protected override void OnLostFocus(EventArgs e) { Invalidate(); base.OnLostFocus(e); }

Expand All @@ -140,7 +147,7 @@ protected override void OnKeyDown(KeyEventArgs e)
if (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter)
{
_pressed = true;
Invalidate();
SetPressed(true);
e.Handled = true;
}
base.OnKeyDown(e);
Expand All @@ -151,7 +158,7 @@ protected override void OnKeyUp(KeyEventArgs e)
if (_pressed && (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter))
{
_pressed = false;
Invalidate();
SetPressed(false);
PerformClick();
e.Handled = true;
}
Expand Down Expand Up @@ -191,9 +198,31 @@ void Render(PaintEventArgs e)
else { border = NocturneTheme.Divider; text = NocturneTheme.Text; }

if (!Enabled) { border = NocturneTheme.Border; text = NocturneTheme.TextDim; }
else if (_pressed) fill = _style == NButtonStyle.Secondary ? NocturneTheme.Border : NocturneTheme.PressedAccent;
else if (_hover) fill = _style == NButtonStyle.Secondary || _style == NButtonStyle.Icon
? NocturneTheme.HoverFill : NocturneTheme.HoverAccent;
else
{
// Hover and press used to be two discrete fills swapped in by
// an if/else. They are now one colour faded up from nothing,
// so the button arrives and leaves instead of blinking.
//
// Alpha-blended rather than mixed against a ground colour
// because these controls are transparent-backed: the parent
// has already painted whatever sits behind, and at amount 0
// there must be nothing here at all.
Color hoverFill = _style == NButtonStyle.Secondary || _style == NButtonStyle.Icon
? NocturneTheme.HoverFill : NocturneTheme.HoverAccent;
Color pressFill = _style == NButtonStyle.Secondary
? NocturneTheme.Border : NocturneTheme.PressedAccent;

float h = HoverAmount, p = PressAmount;
float amount = Math.Max(h, p);
if (amount > 0.004f)
{
// Press wins as it comes in, so a click deepens the hover
// tint rather than replacing it and jumping.
Color blended = p > 0f ? NocturneTheme.Mix(pressFill, hoverFill, p) : hoverFill;
fill = NocturneTheme.Alpha(blended, amount);
}
}

NocturneDraw.Card(g, r, fill, border, radius);
if (Focused && Enabled && NocturneDraw.ShowFocusRings)
Expand Down
89 changes: 89 additions & 0 deletions src/ConfigurO/Nocturne/Controls/NControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,71 @@ internal class NControl : Control
{
bool _subscribed;

readonly NAnim _hoverAnim;
readonly NAnim _pressAnim;

internal NControl()
{
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint |
ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw |
ControlStyles.SupportsTransparentBackColor, true);
BackColor = Color.Transparent;
ForeColor = NocturneTheme.Text;

// Press settles faster than hover: a click wants to feel immediate,
// where a pointer crossing a surface wants to feel unhurried.
_hoverAnim = new NAnim(Invalidate, 130);
_pressAnim = new NAnim(Invalidate, 90);

NocturneTheme.Changed += OnThemeChangedInternal;
NocturneScale.Changed += OnScaleChangedInternal;
_subscribed = true;
}

// ── Eased interaction state ─────────────────────────────────────
// Painters read these instead of a bool. At 0 the control is at rest,
// at 1 it is fully hovered or fully pressed, and in between it is on
// its way -- which is the whole difference between a surface that
// responds and one that switches.

/// <summary>0 at rest, 1 hovered, eased in between.</summary>
protected float HoverAmount { get { return _hoverAnim.Value; } }

/// <summary>0 at rest, 1 pressed, eased in between.</summary>
protected float PressAmount { get { return _pressAnim.Value; } }

/// <summary>Drives the hover easing. Called for you on enter and leave.</summary>
protected void SetHover(bool on) { _hoverAnim.To(on ? 1f : 0f); }

/// <summary>
/// Drives the press easing. The mouse path calls this for you; keyboard
/// activation has to say so itself, since Space and Enter produce no
/// mouse events.
/// </summary>
protected void SetPressed(bool on) { _pressAnim.To(on ? 1f : 0f); }

protected override void OnMouseEnter(EventArgs e) { SetHover(true); base.OnMouseEnter(e); }

protected override void OnMouseLeave(EventArgs e)
{
// A pointer that leaves mid-press must not leave the press behind:
// the mouse-up will land somewhere else and never come back here.
SetHover(false);
SetPressed(false);
base.OnMouseLeave(e);
}

protected override void OnMouseDown(MouseEventArgs e) { SetPressed(true); base.OnMouseDown(e); }
protected override void OnMouseUp(MouseEventArgs e) { SetPressed(false); base.OnMouseUp(e); }

protected override void OnEnabledChanged(EventArgs e)
{
// A control disabled under the pointer keeps its hover for as long
// as the pointer stays, and comes back wearing it.
if (!Enabled) { _hoverAnim.Set(0f); _pressAnim.Set(0f); }
base.OnEnabledChanged(e);
}

void OnThemeChangedInternal(object sender, EventArgs e)
{
if (IsDisposed) return;
Expand All @@ -55,6 +108,8 @@ protected override void Dispose(bool disposing)
{
NocturneTheme.Changed -= OnThemeChangedInternal;
NocturneScale.Changed -= OnScaleChangedInternal;
_hoverAnim.Dispose();
_pressAnim.Dispose();
_subscribed = false;
}
base.Dispose(disposing);
Expand All @@ -66,15 +121,47 @@ internal class NPanel : Panel
{
bool _subscribed;

readonly NAnim _hoverAnim;
readonly NAnim _pressAnim;

internal NPanel()
{
SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint |
ControlStyles.UserPaint | ControlStyles.ResizeRedraw, true);

// Mirrors NControl. Most panels are containers that never drive
// these, and an NAnim that is never told to move never starts its
// timer, so the pair costs a couple of managed objects and nothing
// else -- a WinForms Timer does not touch the OS until Start.
_hoverAnim = new NAnim(Invalidate, 130);
_pressAnim = new NAnim(Invalidate, 90);

NocturneTheme.Changed += OnThemeChangedInternal;
NocturneScale.Changed += OnScaleChangedInternal;
_subscribed = true;
}

/// <summary>0 at rest, 1 hovered, eased in between.</summary>
protected float HoverAmount { get { return _hoverAnim.Value; } }

/// <summary>0 at rest, 1 pressed, eased in between.</summary>
protected float PressAmount { get { return _pressAnim.Value; } }

protected void SetHover(bool on) { _hoverAnim.To(on ? 1f : 0f); }
protected void SetPressed(bool on) { _pressAnim.To(on ? 1f : 0f); }

protected override void OnMouseEnter(EventArgs e) { SetHover(true); base.OnMouseEnter(e); }

protected override void OnMouseLeave(EventArgs e)
{
SetHover(false);
SetPressed(false);
base.OnMouseLeave(e);
}

protected override void OnMouseDown(MouseEventArgs e) { SetPressed(true); base.OnMouseDown(e); }
protected override void OnMouseUp(MouseEventArgs e) { SetPressed(false); base.OnMouseUp(e); }

void OnThemeChangedInternal(object sender, EventArgs e)
{
if (IsDisposed) return;
Expand All @@ -98,6 +185,8 @@ protected override void Dispose(bool disposing)
{
NocturneTheme.Changed -= OnThemeChangedInternal;
NocturneScale.Changed -= OnScaleChangedInternal;
_hoverAnim.Dispose();
_pressAnim.Dispose();
_subscribed = false;
}
base.Dispose(disposing);
Expand Down
3 changes: 1 addition & 2 deletions src/ConfigurO/Nocturne/Controls/NLanguagePicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,7 @@ protected override void OnPaint(PaintEventArgs e)
g.DrawImage(entry.Flag, flag);
// Japan, and any other flag with a white field, has no edge of
// its own against a light surface.
using (Pen edge = new Pen(NocturneTheme.Border))
g.DrawRectangle(edge, flag.X, flag.Y, flag.Width - 1, flag.Height - 1);
NocturneTheme.DrawRounded(g, flag, 0, NocturneTheme.Border);

int tx = fx + flagW + pad;
// Each name is written in its own script, so the face has to be
Expand Down
Loading
Loading