summaryrefslogtreecommitdiffstatshomepage
path: root/icon.go
blob: e4870bfb6bec4321475368e0e5a67520da383f2b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
// Copyright 2011 The Walk Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build windows

package walk

import (
	"image"
	"path/filepath"
	"syscall"
	"unsafe"

	"golang.org/x/sys/windows"

	"github.com/lxn/win"
)

// Icon is a bitmap that supports transparency and combining multiple
// variants of an image in different resolutions.
type Icon struct {
	filePath  string
	index     int
	res       *uint16
	dpi2hIcon map[int]win.HICON
	size96dpi Size
	isStock   bool
	hasIndex  bool
}

type ExtractableIcon interface {
	FilePath_() string
	Index_() int
	Size_() int
}

func IconFrom(src interface{}, dpi int) (*Icon, error) {
	if src == nil {
		return nil, nil
	}

	img, err := ImageFrom(src)
	if err != nil {
		return nil, err
	}

	return iconCache.Icon(img, dpi)
}

func IconApplication() *Icon {
	return stockIcon(win.IDI_APPLICATION)
}

func IconError() *Icon {
	return stockIcon(win.IDI_ERROR)
}

func IconQuestion() *Icon {
	return stockIcon(win.IDI_QUESTION)
}

func IconWarning() *Icon {
	return stockIcon(win.IDI_WARNING)
}

func IconInformation() *Icon {
	return stockIcon(win.IDI_INFORMATION)
}

func IconWinLogo() *Icon {
	return stockIcon(win.IDI_WINLOGO)
}

func IconShield() *Icon {
	return stockIcon(win.IDI_SHIELD)
}

func stockIcon(id uintptr) *Icon {
	return &Icon{res: win.MAKEINTRESOURCE(id), size96dpi: defaultIconSize(), isStock: true}
}

// NewIconFromFile returns a new Icon, using the specified icon image file and default size.
func NewIconFromFile(filePath string) (*Icon, error) {
	return NewIconFromFileWithSize(filePath, Size{})
}

// NewIconFromFileWithSize returns a new Icon, using the specified icon image file and size.
func NewIconFromFileWithSize(filePath string, size Size) (*Icon, error) {
	if size.Width == 0 || size.Height == 0 {
		size = defaultIconSize()
	}

	return checkNewIcon(&Icon{filePath: filePath, size96dpi: size})
}

// NewIconFromResource returns a new Icon of default size, using the specified icon resource.
func NewIconFromResource(name string) (*Icon, error) {
	return NewIconFromResourceWithSize(name, Size{})
}

// NewIconFromResourceWithSize returns a new Icon of size size, using the specified icon resource.
func NewIconFromResourceWithSize(name string, size Size) (*Icon, error) {
	return newIconFromResource(syscall.StringToUTF16Ptr(name), size)
}

// NewIconFromResourceId returns a new Icon of default size, using the specified icon resource.
func NewIconFromResourceId(id int) (*Icon, error) {
	return NewIconFromResourceIdWithSize(id, Size{})
}

// NewIconFromResourceIdWithSize returns a new Icon of size size, using the specified icon resource.
func NewIconFromResourceIdWithSize(id int, size Size) (*Icon, error) {
	return newIconFromResource(win.MAKEINTRESOURCE(uintptr(id)), size)
}

func newIconFromResource(res *uint16, size Size) (*Icon, error) {
	if size.Width == 0 || size.Height == 0 {
		size = defaultIconSize()
	}

	return checkNewIcon(&Icon{res: res, size96dpi: size})
}

// NewIconFromSysDLL returns a new Icon, as identified by index of
// size 16x16 from the system DLL identified by dllBaseName.
func NewIconFromSysDLL(dllBaseName string, index int) (*Icon, error) {
	return NewIconFromSysDLLWithSize(dllBaseName, index, 16)
}

// NewIconFromSysDLLWithSize returns a new Icon, as identified by
// index of the desired size from the system DLL identified by dllBaseName.
func NewIconFromSysDLLWithSize(dllBaseName string, index, size int) (*Icon, error) {
	system32, err := windows.GetSystemDirectory()
	if err != nil {
		return nil, err
	}

	return checkNewIcon(&Icon{filePath: filepath.Join(system32, dllBaseName+".dll"), index: index, hasIndex: true, size96dpi: Size{size, size}})
}

// NewIconExtractedFromFile returns a new Icon, as identified by index of size 16x16 from filePath.
func NewIconExtractedFromFile(filePath string, index, _ int) (*Icon, error) {
	return checkNewIcon(&Icon{filePath: filePath, index: index, hasIndex: true, size96dpi: Size{16, 16}})
}

// NewIconExtractedFromFileWithSize returns a new Icon, as identified by index of the desired size from filePath.
func NewIconExtractedFromFileWithSize(filePath string, index, size int) (*Icon, error) {
	return checkNewIcon(&Icon{filePath: filePath, index: index, hasIndex: true, size96dpi: Size{size, size}})
}

