@@ -183,6 +183,54 @@ function Get-ClampedAnnotationRect {
183183 [pscustomobject ]@ { X = $nx ; Y = $ny ; Width = $nw ; Height = $nh }
184184}
185185
186+ function Get-ZoomCenteredOffset {
187+ # Compute the scroll offset that keeps the content point currently under
188+ # ($CursorX, $CursorY) anchored after a scale change (Ctrl+MouseWheel zoom).
189+ # Content coords = viewport-offset + viewport-position; if the same image
190+ # pixel should land on the same viewport pixel after scaling, the offset
191+ # must shift by (OldOffset + Cursor) * NewScale/OldScale - Cursor.
192+ # Result is clamped to [0, Content - Viewport].
193+ param (
194+ [Parameter (Mandatory )] [double ]$CursorX ,
195+ [Parameter (Mandatory )] [double ]$CursorY ,
196+ [Parameter (Mandatory )] [double ]$OldScrollX ,
197+ [Parameter (Mandatory )] [double ]$OldScrollY ,
198+ [Parameter (Mandatory )] [double ]$OldScale ,
199+ [Parameter (Mandatory )] [double ]$NewScale ,
200+ [Parameter (Mandatory )] [double ]$ContentWidth ,
201+ [Parameter (Mandatory )] [double ]$ContentHeight ,
202+ [Parameter (Mandatory )] [double ]$ViewportWidth ,
203+ [Parameter (Mandatory )] [double ]$ViewportHeight
204+ )
205+ if ($OldScale -le 0 ) { $OldScale = 1.0 }
206+ $ratio = $NewScale / $OldScale
207+ $newX = ($OldScrollX + $CursorX ) * $ratio - $CursorX
208+ $newY = ($OldScrollY + $CursorY ) * $ratio - $CursorY
209+ $maxX = [math ]::Max(0.0 , $ContentWidth - $ViewportWidth )
210+ $maxY = [math ]::Max(0.0 , $ContentHeight - $ViewportHeight )
211+ $newX = [math ]::Max(0.0 , [math ]::Min($maxX , $newX ))
212+ $newY = [math ]::Max(0.0 , [math ]::Min($maxY , $newY ))
213+ [pscustomobject ]@ { X = $newX ; Y = $newY }
214+ }
215+
216+ function Copy-AnnotationList {
217+ # Deep-clone an enumerable of annotation pscustomobjects into a new
218+ # ArrayList. ArrayList stores references, so Snapshot-State / Do-Undo /
219+ # Do-Redo need fresh copies to avoid letting undo entries mutate each
220+ # other when the live Annotations list is edited.
221+ param ([AllowNull ()][AllowEmptyCollection ()] $Annotations )
222+ $copy = New-Object System.Collections.ArrayList
223+ if ($null -eq $Annotations ) { return , $copy }
224+ foreach ($a in $Annotations ) {
225+ [void ]$copy.Add ([pscustomobject ]@ {
226+ Type = $a.Type ; Color = $a.Color
227+ X = $a.X ; Y = $a.Y ; W = $a.W ; H = $a.H
228+ Text = $a.Text ; FontSize = $a.FontSize
229+ })
230+ }
231+ return , $copy
232+ }
233+
186234function Get-TrimmedRecent {
187235 # Keep only the top N items (for capping unbounded undo/redo stacks).
188236 # $Items is expected in most-recent-first order, matching [Stack].ToArray().
@@ -545,7 +593,7 @@ function Show-AboutWindow {
545593 [xml ]$xaml = @"
546594<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
547595 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
548- Title="About SnipIT" Width="420" Height="300"
596+ Title="About SnipIT" Width="420" Height="300" MinWidth="360" MinHeight="280"
549597 WindowStartupLocation="CenterScreen"
550598 WindowStyle="None" AllowsTransparency="True" ResizeMode="NoResize"
551599 Background="#FF1B1B1B">
@@ -565,7 +613,7 @@ function Show-AboutWindow {
565613 </Border>
566614 <StackPanel Grid.Row="1" Margin="24,18,24,0">
567615 <TextBlock Text="Professional snipping tool" FontSize="13" Foreground="#CCFFFFFF" Margin="0,0,0,14"/>
568- <TextBlock Text="PowerShell 7.5+ on .NET 9" FontSize="11" Foreground="#88FFFFFF " Margin="0,0,0,16"/>
616+ <TextBlock Text="PowerShell 7.5+ on .NET 9" FontSize="11" Foreground="#BBFFFFFF " Margin="0,0,0,16"/>
569617 <TextBlock Text="Hotkeys" FontSize="12" FontWeight="SemiBold" Foreground="White" Margin="0,0,0,6"/>
570618 <Grid>
571619 <Grid.ColumnDefinitions>
@@ -576,11 +624,11 @@ function Show-AboutWindow {
576624 <RowDefinition/><RowDefinition/><RowDefinition/>
577625 </Grid.RowDefinitions>
578626 <TextBlock Grid.Row="0" Grid.Column="0" Text="Ctrl + Shift + S" FontFamily="Consolas" Foreground="#DDDDDD"/>
579- <TextBlock Grid.Row="0" Grid.Column="1" Text="Smart capture" Foreground="#BBBBBB "/>
627+ <TextBlock Grid.Row="0" Grid.Column="1" Text="Smart capture" Foreground="#DDDDDD "/>
580628 <TextBlock Grid.Row="1" Grid.Column="0" Text="Ctrl + Shift + F" FontFamily="Consolas" Foreground="#DDDDDD"/>
581- <TextBlock Grid.Row="1" Grid.Column="1" Text="Full screen" Foreground="#BBBBBB "/>
629+ <TextBlock Grid.Row="1" Grid.Column="1" Text="Full screen" Foreground="#DDDDDD "/>
582630 <TextBlock Grid.Row="2" Grid.Column="0" Text="Ctrl + Shift + W" FontFamily="Consolas" Foreground="#DDDDDD"/>
583- <TextBlock Grid.Row="2" Grid.Column="1" Text="Active window" Foreground="#BBBBBB "/>
631+ <TextBlock Grid.Row="2" Grid.Column="1" Text="Active window" Foreground="#DDDDDD "/>
584632 </Grid>
585633 </StackPanel>
586634 <Border Grid.Row="2" Padding="20,12" Background="#22000000">
@@ -645,6 +693,9 @@ function Show-SmartOverlay {
645693 RenderOptions.BitmapScalingMode="NearestNeighbor"/>
646694 <Line X1="72" Y1="0" X2="72" Y2="144" Stroke="#990078D4" StrokeThickness="1"/>
647695 <Line X1="0" Y1="72" X2="144" Y2="72" Stroke="#990078D4" StrokeThickness="1"/>
696+ <!-- Tiny center dot — marks the exact pixel under the cursor -->
697+ <Rectangle Width="4" Height="4" Fill="#FFFF4081"
698+ HorizontalAlignment="Center" VerticalAlignment="Center"/>
648699 </Grid>
649700 </Border>
650701 <TextBlock x:Name="LoupeText" Foreground="White" FontFamily="Consolas"
@@ -842,7 +893,7 @@ function Show-PreviewWindow {
842893 <SolidColorBrush x:Key="AccentBrush" Color="#FF0078D4"/>
843894 <SolidColorBrush x:Key="AccentHoverBrush" Color="#330078D4"/>
844895 <SolidColorBrush x:Key="PanelOverlayBrush" Color="#22000000"/>
845- <SolidColorBrush x:Key="TextSecondaryBrush" Color="#AAFFFFFF "/>
896+ <SolidColorBrush x:Key="TextSecondaryBrush" Color="#DDFFFFFF "/>
846897 <SolidColorBrush x:Key="BorderLightBrush" Color="#33FFFFFF"/>
847898
848899 <!-- Active-tool visual state for the 4 annotation tools + Pin.
@@ -985,6 +1036,11 @@ function Show-PreviewWindow {
9851036 </Button>
9861037 </StackPanel>
9871038 </Border>
1039+
1040+ <!-- Resize grip affordance. Chromeless windows still accept edge-drag,
1041+ but this makes the corner discoverable. -->
1042+ <ResizeGrip Grid.Row="3" HorizontalAlignment="Right" VerticalAlignment="Bottom"
1043+ Width="14" Height="14" Opacity="0.55" IsTabStop="False"/>
9881044 </Grid>
9891045</Window>
9901046"@
@@ -1151,63 +1207,30 @@ function Show-PreviewWindow {
11511207 }
11521208
11531209 function script :Snapshot-State {
1154- # Deep-copy current annotations into undo stack, clear redo
1155- $copy = New-Object System.Collections.ArrayList
1156- foreach ($a in $state.Annotations ) {
1157- [void ]$copy.Add ([pscustomobject ]@ {
1158- Type = $a.Type ; Color = $a.Color
1159- X = $a.X ; Y = $a.Y ; W = $a.W ; H = $a.H
1160- Text = $a.Text ; FontSize = $a.FontSize
1161- })
1162- }
1163- $state.UndoStack.Push ($copy )
1210+ $state.UndoStack.Push ( (Copy-AnnotationList $state.Annotations ) )
11641211 $state.RedoStack.Clear ()
11651212 Trim- SnipStack $state.UndoStack
11661213 }
11671214
11681215 function script :Restore-State {
11691216 param ($snapshot )
11701217 $state.Annotations.Clear ()
1171- foreach ($a in $snapshot ) {
1172- [void ]$state.Annotations.Add ([pscustomobject ]@ {
1173- Type = $a.Type ; Color = $a.Color
1174- X = $a.X ; Y = $a.Y ; W = $a.W ; H = $a.H
1175- Text = $a.Text ; FontSize = $a.FontSize
1176- })
1177- }
1218+ foreach ($a in (Copy-AnnotationList $snapshot )) { [void ]$state.Annotations.Add ($a ) }
11781219 Render- Annotations
11791220 }
11801221
11811222 function script :Do-Undo {
11821223 if ($state.UndoStack.Count -eq 0 ) { return }
1183- $current = New-Object System.Collections.ArrayList
1184- foreach ($a in $state.Annotations ) {
1185- [void ]$current.Add ([pscustomobject ]@ {
1186- Type = $a.Type ; Color = $a.Color
1187- X = $a.X ; Y = $a.Y ; W = $a.W ; H = $a.H
1188- Text = $a.Text ; FontSize = $a.FontSize
1189- })
1190- }
1191- $state.RedoStack.Push ($current )
1224+ $state.RedoStack.Push ( (Copy-AnnotationList $state.Annotations ) )
11921225 Trim- SnipStack $state.RedoStack
1193- $prev = $state.UndoStack.Pop ()
1194- Restore-State $prev
1226+ Restore-State $state.UndoStack.Pop ()
11951227 }
11961228
11971229 function script :Do-Redo {
11981230 if ($state.RedoStack.Count -eq 0 ) { return }
1199- $current = New-Object System.Collections.ArrayList
1200- foreach ($a in $state.Annotations ) {
1201- [void ]$current.Add ([pscustomobject ]@ {
1202- Type = $a.Type ; Color = $a.Color
1203- X = $a.X ; Y = $a.Y ; W = $a.W ; H = $a.H
1204- Text = $a.Text ; FontSize = $a.FontSize
1205- })
1206- }
1207- $state.UndoStack.Push ($current )
1231+ $state.UndoStack.Push ( (Copy-AnnotationList $state.Annotations ) )
12081232 Trim- SnipStack $state.UndoStack
1209- $next = $state.RedoStack.Pop ()
1210- Restore-State $next
1233+ Restore-State $state.RedoStack.Pop ()
12111234 }
12121235
12131236 # Named color picker. Tests and the real swatch click handler both call
@@ -1237,18 +1260,24 @@ function Show-PreviewWindow {
12371260 $colorBar.Children.Clear ()
12381261 foreach ($name in $palette.Keys ) {
12391262 $rgb = $palette [$name ]
1263+ $isActive = ($state.ActiveColor -eq $name )
12401264 $sw = New-Object System.Windows.Controls.Border
1241- $sw.Width = 26 ; $sw.Height = 26
1265+ # Slightly larger + accent-coloured ring when active — makes the
1266+ # selected swatch unambiguous at a glance.
1267+ if ($isActive ) { $sw.Width = 30 ; $sw.Height = 30 }
1268+ else { $sw.Width = 26 ; $sw.Height = 26 }
12421269 $sw.Margin = New-Object System.Windows.Thickness 3 , 0 , 3 , 0
12431270 $sw.CornerRadius = New-Object System.Windows.CornerRadius 0
12441271 $sw.Background = New-Object System.Windows.Media.SolidColorBrush(
12451272 (To- WpfColor 255 $rgb.R $rgb.G $rgb.B ))
1246- $sw .BorderBrush = New-Object System.Windows.Media.SolidColorBrush(
1247- ([ System.Windows.Media.Colors ]::White))
1248- $sw .BorderThickness = if ( $state .ActiveColor -eq $name ) {
1249- New-Object System.Windows.Thickness 2
1273+ if ( $isActive ) {
1274+ $sw .BorderBrush = New-Object System.Windows.Media.SolidColorBrush(
1275+ ([ System.Windows.Media.Color ]::FromArgb( 255 , 0x00 , 0x78 , 0xD4 )))
1276+ $sw .BorderThickness = New-Object System.Windows.Thickness 3
12501277 } else {
1251- New-Object System.Windows.Thickness 0
1278+ $sw.BorderBrush = New-Object System.Windows.Media.SolidColorBrush(
1279+ ([System.Windows.Media.Colors ]::White))
1280+ $sw.BorderThickness = New-Object System.Windows.Thickness 0
12521281 }
12531282 $sw.Cursor = [System.Windows.Input.Cursors ]::Hand
12541283 # Non-focusable so clicking a swatch while a text box is open
@@ -1719,8 +1748,23 @@ function Show-PreviewWindow {
17191748
17201749 $win.Add_PreviewMouseWheel ({
17211750 if (([System.Windows.Input.Keyboard ]::Modifiers -band [System.Windows.Input.ModifierKeys ]::Control) -ne 0 ) {
1722- $factor = if ($_.Delta -gt 0 ) { 1.25 } else { 1 / 1.25 }
1751+ $factor = if ($_.Delta -gt 0 ) { 1.25 } else { 1 / 1.25 }
1752+ $oldScale = $layoutScale.ScaleX
1753+ $cursor = $_.GetPosition ($scroller )
1754+ $oldSx = $scroller.HorizontalOffset
1755+ $oldSy = $scroller.VerticalOffset
17231756 & $zoomBy $factor
1757+ $newScale = $layoutScale.ScaleX
1758+ $offset = Get-ZoomCenteredOffset `
1759+ - CursorX $cursor.X - CursorY $cursor.Y `
1760+ - OldScrollX $oldSx - OldScrollY $oldSy `
1761+ - OldScale $oldScale - NewScale $newScale `
1762+ - ContentWidth ($Bitmap.Width * $newScale ) `
1763+ - ContentHeight ($Bitmap.Height * $newScale ) `
1764+ - ViewportWidth $scroller.ViewportWidth `
1765+ - ViewportHeight $scroller.ViewportHeight
1766+ $scroller.ScrollToHorizontalOffset ($offset.X )
1767+ $scroller.ScrollToVerticalOffset ( $offset.Y )
17241768 $_.Handled = $true
17251769 }
17261770 }.GetNewClosure())
@@ -1793,12 +1837,20 @@ function Show-PreviewWindow {
17931837 return $flat
17941838 }
17951839
1796- $win.FindName (' CopyBtn' ).Add_Click({
1840+ $copyBtn = $win.FindName (' CopyBtn' )
1841+ $copyBtn.Add_Click ({
17971842 $flat = Get-FlattenedBitmap
17981843 $clipSrc = Convert-BitmapToBitmapSource $flat
17991844 [System.Windows.Clipboard ]::SetImage($clipSrc )
18001845 if ($flat -ne $Bitmap ) { $flat.Dispose () }
1801- })
1846+ # Brief confirmation — second StackPanel child is the "Copy" label.
1847+ $lbl = $copyBtn.Content.Children [1 ]
1848+ $lbl.Text = ' Copied!'
1849+ $timer = New-Object System.Windows.Threading.DispatcherTimer
1850+ $timer.Interval = [timespan ]::FromMilliseconds(1200 )
1851+ $timer.Add_Tick ({ $lbl.Text = ' Copy' ; $timer.Stop () }.GetNewClosure())
1852+ $timer.Start ()
1853+ }.GetNewClosure())
18021854 $win.FindName (' SaveBtn' ).Add_Click({
18031855 $flat = Get-FlattenedBitmap
18041856 Save-CaptureToFile - Bitmap $flat | Out-Null
0 commit comments