// NewIconFromImage returns a new Icon at 96dpi, using the specified image.Image as source.
//
// Deprecated: Newer applications should use NewIconFromImageForDPI.
func NewIconFromImage(im image.Image) (ic *Icon, err error) {
	return NewIconFromImageForDPI(im, 96)
}

// NewIconFromImageForDPI returns a new Icon at given DPI, using the specified image.Image as source.
func NewIconFromImageForDPI(im image.Image, dpi int) (ic *Icon, err error) {
	hIcon, err := createAlphaCursorOrIconFromImage(im, image.Pt(0, 0), true)
	if err != nil {
		return nil, err
	}
	b := im.Bounds()
	return newIconFromHICONAndSize(hIcon, SizeTo96DPI(Size{b.Dx(), b.Dy()}, dpi), dpi), nil
}

// NewIconFromImageWithSize returns a new Icon of the given size in native pixels, using the
// specified Image as source.
func NewIconFromImageWithSize(image Image, size Size) (*Icon, error) {
	bmp, err := NewBitmapFromImageWithSize(image, size)
	if err != nil {
		return nil, err
	}

	return NewIconFromBitmap(bmp)
}

func newIconFromImageForDPI(image Image, dpi int) (*Icon, error) {
	size96dpi := image.Size()
	size := SizeFrom96DPI(size96dpi, dpi)

	bmp, err := NewBitmapFromImageWithSize(image, size)
	if err != nil {
		return nil, err
	}

	hIcon, err := createAlphaCursorOrIconFromBitmap(bmp, Point{}, true)
	if err != nil {
		return nil, err
	}

	return &Icon{dpi2hIcon: map[int]win.HICON{dpi: hIcon}, size96dpi: size96dpi}, nil
}

// NewIconFromBitmap returns a new Icon, using the specified Bitmap as source.
func NewIconFromBitmap(bmp *Bitmap) (ic *Icon, err error) {
	hIcon, err := createAlphaCursorOrIconFromBitmap(bmp, Point{}, true)
	if err != nil {
		return nil, err
	}
	return newIconFromHICONAndSize(hIcon, bmp.Size(), bmp.dpi), nil
}

// NewIconFromHICON returns a new Icon at 96dpi, using the specified win.HICON as source.
//
// Deprecated: Newer applications should use NewIconFromHICONForDPI.
func NewIconFromHICON(hIcon win.HICON) (ic *Icon, err error) {
	return NewIconFromHICONForDPI(hIcon, 96)
}

// NewIconFromHICONForDPI returns a new Icon at given DPI, using the specified win.HICON as source.
func NewIconFromHICONForDPI(hIcon win.HICON, dpi int) (ic *Icon, err error) {
	s, err := sizeFromHICON(hIcon)
	if err != nil {
		return nil, err
	}

	return newIconFromHICONAndSize(hIcon, SizeTo96DPI(s, dpi), dpi), nil
}

func newIconFromHICONAndSize(hIcon win.HICON, size Size, dpi int) *Icon {
	return &Icon{dpi2hIcon: map[int]win.HICON{dpi: hIcon}, size96dpi: size}
}

func checkNewIcon(icon *Icon) (*Icon, error) {
	if _, err := icon.handleForDPIWithError(96); err != nil {
		return nil, err
	}

	return icon, nil
}

func (i *Icon) handleForDPI(dpi int) win.HICON {
	hIcon, _ := i.handleForDPIWithError(dpi)
	return hIcon
}

func (i *Icon) handleForDPIWithError(dpi int) (win.HICON, error) {
	if i.dpi2hIcon == nil {
		i.dpi2hIcon = make(map[int]win.HICON)
	} else if handle, ok := i.dpi2hIcon[dpi]; ok {
		return handle, nil
	}

	var hInst win.HINSTANCE
	var name *uint16
	if i.filePath != "" {
		absFilePath, err := filepath.Abs(i.filePath)
		if err != nil {
			return 0, err
		}

		name = syscall.StringToUTF16Ptr(absFilePath)
	} else {
		if !i.isStock {
			if hInst = win.GetModuleHandle(nil); hInst == 0 {
				return 0, lastError("GetModuleHandle")
			}
		}

		name = i.res
	}

	var size Size
	if i.size96dpi.Width == 0 || i.size96dpi.Height == 0 {
		size = SizeFrom96DPI(defaultIconSize(), dpi)
	} else {
		size = SizeFrom96DPI(i.size96dpi, dpi)
	}

	var hIcon win.HICON

	if i.hasIndex {
		win.SHDefExtractIcon(
			name,
			int32(i.index),
			0,
			nil,
			&hIcon,
			win.MAKELONG(0, uint16(size.Width)))
		if hIcon == 0 {
			return 0, newError("SHDefExtractIcon")
		}
	} else {
		hr := win.HICON(win.LoadIconWithScaleDown(
			hInst,
			name,
			int32(size.Width),
			int32(size.Height),
			&hIcon))
		if hr < 0 || hIcon == 0 {
			return 0, lastError("LoadIconWithScaleDown")
		}
	}

	i.dpi2hIcon[dpi] = hIcon

	return hIcon, nil
}

// Dispose releases the operating system resources associated with the Icon.
func (i *Icon) Dispose() {
	if i.isStock || len(i.dpi2hIcon) == 0 {
		return
	}

	for dpi, hIcon := range i.dpi2hIcon {
		win.DestroyIcon(hIcon)
		delete(i.dpi2hIcon, dpi)
	}
}

func (i *Icon) draw(hdc win.HDC, location Point) error {
	dpi := dpiForHDC(hdc)
	size := SizeFrom96DPI(i.size96dpi, dpi)

	return i.drawStretched(hdc, Rectangle{location.X, location.Y, size.Width, size.Height})
}

func (i *Icon) drawStretched(hdc win.HDC, bounds Rectangle) error {
	dpi := int(float64(bounds.Width) / float64(i.size96dpi.Width) * 96.0)

	hIcon := i.handleForDPI(dpi)
	if hIcon == 0 {
		var dpiAvailMax int
		for dpiAvail, handle := range i.dpi2hIcon {
			if dpiAvail > dpiAvailMax {
				hIcon = handle
				dpiAvailMax = dpiAvail
			}
			if dpiAvail > dpi {
				break
			}
		}
	}

	if !win.DrawIconEx(hdc, int32(bounds.X), int32(bounds.Y), hIcon, int32(bounds.Width), int32(bounds.Height), 0, 0, win.DI_NORMAL) {
		return lastError("DrawIconEx")
	}

	return nil
}

// Size returns icon size in 1/96" units.
func (i *Icon) Size() Size {
	return i.size96dpi
}

// create an Alpha Icon or Cursor from an Image
// http://support.microsoft.com/kb/318876
func createAlphaCursorOrIconFromImage(im image.Image, hotspot image.Point, fIcon bool) (win.HICON, error) {
	bmp, err := NewBitmapFromImage(im)
	if err != nil {
		return 0, err
	}
	defer bmp.Dispose()

	return createAlphaCursorOrIconFromBitmap(bmp, Point{hotspot.X, hotspot.Y}, fIcon)
}

// createAlphaCursorOrIconFromBitmap creates a cursor/icon from a bitmap. hotspot coordinates are in native pixels.
func createAlphaCursorOrIconFromBitmap(bmp *Bitmap, hotspot Point, fIcon bool) (win.HICON, error) {
	// Create an empty mask bitmap.
	hMonoBitmap := win.CreateBitmap(int32(bmp.size.Width), int32(bmp.size.Height), 1, 1, nil)
	if hMonoBitmap == 0 {
		return 0, newError("CreateBitmap failed")
	}
	defer win.DeleteObject(win.HGDIOBJ(hMonoBitmap))

	var ii win.ICONINFO
	if fIcon {
		ii.FIcon = win.TRUE
	}
	ii.XHotspot = uint32(hotspot.X)
	ii.YHotspot = uint32(hotspot.Y)
	ii.HbmMask = hMonoBitmap
	ii.HbmColor = bmp.hBmp

	// Create the alpha cursor with the alpha DIB section.
	hIconOrCursor := win.CreateIconIndirect(&ii)

	return hIconOrCursor, nil
}

// sizeFromHICON returns icon size in native pixels.
func sizeFromHICON(hIcon win.HICON) (Size, error) {
	var ii win.ICONINFO
	var bi win.BITMAPINFO

	if !win.GetIconInfo(hIcon, &ii) {
		return Size{}, lastError("GetIconInfo")
	}
	defer win.DeleteObject(win.HGDIOBJ(ii.HbmMask))

	var hBmp win.HBITMAP
	if ii.HbmColor != 0 {
		hBmp = ii.HbmColor

		defer win.DeleteObject(win.HGDIOBJ(ii.HbmColor))
	} else {
		hBmp = ii.HbmMask
	}

	if 0 == win.GetObject(win.HGDIOBJ(hBmp), unsafe.Sizeof(bi), unsafe.Pointer(&bi)) {
		return Size{}, newError("GetObject")
	}

	return Size{int(bi.BmiHeader.BiWidth), int(bi.BmiHeader.BiHeight)}, nil
}

// defaultIconSize returns default small icon size in 1/92" units.
func defaultIconSize() Size {
	return Size{int(win.GetSystemMetricsForDpi(win.SM_CXSMICON, 96)), int(win.GetSystemMetricsForDpi(win.SM_CYSMICON, 96))}
}