mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
[v3-Windows] New DIP system for Enhanced High DPI Monitor Support (#3665)
* [v3-Windows] New DIP system for Enhanced High DPI Monitor Support * Update changelog * Remove asset middleware * Remove SetThreadDpiAwarenessContext() * Fix macOS build. * Fill missing screens fields (linux, darwin) * Skip DPI transformation on unsupported platforms * Simplify distanceFromRectSquared() * Update v3/pkg/application/screenmanager.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: Lea Anthony <lea.anthony@gmail.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
parent
d5a6d7719c
commit
efe0c8d534
33 changed files with 3318 additions and 498 deletions
|
|
@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## v3.0.0-alpha.7 - 2024-09-18
|
||||
|
||||
### Added
|
||||
- [windows] New DIP system for Enhanced High DPI Monitor Support by [mmghv](https://github.com/mmghv) in [#3665](https://github.com/wailsapp/wails/pull/3665)
|
||||
- [windows] Window class name option by [windom](https://github.com/windom/) in [#3682](https://github.com/wailsapp/wails/pull/3682)
|
||||
- Services have been expanded to provide plugin functionality. By [atterpac](https://github.com/atterpac) and [leaanthony](https://github.com/leaanthony) in [#3570](https://github.com/wailsapp/wails/pull/3570)
|
||||
|
||||
|
|
|
|||
206
v3/examples/screen/assets/examples.js
Normal file
206
v3/examples/screen/assets/examples.js
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
window.examples = [
|
||||
[
|
||||
// Normal examples (demonstrate real life scenarios)
|
||||
{
|
||||
name: "Single 4k monitor",
|
||||
screens: [
|
||||
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Two monitors",
|
||||
screens: [
|
||||
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
||||
{id: 2, w: 1920, h: 1080, s: 1, parent: {id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI`},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Two monitors (2)",
|
||||
screens: [
|
||||
{id: 1, w: 1920, h: 1080, s: 1, name: `23" FHD 96DPI`},
|
||||
{id: 2, w: 1920, h: 1080, s: 1.25, parent: {id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI (125%)`},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Three monitors",
|
||||
screens: [
|
||||
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
||||
{id: 2, w: 1920, h: 1080, s: 1, parent: {id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI`},
|
||||
{id: 3, w: 1920, h: 1080, s: 1.25, parent: {id: 1, align: "l", offset: 0}, name: `23" FHD 96DPI (125%)`},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Four monitors",
|
||||
screens: [
|
||||
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
||||
{id: 2, w: 1920, h: 1080, s: 1, parent: {id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI`},
|
||||
{id: 3, w: 1920, h: 1080, s: 1.25, parent: {id: 2, align: "b", offset: 0}, name: `23" FHD 96DPI (125%)`},
|
||||
{id: 4, w: 1080, h: 1920, s: 1, parent: {id: 1, align: "l", offset: 0}, name: `23" FHD (90deg)`},
|
||||
]
|
||||
},
|
||||
],
|
||||
[
|
||||
// Test cases examples (demonstrate the algorithm basics)
|
||||
{
|
||||
name: "Child scaled, Start offset",
|
||||
screens: [
|
||||
{id: 1, w: 1200, h: 1200, s: 1, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1200, s: 1.5, parent: {id: 1, align: "r", offset: 600}, name: "Child"},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Child scaled, End offset",
|
||||
screens: [
|
||||
{id: 1, w: 1200, h: 1200, s: 1, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1200, s: 1.5, parent: {id: 1, align: "r", offset: -600}, name: "Child"},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Parent scaled, Start offset percent",
|
||||
screens: [
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1200, s: 1, parent: {id: 1, align: "r", offset: 600}, name: "Child"},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Parent scaled, End offset percent",
|
||||
screens: [
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1200, s: 1, parent: {id: 1, align: "r", offset: -600}, name: "Child"},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Parent scaled, Start align",
|
||||
screens: [
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1100, s: 1, parent: {id: 1, align: "r", offset: 0}, name: "Child"},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Parent scaled, End align",
|
||||
screens: [
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1200, s: 1, parent: {id: 1, align: "r", offset: 0}, name: "Child"},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Parent scaled, in-between",
|
||||
screens: [
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1500, s: 1, parent: {id: 1, align: "r", offset: -200}, name: "Child"},
|
||||
]
|
||||
},
|
||||
],
|
||||
[
|
||||
// Edge cases examples
|
||||
{
|
||||
name: "Parent order (5 is parent of 4)",
|
||||
screens: [
|
||||
{id: 1, w: 1920, h: 1080, s: 1},
|
||||
{id: 2, w: 1024, h: 600, s: 1.25, parent: {id: 1, align: "r", offset: -200}},
|
||||
{id: 3, w: 800, h: 800, s: 1.25, parent: {id: 2, align: "b", offset: 0}},
|
||||
{id: 4, w: 800, h: 1080, s: 1.5, parent: {id: 2, align: "re", offset: 100}},
|
||||
{id: 5, w: 600, h: 600, s: 1, parent: {id: 3, align: "r", offset: 100}},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "de-intersection reparent",
|
||||
screens: [
|
||||
{id: 1, w: 1920, h: 1080, s: 1},
|
||||
{id: 2, w: 1680, h: 1050, s: 1.25, parent: {id: 1, align: "r", offset: 10}},
|
||||
{id: 3, w: 1440, h: 900, s: 1.5, parent: {id: 1, align: "le", offset: 150}},
|
||||
{id: 4, w: 1024, h: 768, s: 1, parent: {id: 3, align: "bc", offset: -200}},
|
||||
{id: 5, w: 1024, h: 768, s: 1.25, parent: {id: 4, align: "r", offset: 400}},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "de-intersection (unattached child)",
|
||||
screens: [
|
||||
{id: 1, w: 1920, h: 1080, s: 1},
|
||||
{id: 2, w: 1024, h: 768, s: 1.5, parent: {id: 1, align: "le", offset: 10}},
|
||||
{id: 3, w: 1024, h: 768, s: 1.25, parent: {id: 2, align: "b", offset: 100}},
|
||||
{id: 4, w: 1024, h: 768, s: 1, parent: {id: 3, align: "r", offset: 500}},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Multiple de-intersection",
|
||||
screens: [
|
||||
{id: 1, w: 1920, h: 1080, s: 1},
|
||||
{id: 2, w: 1024, h: 768, s: 1, parent: {id: 1, align: "be", offset: 0}},
|
||||
{id: 3, w: 1024, h: 768, s: 1, parent: {id: 2, align: "b", offset: 300}},
|
||||
{id: 4, w: 1024, h: 768, s: 1.5, parent: {id: 2, align: "le", offset: 100}},
|
||||
{id: 5, w: 1024, h: 768, s: 1, parent: {id: 4, align: "be", offset: 100}},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Multiple de-intersection (left-side)",
|
||||
screens: [
|
||||
{id: 1, w: 1920, h: 1080, s: 1},
|
||||
{id: 2, w: 1024, h: 768, s: 1, parent: {id: 1, align: "le", offset: 0}},
|
||||
{id: 3, w: 1024, h: 768, s: 1, parent: {id: 2, align: "b", offset: 300}},
|
||||
{id: 4, w: 1024, h: 768, s: 1.5, parent: {id: 2, align: "le", offset: 100}},
|
||||
{id: 5, w: 1024, h: 768, s: 1, parent: {id: 4, align: "be", offset: 100}},
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Parent de-intersection child offset",
|
||||
screens: [
|
||||
{id: 1, w: 1600, h: 1600, s: 1.5},
|
||||
{id: 2, w: 800, h: 800, s: 1, parent: {id: 1, align: "r", offset: 0}},
|
||||
{id: 3, w: 800, h: 800, s: 1, parent: {id: 1, align: "r", offset: 800}},
|
||||
{id: 4, w: 800, h: 1600, s: 1, parent: {id: 2, align: "r", offset: 0}},
|
||||
]
|
||||
},
|
||||
],
|
||||
].map(sections => sections.map(layout => {
|
||||
return parseLayout(layout)
|
||||
}))
|
||||
|
||||
function parseLayout(layout) {
|
||||
const screens = []
|
||||
|
||||
for (const screen of layout.screens) {
|
||||
let x = 0, y = 0
|
||||
const {w, h} = screen
|
||||
|
||||
if (screen.parent) {
|
||||
const parent = screens.find(s => s.ID == screen.parent.id).Bounds
|
||||
const offset = screen.parent.offset
|
||||
let align = screen.parent.align
|
||||
let align2 = ""
|
||||
|
||||
if (align.length == 2) {
|
||||
align2 = align.charAt(1)
|
||||
align = align.charAt(0)
|
||||
}
|
||||
|
||||
x = parent.X
|
||||
y = parent.Y
|
||||
// t: top, b: bottom, l: left, r: right, e: edge, c: corner
|
||||
if (align == "t" || align == "b") {
|
||||
x += offset + (align2 == "e" || align2 == "c" ? parent.Width : 0) - (align2 == "e" ? w : 0)
|
||||
y += (align == "t" ? -h : parent.Height)
|
||||
} else {
|
||||
y += offset + (align2 == "e" || align2 == "c" ? parent.Height : 0) - (align2 == "e" ? h : 0)
|
||||
x += (align == "l" ? -w : parent.Width)
|
||||
}
|
||||
}
|
||||
|
||||
screens.push({
|
||||
ID: `${screen.id}`,
|
||||
Name: screen.name ?? `Display${screen.id}`,
|
||||
ScaleFactor: Math.round(screen.s * 100) / 100,
|
||||
X: x,
|
||||
Y: y,
|
||||
Size: {Width: w, Height: h},
|
||||
Bounds: {X: x, Y: y, Width: w, Height: h},
|
||||
PhysicalBounds: {X: x, Y: y, Width: w, Height: h},
|
||||
WorkArea: {X: x, Y: y, Width: w, Height: h-Math.round(40*screen.s)},
|
||||
PhysicalWorkArea: {X: x, Y: y, Width: w, Height: h-Math.round(40*screen.s)},
|
||||
IsPrimary: screen.id == 1,
|
||||
Rotation: 0
|
||||
})
|
||||
}
|
||||
|
||||
return {name: layout.name, screens}
|
||||
}
|
||||
|
|
@ -4,65 +4,138 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>Screens Demo</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
padding-top: 75px;
|
||||
margin: 0 auto;
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
background-color: white;
|
||||
}
|
||||
#container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
text-align: -webkit-center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 1em;
|
||||
svg {
|
||||
user-select: none;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
|
||||
.monitor {
|
||||
width: 300px;
|
||||
height: 150px;
|
||||
overflow-y: scroll;
|
||||
border: solid 1em #333;
|
||||
border-radius: .5em;
|
||||
background: lightblue;
|
||||
.controls > div {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.monitor::-webkit-scrollbar {
|
||||
width: 15px;
|
||||
.controls > div > span:first-child {
|
||||
display: inline-block;
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
.monitor::-webkit-scrollbar-thumb {
|
||||
background: #666;
|
||||
.controls .radio-btn {
|
||||
display: inline-block;
|
||||
padding: 0 3px 2px;
|
||||
border-radius: 5px;
|
||||
min-width: 1.5em;
|
||||
text-align: center;
|
||||
background-color: #ecf3f7;
|
||||
box-shadow: 1px 1px 3px 0px #00000070;
|
||||
border: solid 1px #9a9a9a;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #888;
|
||||
.controls .radio-btn.active {
|
||||
background-color: #d0d8dc;
|
||||
box-shadow: inset 1px 1px 3px 0px #00000070;
|
||||
}
|
||||
.center {
|
||||
text-align: -webkit-center;
|
||||
#probe-buttons:not(.active) button:last-child {
|
||||
display: none;
|
||||
}
|
||||
#probe-buttons.active button:not(:last-child) {
|
||||
display: none;
|
||||
}
|
||||
.advanced {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<!-- Remove the underscore "_" from the class name to show test buttons in advanced move -->
|
||||
<div class="advanced_" style="display: none;">
|
||||
<button onclick="document.location.reload()">Reload</button>
|
||||
<button onclick="test1()">Test1</button>
|
||||
<button onclick="test2()">Test2</button>
|
||||
<hr>
|
||||
</div>
|
||||
<div class="advanced">
|
||||
<label>
|
||||
✥
|
||||
<input id="step" type="number" value="50" style="width: 55px" title="DIP Step, Arrow Move" onkeydown="arrowMove(event)">
|
||||
</label>
|
||||
X:
|
||||
<button onclick="updateDipRect(-step.value)"><</button>
|
||||
<button onclick="updateDipRect(+step.value)">></button>
|
||||
Width:
|
||||
<button onclick="updateDipRect(0, 0, -step.value)">-</button>
|
||||
<button onclick="updateDipRect(0, 0, +step.value)">+</button>
|
||||
|
||||
<input id="x" type="number" value="100" style="width: 55px" title="X">
|
||||
<input id="y" type="number" value="100" style="width: 55px" title="Y">
|
||||
|
||||
<input id="w" type="number" value="800" style="width: 55px" title="Width">
|
||||
<input id="h" type="number" value="600" style="width: 55px" title="Height">
|
||||
|
||||
<button onclick="drawRect({X: +x.value, Y: +y.value, Width: +w.value, Height: +h.value})">Set rect</button>
|
||||
|
||||
Layers: <input id="slider" type="range" min="1" max="5" value="3" style="width: 70px" oninput="drawRect()">
|
||||
<hr>
|
||||
</div>
|
||||
<div class="controls" onmousedown="radioBtnClick(event)">
|
||||
<div id="layout-selector" class="radio-btn-group">
|
||||
<span>Screens</span>:
|
||||
<span class="radio-btn" data-value="0">System</span>
|
||||
<span class="advanced">
|
||||
<span> - Examples </span>
|
||||
<select id="examples-type" onchange="setExamplesType(event.target.value)">
|
||||
<option value="0" selected>Normal</option>
|
||||
<option value="1">Test cases</option>
|
||||
<option value="2">Edge cases</option>
|
||||
</select> :
|
||||
<span id="examples-list"></span>
|
||||
<small id="example-name"></small>
|
||||
</span>
|
||||
</div>
|
||||
<div id="coordinate-selector" class="radio-btn-group">
|
||||
<span>Coordinates</span>:
|
||||
<span class="radio-btn active" data-value="0">Physical (PX)</span>
|
||||
<span class="radio-btn" data-value="1">Logical (DIP)</span>
|
||||
<label><input type="checkbox" id="retain-viewbox" onchange="draw()" checked> Retain viewbox</label>
|
||||
<button onclick="showAdvanced(event)" style="margin-left: 1em">Show advanced</button>
|
||||
|
||||
<span id="probe-buttons" class="advanced">
|
||||
<button onclick="probeLayout()">Probe layout</button>
|
||||
<button onclick="probeAllExamples()">Probe all examples</button>
|
||||
<button onclick="window.cancelProbing = true">Cancel Probing</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
id="svg"
|
||||
viewBox="0 0 500 500"
|
||||
fill="none"
|
||||
stroke="none"
|
||||
preserveAspectRatio="xMinYMin"
|
||||
>
|
||||
<defs>
|
||||
<svg id="point1" preserveAspectRatio="xMinYMin" viewBox="0 0 100 100"><path d="M9 1H2.4L7.1 5.7 5.7 7.1 1 2.4V9H-1V2.3L-5.7 7-7.1 5.6-2.6 1H-9V-1H-2.4L-7.1-5.7-6-7-1-2.4V-9H1V-2.6L5.6-7.2 7-5.7 2.3-1H9V1Z" /></svg>
|
||||
<path id="point2" d="M9 1H2.4L7.1 5.7 5.7 7.1 1 2.4V9H-1V2.3L-5.7 7-7.1 5.6-2.6 1H-9V-1H-2.4L-7.1-5.7-6-7-1-2.4V-9H1V-2.6L5.6-7.2 7-5.7 2.3-1H9V1Z" />
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import * as wails from "/wails/runtime.js";
|
||||
|
||||
let monitors = wails.Screens.GetAll();
|
||||
monitors.then((result) => {
|
||||
console.log({result})
|
||||
let html = "";
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
html += "<article class='center'><div class='monitor'><div style='padding:5px;'>";
|
||||
let hidpi = result[i].Scale === 2 ? " (HiDPI)" : "";
|
||||
html += "<h3 class='center'>"+result[i].Name+"</h3>";
|
||||
html += "<h4 class='center'>"+hidpi+"</h4>";
|
||||
html += "<h4 class='center'>"+result[i].Size.Width+"x"+result[i].Size.Height+"</h4>";
|
||||
html += "</div></div></article>";
|
||||
}
|
||||
document.body.innerHTML = html;
|
||||
})
|
||||
</script>
|
||||
<script src="/wails/runtime.js" type="module"></script>
|
||||
<script id="script" src="./examples.js"></script>
|
||||
<script id="script" src="./main.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
406
v3/examples/screen/assets/main.js
Normal file
406
v3/examples/screen/assets/main.js
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
setExamplesType(document.getElementById('examples-type').value, 0)
|
||||
|
||||
function setExamplesType(type, autoSelectLayout = 1) {
|
||||
window.examples_type = parseInt(type)
|
||||
document.getElementById('examples-list').innerHTML = examples[examples_type].map((layout, i) => {
|
||||
return `<span class="radio-btn" data-value="${i + 1}" title="${layout.name}">${i + 1}</span>`
|
||||
}).join("\n")
|
||||
if (autoSelectLayout != null) setLayout(autoSelectLayout)
|
||||
}
|
||||
|
||||
async function setLayout(indexOrLayout, physicalCoordinate = true) {
|
||||
if (typeof indexOrLayout == 'number') {
|
||||
await radioBtnClick(null, `#layout-selector [data-value="${indexOrLayout}"]`)
|
||||
} else {
|
||||
document.querySelectorAll('#layout-selector .active').forEach(el => el.classList.remove('active'))
|
||||
window.layout = indexOrLayout
|
||||
window.point = null
|
||||
window.rect = null
|
||||
await processLayout()
|
||||
await draw()
|
||||
}
|
||||
|
||||
const physical = !parseInt(document.querySelector('#coordinate-selector .active').dataset.value)
|
||||
if (physical != physicalCoordinate) {
|
||||
await setCoordinateType(physicalCoordinate)
|
||||
}
|
||||
}
|
||||
|
||||
async function setCoordinateType(physicalCoordinate = true) {
|
||||
await radioBtnClick(null, `#coordinate-selector [data-value="${physicalCoordinate ? 0 : 1}"]`)
|
||||
}
|
||||
|
||||
async function radioBtnClick(e, selector) {
|
||||
if (e == null) {
|
||||
e = new Event("mousedown")
|
||||
document.querySelector(selector).dispatchEvent(e)
|
||||
}
|
||||
if (!e.target.classList.contains('radio-btn')) return
|
||||
const btnGroup = e.target.closest('.radio-btn-group')
|
||||
btnGroup.querySelectorAll('.radio-btn.active').forEach(el => el.classList.remove('active'))
|
||||
e.target.classList.add('active')
|
||||
|
||||
if (btnGroup.id == 'layout-selector') {
|
||||
window.point = null
|
||||
window.rect = null
|
||||
await processLayout()
|
||||
}
|
||||
|
||||
await draw()
|
||||
}
|
||||
|
||||
async function processLayout() {
|
||||
const layoutBtn = document.querySelector('#layout-selector .active')
|
||||
const i = layoutBtn ? parseInt(layoutBtn.dataset.value) : -1
|
||||
if (i == 0) {
|
||||
// system screens
|
||||
window.layout = {
|
||||
name: '',
|
||||
screens: await callBinding('main.ScreenService.GetSystemScreens'),
|
||||
}
|
||||
} else {
|
||||
if (i > 0) {
|
||||
// example layouts
|
||||
window.layout = structuredClone(examples[examples_type][i - 1])
|
||||
}
|
||||
layout.screens = await callBinding('main.ScreenService.ProcessExampleScreens', layout.screens)
|
||||
}
|
||||
document.getElementById('example-name').textContent = layout.name
|
||||
}
|
||||
|
||||
async function draw() {
|
||||
console.log(layout)
|
||||
let minX = 0, minY = 0, maxX = 0, maxY = 0;
|
||||
let html = '';
|
||||
|
||||
const physical = !parseInt(document.querySelector('#coordinate-selector .active').dataset.value)
|
||||
const retainViewbox = document.querySelector('#retain-viewbox').checked
|
||||
|
||||
layout.screens.forEach(screen => {
|
||||
const b = physical ? screen.PhysicalBounds : screen.Bounds
|
||||
const wa = physical ? screen.PhysicalWorkArea : screen.WorkArea
|
||||
const vbBounds = retainViewbox ? [screen.Bounds, screen.PhysicalBounds] : [b]
|
||||
|
||||
minX = Math.min(minX, ...vbBounds.map(b => b.X))
|
||||
minY = Math.min(minY, ...vbBounds.map(b => b.Y))
|
||||
maxX = Math.max(maxX, ...vbBounds.map(b => b.X + b.Width))
|
||||
maxY = Math.max(maxY, ...vbBounds.map(b => b.Y + b.Height))
|
||||
|
||||
html += `
|
||||
<rect x="${b.X}" y="${b.Y}" width="${b.Width}" height="${b.Height}" fill="#00ceff" />
|
||||
<rect x="${wa.X}" y="${wa.Y}" width="${wa.Width}" height="${wa.Height}" fill="#def9ff" />
|
||||
<rect x="${b.X + 1}" y="${b.Y + 1}" width="${b.Width - 2}" height="${b.Height - 2}" stroke="black" stroke-width="2" />
|
||||
|
||||
<g transform="translate(${b.X}, ${b.Y})" fill="black">
|
||||
<text x="10" y="10" text-anchor="start" dominant-baseline="hanging" font-size="50">(${b.X}, ${b.Y})</text>
|
||||
<g transform="translate(${b.Width / 2}, ${b.Height / 2})" text-anchor="middle" font-size="100">
|
||||
<text x="0" y="-1.5em" fill="${screen.IsPrimary ? '#006bff' : '#5881ba'}" font-weight="bold">${screen.Name}</text>
|
||||
<text x="0" y="0">${b.Width} x ${b.Height}</text>
|
||||
<text x="0" y="1.2em">Scale factor: ${screen.ScaleFactor}</text>
|
||||
</g>
|
||||
</g>
|
||||
`
|
||||
})
|
||||
|
||||
const svg = document.getElementById('svg')
|
||||
svg.innerHTML = `
|
||||
${svg.querySelector('& > defs').outerHTML}
|
||||
<rect x="${minX}" y="${minY}" width="${maxX - minX}" height="${maxY - minY}" fill="antiquewhite" />
|
||||
${html}
|
||||
<g id="rects"></g>
|
||||
<g id="points"></g>
|
||||
`
|
||||
|
||||
svg.setAttribute('viewBox', `${minX} ${minY} ${maxX - minX} ${maxY - minY}`)
|
||||
|
||||
if (window.point) await probePoint()
|
||||
if (window.rect) await drawRect()
|
||||
|
||||
svg.onmousedown = async function(e) {
|
||||
let pt = new DOMPoint(e.clientX, e.clientY)
|
||||
pt = pt.matrixTransform(svg.getScreenCTM().inverse())
|
||||
pt.x = parseInt(pt.x)
|
||||
pt.y = parseInt(pt.y)
|
||||
if (e.buttons == 1) {
|
||||
await probePoint({X: pt.x, Y: pt.y})
|
||||
} else if (e.buttons == 2) {
|
||||
if (e.ctrlKey) {
|
||||
if (!window.rect) {
|
||||
window.rect = {X: pt.x, Y: pt.y, Width: 0, Height: 0}
|
||||
}
|
||||
if (!window.rectCursor) {
|
||||
window.rectAnchor = {x: window.rect.X, y: window.rect.Y}
|
||||
window.rectCursor = {x: window.rectAnchor.x + window.rect.Width, y: window.rectAnchor.y + window.rect.Height}
|
||||
}
|
||||
window.rectCursorOffset = {
|
||||
x: pt.x - window.rectCursor.x,
|
||||
y: pt.y - window.rectCursor.y,
|
||||
}
|
||||
} else {
|
||||
window.rectAnchor = pt
|
||||
window.rectCursorOffset = {x: 0, y: 0}
|
||||
window.probing = true
|
||||
drawRect({X: pt.x, Y: pt.y, Width: 0, Height: 0})
|
||||
window.probing = false
|
||||
}
|
||||
} else if (e.buttons == 4) {
|
||||
drawRect({X: pt.x, Y: pt.y, Width: 50, Height: 50})
|
||||
}
|
||||
}
|
||||
svg.onmousemove = async function(e) {
|
||||
if (window.probing) return
|
||||
window.probing = true
|
||||
if (e.buttons == 1) {
|
||||
await svg.onmousedown(e)
|
||||
} else if (e.buttons == 2) {
|
||||
let pt = new DOMPoint(e.clientX, e.clientY)
|
||||
pt = pt.matrixTransform(svg.getScreenCTM().inverse())
|
||||
if (e.ctrlKey) {
|
||||
window.rectAnchor.x += pt.x - rectCursor.x - window.rectCursorOffset.x
|
||||
window.rectAnchor.y += pt.y - rectCursor.y - window.rectCursorOffset.y
|
||||
}
|
||||
window.rectCursor = {
|
||||
x: pt.x - window.rectCursorOffset.x,
|
||||
y: pt.y - window.rectCursorOffset.y,
|
||||
}
|
||||
await drawRect({
|
||||
X: parseInt(Math.min(window.rectAnchor.x, window.rectCursor.x)),
|
||||
Y: parseInt(Math.min(window.rectAnchor.y, window.rectCursor.y)),
|
||||
Width: parseInt(Math.abs(window.rectCursor.x - window.rectAnchor.x)),
|
||||
Height: parseInt(Math.abs(window.rectCursor.y - window.rectAnchor.y)),
|
||||
})
|
||||
}
|
||||
window.probing = false
|
||||
}
|
||||
svg.oncontextmenu = function(e) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
async function probePoint(p = null) {
|
||||
const svg = document.getElementById('svg');
|
||||
const physical = !parseInt(document.querySelector('#coordinate-selector .active').dataset.value)
|
||||
|
||||
if (p == null) {
|
||||
if (window.pointIsPhysical == physical) {
|
||||
p = window.point
|
||||
} else {
|
||||
p = (await callBinding('main.ScreenService.TransformPoint', window.point, window.pointIsPhysical))[0]
|
||||
}
|
||||
}
|
||||
|
||||
window.point = p
|
||||
window.pointIsPhysical = physical
|
||||
const [ptTransformed, ptDblTransformed] = await callBinding('main.ScreenService.TransformPoint', p, physical)
|
||||
|
||||
svg.getElementById('points').innerHTML = `
|
||||
<circle cx="${p.X}" cy="${p.Y}" r="15" fill="red" />
|
||||
<circle cx="${ptTransformed.X}" cy="${ptTransformed.Y}" r="5" fill="green" />
|
||||
<circle cx="${ptTransformed.X}" cy="${ptTransformed.Y}" r="35" stroke="green" stroke-width="4" />
|
||||
<circle cx="${ptDblTransformed.X}" cy="${ptDblTransformed.Y}" r="25" stroke="red" stroke-width="4" />
|
||||
`
|
||||
// await new Promise((resolve) => setTimeout(resolve, 200)) // delay
|
||||
return ptDblTransformed
|
||||
}
|
||||
|
||||
async function drawRect(r = null) {
|
||||
const svg = document.getElementById('svg');
|
||||
const physical = !parseInt(document.querySelector('#coordinate-selector .active').dataset.value)
|
||||
|
||||
if (r == null) {
|
||||
if (window.rectIsPhysical == physical) {
|
||||
r = window.rect
|
||||
} else {
|
||||
r = await callBinding('main.ScreenService.TransformRect', window.rect, window.rectIsPhysical)
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.probing) {
|
||||
window.rectAnchor = null
|
||||
window.rectCursor = null
|
||||
}
|
||||
|
||||
document.getElementById('x').value = r.X
|
||||
document.getElementById('y').value = r.Y
|
||||
document.getElementById('w').value = r.Width
|
||||
document.getElementById('h').value = r.Height
|
||||
|
||||
window.rect = r
|
||||
window.rectIsPhysical = physical
|
||||
window.rTransformed = await callBinding('main.ScreenService.TransformRect', r, physical)
|
||||
window.rDblTransformed = await callBinding('main.ScreenService.TransformRect', rTransformed, !physical)
|
||||
window.rTransformed = rTransformed
|
||||
|
||||
await rectLayers()
|
||||
return rDblTransformed
|
||||
}
|
||||
|
||||
async function rectLayers() {
|
||||
const s = document.getElementById('slider').value
|
||||
if (window.rect == null) await test1()
|
||||
|
||||
const r = await callBinding('main.ScreenService.TransformRect', rectIsPhysical ? rect : rTransformed, true)
|
||||
const rShifted = {...r, X: r.X+50}
|
||||
const rShiftedPhysical = await callBinding('main.ScreenService.TransformRect', rShifted, false)
|
||||
|
||||
svg.getElementById('rects').innerHTML = [
|
||||
[window.rect, 'rgb(255 255 255 / 100%)'], // w
|
||||
[window.rTransformed, 'rgb(0 255 0 / 25%)'], // g
|
||||
[window.rDblTransformed, 'none'], // none
|
||||
[rShifted, 'rgb(255 0 0 / 15%)'], // r
|
||||
[rShiftedPhysical, 'rgb(0 0 255 / 15%)'], // b
|
||||
].filter((_,i) => i<s).map(([r, color], i) => {
|
||||
let lines = ''
|
||||
if (i == 0) {
|
||||
const center = {X: r.X + (r.Width-1)/2, Y: r.Y + (r.Height-1)/2}
|
||||
lines += `
|
||||
<line x1="${center.X}" x2="${center.X}" y1="${r.Y}" y2="${r.Y + r.Height-1}" stroke="gray" stroke-width="1" />
|
||||
<line x1="${r.X}" x2="${r.X + r.Width-1}" y1="${center.Y}" y2="${center.Y}" stroke="gray" stroke-width="1" />
|
||||
`
|
||||
}
|
||||
return `<rect x="${r.X}" y="${r.Y}" width="${r.Width}" height="${r.Height}" fill="${color}" stroke="${color == 'none' ? 'red' : 'black'}" stroke-width="${color == 'none' ? 5 : 1}" stroke-dasharray="${color == 'none' ? 5 : 'none'}" />${lines}`
|
||||
}).join('/n')
|
||||
}
|
||||
|
||||
async function updateDipRect(x, y=0, w=0, h=0) {
|
||||
if (rect == null) {
|
||||
await drawRect({
|
||||
X: +document.getElementById('x').value,
|
||||
Y: +document.getElementById('y').value,
|
||||
Width: +document.getElementById('w').value,
|
||||
Height: +document.getElementById('h').value,
|
||||
})
|
||||
}
|
||||
// Simulate real window by first retrieving the physical bounds then transforming it to dip
|
||||
// then updating the bounds and transforming it back to physical
|
||||
let rPhysical = rectIsPhysical ? rect : rTransformed
|
||||
const r = await callBinding('main.ScreenService.TransformRect', rPhysical, true)
|
||||
r.X += x
|
||||
r.Y += y
|
||||
r.Width += w
|
||||
r.Height += h
|
||||
rPhysical = await callBinding('main.ScreenService.TransformRect', r, false)
|
||||
drawRect(rectIsPhysical ? rPhysical : r)
|
||||
}
|
||||
|
||||
function arrowMove(e) {
|
||||
let x = 0, y = 0
|
||||
if (e.key == 'ArrowLeft') x = -step.value
|
||||
if (e.key == 'ArrowRight') x = +step.value
|
||||
if (e.key == 'ArrowUp') y = -step.value
|
||||
if (e.key == 'ArrowDown') y = +step.value
|
||||
if (!(x || y)) return
|
||||
e.preventDefault()
|
||||
updateDipRect(x, y)
|
||||
}
|
||||
|
||||
async function test1() {
|
||||
// Edge case 1: invalid dip rect: no physical rect can produce it
|
||||
await setLayout(parseLayout({screens: [
|
||||
{id: 1, w: 1200, h: 1200, s: 1},
|
||||
{id: 2, w: 1200, h: 1100, s: 1.5, parent: {id: 1, align: "r", offset: 0}},
|
||||
]}), false)
|
||||
await drawRect({X: 1050, Y: 700, Width: 400, Height: 300})
|
||||
}
|
||||
|
||||
async function test2() {
|
||||
// Edge case 2: physical rect that changes when double transformed (2 physical rects produce the same dip rect)
|
||||
await setLayout(parseLayout({screens: [
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5},
|
||||
{id: 2, w: 1200, h: 900, s: 1, parent: {id: 1, align: "r", offset: 0}},
|
||||
]}), true)
|
||||
await drawRect({X: 1050, Y: 890, Width: 400, Height: 300})
|
||||
}
|
||||
|
||||
async function probeLayout(finishup = true) {
|
||||
const probeButtons = document.getElementById('probe-buttons')
|
||||
const svg = document.getElementById('svg')
|
||||
const threshold = 1
|
||||
|
||||
const physical = !parseInt(document.querySelector('#coordinate-selector .active').dataset.value)
|
||||
window.cancelProbing = false
|
||||
probeButtons.classList.add('active')
|
||||
|
||||
const steps = 3
|
||||
let failed = false
|
||||
for (const screen of layout.screens) {
|
||||
if (window.cancelProbing) break
|
||||
const b = physical ? screen.PhysicalBounds : screen.Bounds
|
||||
const xStep = parseInt(b.Width / steps) || 1
|
||||
const yStep = parseInt(b.Height / steps) || 1
|
||||
let x = b.X, y = b.Y
|
||||
let xDone = false, yDone = false
|
||||
|
||||
while (!(yDone || window.cancelProbing)) {
|
||||
if (y >= b.Y + b.Height - 1) {
|
||||
y = b.Y + b.Height - 1
|
||||
yDone = true
|
||||
}
|
||||
x = b.X
|
||||
xDone = false
|
||||
while (!(xDone || window.cancelProbing)) {
|
||||
if (x >= b.X + b.Width - 1) {
|
||||
x = b.X + b.Width - 1
|
||||
xDone = true
|
||||
}
|
||||
const pt = {X: x, Y: y}
|
||||
let ptDblTransformed, err
|
||||
try {
|
||||
ptDblTransformed = await probePoint(pt)
|
||||
} catch (e) {
|
||||
err = e
|
||||
}
|
||||
if (err || Math.abs(pt.X - ptDblTransformed.X) > threshold || Math.abs(pt.Y - ptDblTransformed.Y) > threshold) {
|
||||
failed = true
|
||||
console.log(pt, ptDblTransformed)
|
||||
window.cancelProbing = true
|
||||
setTimeout(() => {
|
||||
alert(err ?? `**FAILED**\nProbing failed at point: {X: ${pt.X}, Y: ${pt.Y}}\nDouble transformed point: {X: ${ptDblTransformed.X}, Y: ${ptDblTransformed.Y}}\n(Exceeded threshold of ${threshold} pixels)`)
|
||||
}, 50)
|
||||
}
|
||||
x += xStep
|
||||
}
|
||||
y += yStep
|
||||
}
|
||||
}
|
||||
|
||||
if (finishup || window.cancelProbing) probeButtons.classList.remove('active')
|
||||
if (!(failed || window.cancelProbing)) {
|
||||
window.point = null
|
||||
if (finishup) {
|
||||
setTimeout(() => {
|
||||
svg.getElementById('points').innerHTML = ''
|
||||
alert(`Successfully probed all points!, All within threshold of ${threshold} pixels.`)
|
||||
}, 50)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
async function probeAllExamples() {
|
||||
console.time('probeAllExamples')
|
||||
loop1:
|
||||
for (let typeI = 0; typeI < examples.length; typeI++) {
|
||||
document.getElementById('examples-type').value = typeI
|
||||
setExamplesType(typeI, null)
|
||||
|
||||
for (let layoutI = (typeI ? 0 : -1); layoutI < examples[typeI].length; layoutI++) {
|
||||
await radioBtnClick(null, `#layout-selector [data-value="${layoutI + 1}"]`)
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const lastLayout = (typeI == examples.length - 1 && layoutI == examples[typeI].length - 1 && i == 1)
|
||||
if (!await probeLayout(lastLayout)) break loop1
|
||||
if (i == 0) await setCoordinateType(!pointIsPhysical)
|
||||
}
|
||||
}
|
||||
}
|
||||
console.timeEnd('probeAllExamples')
|
||||
}
|
||||
|
||||
async function callBinding(name, ...params) {
|
||||
return wails.Call.ByName(name, ...params)
|
||||
}
|
||||
|
||||
function showAdvanced(e) {
|
||||
e.target.style.display = 'none'
|
||||
document.querySelectorAll('.advanced').forEach(el => el.style.display = 'initial')
|
||||
}
|
||||
|
|
@ -2,8 +2,12 @@ package main
|
|||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
|
@ -25,8 +29,33 @@ func main() {
|
|||
WebviewUserDataPath: "",
|
||||
WebviewBrowserPath: "",
|
||||
},
|
||||
Services: []application.Service{
|
||||
application.NewService(&ScreenService{}),
|
||||
},
|
||||
LogLevel: slog.LevelError,
|
||||
Assets: application.AssetOptions{
|
||||
Handler: application.BundledAssetFileServer(assets),
|
||||
Middleware: func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Disable caching
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
dir := filepath.Dir(filename)
|
||||
url := r.URL.Path
|
||||
path := dir + "/assets" + url
|
||||
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
// Serve file from disk to make testing easy
|
||||
http.ServeFile(w, r, path)
|
||||
} else {
|
||||
// Passthrough to the default asset handler if file not found on disk
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
129
v3/examples/screen/screens.go
Normal file
129
v3/examples/screen/screens.go
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type ScreenService struct {
|
||||
screenManager application.ScreenManager
|
||||
isExampleLayout bool
|
||||
}
|
||||
|
||||
func (s *ScreenService) GetSystemScreens() []*application.Screen {
|
||||
s.isExampleLayout = false
|
||||
screens, _ := application.Get().GetScreens()
|
||||
return screens
|
||||
}
|
||||
|
||||
func (s *ScreenService) ProcessExampleScreens(rawScreens []interface{}) []*application.Screen {
|
||||
s.isExampleLayout = true
|
||||
|
||||
parseRect := func(m map[string]interface{}) application.Rect {
|
||||
return application.Rect{
|
||||
X: int(m["X"].(float64)),
|
||||
Y: int(m["Y"].(float64)),
|
||||
Width: int(m["Width"].(float64)),
|
||||
Height: int(m["Height"].(float64)),
|
||||
}
|
||||
}
|
||||
|
||||
screens := []*application.Screen{}
|
||||
for _, s := range rawScreens {
|
||||
s := s.(map[string]interface{})
|
||||
|
||||
bounds := parseRect(s["Bounds"].(map[string]interface{}))
|
||||
|
||||
screens = append(screens, &application.Screen{
|
||||
ID: s["ID"].(string),
|
||||
Name: s["Name"].(string),
|
||||
X: bounds.X,
|
||||
Y: bounds.Y,
|
||||
Size: application.Size{Width: bounds.Width, Height: bounds.Height},
|
||||
Bounds: bounds,
|
||||
PhysicalBounds: parseRect(s["PhysicalBounds"].(map[string]interface{})),
|
||||
WorkArea: parseRect(s["WorkArea"].(map[string]interface{})),
|
||||
PhysicalWorkArea: parseRect(s["PhysicalWorkArea"].(map[string]interface{})),
|
||||
IsPrimary: s["IsPrimary"].(bool),
|
||||
ScaleFactor: float32(s["ScaleFactor"].(float64)),
|
||||
Rotation: 0,
|
||||
})
|
||||
}
|
||||
|
||||
s.screenManager.LayoutScreens(screens)
|
||||
return s.screenManager.Screens()
|
||||
}
|
||||
|
||||
func (s *ScreenService) transformPoint(point application.Point, toDIP bool) application.Point {
|
||||
if s.isExampleLayout {
|
||||
if toDIP {
|
||||
return s.screenManager.PhysicalToDipPoint(point)
|
||||
} else {
|
||||
return s.screenManager.DipToPhysicalPoint(point)
|
||||
}
|
||||
} else {
|
||||
// =======================
|
||||
// TODO: remove this block when DPI is implemented in Linux & Mac
|
||||
if runtime.GOOS != "windows" {
|
||||
println("DPI not implemented yet!")
|
||||
return point
|
||||
}
|
||||
// =======================
|
||||
if toDIP {
|
||||
return application.PhysicalToDipPoint(point)
|
||||
} else {
|
||||
return application.DipToPhysicalPoint(point)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScreenService) TransformPoint(point map[string]interface{}, toDIP bool) (points [2]application.Point) {
|
||||
pt := application.Point{
|
||||
X: int(point["X"].(float64)),
|
||||
Y: int(point["Y"].(float64)),
|
||||
}
|
||||
|
||||
ptTransformed := s.transformPoint(pt, toDIP)
|
||||
ptDblTransformed := s.transformPoint(ptTransformed, !toDIP)
|
||||
|
||||
// double-transform multiple times to catch any double-rounding issues
|
||||
for i := 0; i < 10; i++ {
|
||||
ptTransformed = s.transformPoint(ptDblTransformed, toDIP)
|
||||
ptDblTransformed = s.transformPoint(ptTransformed, !toDIP)
|
||||
}
|
||||
|
||||
points[0] = ptTransformed
|
||||
points[1] = ptDblTransformed
|
||||
return points
|
||||
}
|
||||
|
||||
func (s *ScreenService) TransformRect(rect map[string]interface{}, toDIP bool) application.Rect {
|
||||
r := application.Rect{
|
||||
X: int(rect["X"].(float64)),
|
||||
Y: int(rect["Y"].(float64)),
|
||||
Width: int(rect["Width"].(float64)),
|
||||
Height: int(rect["Height"].(float64)),
|
||||
}
|
||||
|
||||
if s.isExampleLayout {
|
||||
if toDIP {
|
||||
return s.screenManager.PhysicalToDipRect(r)
|
||||
} else {
|
||||
return s.screenManager.DipToPhysicalRect(r)
|
||||
}
|
||||
} else {
|
||||
// =======================
|
||||
// TODO: remove this block when DPI is implemented in Linux & Mac
|
||||
if runtime.GOOS != "windows" {
|
||||
println("DPI not implemented yet!")
|
||||
return r
|
||||
}
|
||||
// =======================
|
||||
if toDIP {
|
||||
return application.PhysicalToDipRect(r)
|
||||
} else {
|
||||
return application.DipToPhysicalRect(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ func main() {
|
|||
|
||||
_ = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Width: 500,
|
||||
Height: 800,
|
||||
Height: 500,
|
||||
Name: "Systray Demo Window",
|
||||
Frameless: true,
|
||||
AlwaysOnTop: true,
|
||||
|
|
|
|||
90
v3/examples/window/assets/index.html
Normal file
90
v3/examples/window/assets/index.html
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Window Demo</title>
|
||||
<style>
|
||||
body {
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<script src="/wails/runtime.js" type="module"></script>
|
||||
<script>
|
||||
async function callBinding(name, ...params) {
|
||||
return wails.Call.ByName(name, ...params)
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div style="background-color: aquamarine; height: 50px;" onclick="showInfo = !showInfo">
|
||||
<div style="background-color: rgb(57, 109, 92); width: 50%; height: 100%; position: relative;">
|
||||
<span id="text" style="float: left; font-size: 1.5em;"></span>
|
||||
<div style="background-color: rgb(237, 157, 66); width: 40px; height: 100%; opacity: 0.5; position: absolute; right: -20px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ===================================================== -->
|
||||
<hr>
|
||||
<label>
|
||||
✥
|
||||
<input id="step" type="number" value="50" style="width: 50px" title="Step, Arrow Move" onkeydown="arrowMove(event)">
|
||||
</label>
|
||||
X:
|
||||
<button onclick="setPos(true, -step.value)"><</button>
|
||||
<button onclick="setPos(true, +step.value)">></button>
|
||||
Width:
|
||||
<button onclick="setSize(true, -step.value)">-</button>
|
||||
<button onclick="setSize(true, +step.value)">+</button>
|
||||
|
||||
<button onclick="getBounds()">getBounds</button>
|
||||
<button onclick="getBounds().then(b => {x.value = b.x; y.value = b.y; w.value = b.width; h.value = b.height})">↡</button>
|
||||
<div style="height: 10px"></div>
|
||||
<input id="x" type="number" value="100" style="width: 50px" title="X">
|
||||
<input id="y" type="number" value="100" style="width: 50px" title="Y">
|
||||
<button onclick="setPos(false, +x.value, +y.value)">setPos</button>
|
||||
|
||||
<input id="w" type="number" value="800" style="width: 50px" title="Width">
|
||||
<input id="h" type="number" value="600" style="width: 50px" title="Height">
|
||||
<button onclick="setSize(false, +w.value, +h.value)">setSize</button>
|
||||
|
||||
<button onclick="setBounds(+x.value, +y.value, +w.value, +h.value)">setBounds</button>
|
||||
<hr>
|
||||
<!-- ===================================================== -->
|
||||
|
||||
<script>
|
||||
function arrowMove(e) {
|
||||
let x = 0, y = 0
|
||||
if (e.key == 'ArrowLeft') x = -step.value
|
||||
if (e.key == 'ArrowRight') x = +step.value
|
||||
if (e.key == 'ArrowUp') y = -step.value
|
||||
if (e.key == 'ArrowDown') y = +step.value
|
||||
if (!(x || y)) return
|
||||
e.preventDefault()
|
||||
setPos(true, x, y)
|
||||
}
|
||||
function setPos(relative, x, y=0) {
|
||||
callBinding('main.WindowService.SetPos', relative, x, y)
|
||||
}
|
||||
function setSize(relative, w, h=0) {
|
||||
callBinding('main.WindowService.SetSize', relative, w, h)
|
||||
}
|
||||
function setBounds(x, y, w, h) {
|
||||
callBinding('main.WindowService.SetBounds', x, y, w, h)
|
||||
}
|
||||
async function getBounds() {
|
||||
const b = await callBinding('main.WindowService.GetBounds')
|
||||
return {x: b.X, y: b.Y, width: b.Width, height: b.Height}
|
||||
}
|
||||
let showInfo = false
|
||||
setInterval(async () => {
|
||||
if (!showInfo) {
|
||||
text.textContent = ''
|
||||
return
|
||||
}
|
||||
const b = await getBounds()
|
||||
text.textContent = `${b.x}, ${b.y} - ${b.width}, ${b.height}`
|
||||
}, 100);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
|
|
@ -20,14 +20,69 @@ var getExStyle = func() int {
|
|||
return 0
|
||||
}
|
||||
|
||||
//go:embed assets/*
|
||||
var assets embed.FS
|
||||
|
||||
type WindowService struct{}
|
||||
|
||||
// ==============================================
|
||||
func (s *WindowService) SetPos(relative bool, x, y float64) {
|
||||
win := application.Get().CurrentWindow()
|
||||
initX, initY := win.Position()
|
||||
if relative {
|
||||
x += float64(initX)
|
||||
y += float64(initY)
|
||||
}
|
||||
win.SetPosition(int(x), int(y))
|
||||
currentX, currentY := win.Position()
|
||||
fmt.Printf("SetPos: %d, %d => %d, %d\n", initX, initY, currentX, currentY)
|
||||
}
|
||||
func (s *WindowService) SetSize(relative bool, wdt, hgt float64) {
|
||||
win := application.Get().CurrentWindow()
|
||||
initW, initH := win.Size()
|
||||
if relative {
|
||||
wdt += float64(initW)
|
||||
hgt += float64(initH)
|
||||
}
|
||||
win.SetSize(int(wdt), int(hgt))
|
||||
currentW, currentH := win.Size()
|
||||
fmt.Printf("SetSize: %d, %d => %d, %d\n", initW, initH, currentW, currentH)
|
||||
}
|
||||
func (s *WindowService) SetBounds(x, y, w, h float64) {
|
||||
win := application.Get().CurrentWindow()
|
||||
initR := win.Bounds()
|
||||
win.SetBounds(application.Rect{
|
||||
X: int(x),
|
||||
Y: int(y),
|
||||
Width: int(w),
|
||||
Height: int(h),
|
||||
})
|
||||
currentR := win.Bounds()
|
||||
fmt.Printf("SetBounds: %+v => %+v\n", initR, currentR)
|
||||
}
|
||||
func (s *WindowService) GetBounds() application.Rect {
|
||||
win := application.Get().CurrentWindow()
|
||||
r := win.Bounds()
|
||||
mid := r.X + (r.Width-1)/2
|
||||
fmt.Printf("GetBounds: %+v: mid: %d\n", r, mid)
|
||||
return r
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
|
||||
func main() {
|
||||
app := application.New(application.Options{
|
||||
Name: "WebviewWindow Demo",
|
||||
Description: "A demo of the WebviewWindow API",
|
||||
Assets: application.AlphaAssets,
|
||||
Assets: application.AssetOptions{
|
||||
Handler: application.BundledAssetFileServer(assets),
|
||||
},
|
||||
Mac: application.MacOptions{
|
||||
ApplicationShouldTerminateAfterLastWindowClosed: false,
|
||||
},
|
||||
Services: []application.Service{
|
||||
application.NewService(&WindowService{}),
|
||||
},
|
||||
})
|
||||
app.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) {
|
||||
log.Println("ApplicationDidFinishLaunching")
|
||||
|
|
@ -403,6 +458,12 @@ func main() {
|
|||
w.SetRelativePosition(0, 0)
|
||||
})
|
||||
})
|
||||
positionMenu.Add("Set Relative Position (Corner)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
screen, _ := w.GetScreen()
|
||||
w.SetRelativePosition(screen.WorkArea.Width-w.Width(), screen.WorkArea.Height-w.Height())
|
||||
})
|
||||
})
|
||||
positionMenu.Add("Set Relative Position (Random)").OnClick(func(ctx *application.Context) {
|
||||
currentWindow(func(w *application.WebviewWindow) {
|
||||
w.SetRelativePosition(rand.Intn(1000), rand.Intn(800))
|
||||
|
|
@ -585,6 +646,7 @@ func main() {
|
|||
})
|
||||
|
||||
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Title: "Window Demo",
|
||||
BackgroundColour: application.NewRGB(33, 37, 41),
|
||||
Mac: application.MacWindow{
|
||||
DisableShadow: true,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@wailsio/runtime",
|
||||
"version": "3.0.0-alpha.26",
|
||||
"version": "3.0.0-alpha.27",
|
||||
"description": "Wails Runtime",
|
||||
"main": "src/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
|
|
|
|||
|
|
@ -10,23 +10,17 @@ The electron alternative for Go
|
|||
|
||||
/* jshint esversion: 9 */
|
||||
|
||||
/**
|
||||
* @typedef {Object} Position
|
||||
* @property {number} X - The X coordinate.
|
||||
* @property {number} Y - The Y coordinate.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Size
|
||||
* @property {number} X - The width.
|
||||
* @property {number} Y - The height.
|
||||
* @property {number} Width - The width.
|
||||
* @property {number} Height - The height.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {Object} Rect
|
||||
* @property {number} X - The X coordinate of the top-left corner.
|
||||
* @property {number} Y - The Y coordinate of the top-left corner.
|
||||
* @property {number} X - The X coordinate of the origin.
|
||||
* @property {number} Y - The Y coordinate of the origin.
|
||||
* @property {number} Width - The width of the rectangle.
|
||||
* @property {number} Height - The height of the rectangle.
|
||||
*/
|
||||
|
|
@ -36,11 +30,14 @@ The electron alternative for Go
|
|||
* @typedef {Object} Screen
|
||||
* @property {string} Id - Unique identifier for the screen.
|
||||
* @property {string} Name - Human readable name of the screen.
|
||||
* @property {number} Scale - The resolution scale of the screen. 1 = standard resolution, 2 = high (Retina), etc.
|
||||
* @property {Position} Position - Contains the X and Y coordinates of the screen's position.
|
||||
* @property {number} ScaleFactor - The scale factor of the screen (DPI/96). 1 = standard DPI, 2 = HiDPI (Retina), etc.
|
||||
* @property {number} X - The X coordinate of the screen.
|
||||
* @property {number} Y - The Y coordinate of the screen.
|
||||
* @property {Size} Size - Contains the width and height of the screen.
|
||||
* @property {Rect} Bounds - Contains the bounds of the screen in terms of X, Y, Width, and Height.
|
||||
* @property {Rect} PhysicalBounds - Contains the physical bounds of the screen in terms of X, Y, Width, and Height (before scaling).
|
||||
* @property {Rect} WorkArea - Contains the area of the screen that is actually usable (excluding taskbar and other system UI).
|
||||
* @property {Rect} PhysicalWorkArea - Contains the physical WorkArea of the screen (before scaling).
|
||||
* @property {boolean} IsPrimary - True if this is the primary monitor selected by the user in the operating system.
|
||||
* @property {number} Rotation - The rotation of the screen.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -14,33 +14,23 @@ export function GetPrimary(): Promise<Screen>;
|
|||
* @returns {Promise<Screen>} A promise that resolves with the current active screen.
|
||||
*/
|
||||
export function GetCurrent(): Promise<Screen>;
|
||||
export type Position = {
|
||||
/**
|
||||
* - The X coordinate.
|
||||
*/
|
||||
X: number;
|
||||
/**
|
||||
* - The Y coordinate.
|
||||
*/
|
||||
Y: number;
|
||||
};
|
||||
export type Size = {
|
||||
/**
|
||||
* - The width.
|
||||
*/
|
||||
X: number;
|
||||
Width: number;
|
||||
/**
|
||||
* - The height.
|
||||
*/
|
||||
Y: number;
|
||||
Height: number;
|
||||
};
|
||||
export type Rect = {
|
||||
/**
|
||||
* - The X coordinate of the top-left corner.
|
||||
* - The X coordinate of the origin.
|
||||
*/
|
||||
X: number;
|
||||
/**
|
||||
* - The Y coordinate of the top-left corner.
|
||||
* - The Y coordinate of the origin.
|
||||
*/
|
||||
Y: number;
|
||||
/**
|
||||
|
|
@ -62,13 +52,17 @@ export type Screen = {
|
|||
*/
|
||||
Name: string;
|
||||
/**
|
||||
* - The resolution scale of the screen. 1 = standard resolution, 2 = high (Retina), etc.
|
||||
* - The scale factor of the screen (DPI/96). 1 = standard DPI, 2 = HiDPI (Retina), etc.
|
||||
*/
|
||||
Scale: number;
|
||||
ScaleFactor: number;
|
||||
/**
|
||||
* - Contains the X and Y coordinates of the screen's position.
|
||||
* - The X coordinate of the screen.
|
||||
*/
|
||||
Position: Position;
|
||||
X: number;
|
||||
/**
|
||||
* - The Y coordinate of the screen.
|
||||
*/
|
||||
Y: number;
|
||||
/**
|
||||
* - Contains the width and height of the screen.
|
||||
*/
|
||||
|
|
@ -77,10 +71,18 @@ export type Screen = {
|
|||
* - Contains the bounds of the screen in terms of X, Y, Width, and Height.
|
||||
*/
|
||||
Bounds: Rect;
|
||||
/**
|
||||
* - Contains the physical bounds of the screen in terms of X, Y, Width, and Height (before scaling).
|
||||
*/
|
||||
PhysicalBounds: Rect;
|
||||
/**
|
||||
* - Contains the area of the screen that is actually usable (excluding taskbar and other system UI).
|
||||
*/
|
||||
WorkArea: Rect;
|
||||
/**
|
||||
* - Contains the physical WorkArea of the screen (before scaling).
|
||||
*/
|
||||
PhysicalWorkArea: Rect;
|
||||
/**
|
||||
* - True if this is the primary monitor selected by the user in the operating system.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -276,6 +276,9 @@ type App struct {
|
|||
applicationEventHooks map[uint][]*eventHook
|
||||
applicationEventHooksLock sync.RWMutex
|
||||
|
||||
// Screens layout manager (handles DIP coordinate system)
|
||||
screenManager ScreenManager
|
||||
|
||||
// Windows
|
||||
windows map[uint]Window
|
||||
windowsLock sync.RWMutex
|
||||
|
|
@ -814,12 +817,18 @@ func SaveFileDialog() *SaveFileDialogStruct {
|
|||
return newSaveFileDialog()
|
||||
}
|
||||
|
||||
func (a *App) GetPrimaryScreen() (*Screen, error) {
|
||||
return a.impl.getPrimaryScreen()
|
||||
}
|
||||
|
||||
// NOTE: should use screenManager directly after DPI is implemented in all platforms
|
||||
// (should also get rid of the error return)
|
||||
func (a *App) GetScreens() ([]*Screen, error) {
|
||||
return a.impl.getScreens()
|
||||
// return a.screenManager.screens, nil
|
||||
}
|
||||
|
||||
// NOTE: should use screenManager directly after DPI is implemented in all platforms
|
||||
// (should also get rid of the error return)
|
||||
func (a *App) GetPrimaryScreen() (*Screen, error) {
|
||||
return a.impl.getPrimaryScreen()
|
||||
// return a.screenManager.primaryScreen, nil
|
||||
}
|
||||
|
||||
func (a *App) Clipboard() *Clipboard {
|
||||
|
|
|
|||
|
|
@ -4,15 +4,12 @@ package application
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/go-webview2/webviewloader"
|
||||
"github.com/wailsapp/wails/v3/internal/operatingsystem"
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
|
|
@ -71,54 +68,6 @@ func getNativeApplication() *windowsApp {
|
|||
return globalApplication.impl.(*windowsApp)
|
||||
}
|
||||
|
||||
func (m *windowsApp) getPrimaryScreen() (*Screen, error) {
|
||||
screens, err := m.getScreens()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, screen := range screens {
|
||||
if screen.IsPrimary {
|
||||
return screen, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no primary screen found")
|
||||
}
|
||||
|
||||
func (m *windowsApp) getScreens() ([]*Screen, error) {
|
||||
allScreens, err := w32.GetAllScreens()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Convert result to []*Screen
|
||||
screens := make([]*Screen, len(allScreens))
|
||||
for id, screen := range allScreens {
|
||||
x := int(screen.MONITORINFOEX.RcMonitor.Left)
|
||||
y := int(screen.MONITORINFOEX.RcMonitor.Top)
|
||||
right := int(screen.MONITORINFOEX.RcMonitor.Right)
|
||||
bottom := int(screen.MONITORINFOEX.RcMonitor.Bottom)
|
||||
width := right - x
|
||||
height := bottom - y
|
||||
screens[id] = &Screen{
|
||||
ID: strconv.Itoa(id),
|
||||
Name: windows.UTF16ToString(screen.MONITORINFOEX.SzDevice[:]),
|
||||
X: x,
|
||||
Y: y,
|
||||
Size: Size{Width: width, Height: height},
|
||||
Bounds: Rect{X: x, Y: y, Width: width, Height: height},
|
||||
WorkArea: Rect{
|
||||
X: int(screen.MONITORINFOEX.RcWork.Left),
|
||||
Y: int(screen.MONITORINFOEX.RcWork.Top),
|
||||
Width: int(screen.MONITORINFOEX.RcWork.Right - screen.MONITORINFOEX.RcWork.Left),
|
||||
Height: int(screen.MONITORINFOEX.RcWork.Bottom - screen.MONITORINFOEX.RcWork.Top),
|
||||
},
|
||||
IsPrimary: screen.IsPrimary,
|
||||
Scale: screen.Scale,
|
||||
Rotation: 0,
|
||||
}
|
||||
}
|
||||
return screens, nil
|
||||
}
|
||||
|
||||
func (m *windowsApp) hide() {
|
||||
// Get the current focussed window
|
||||
m.focusedWindow = w32.GetForegroundWindow()
|
||||
|
|
@ -236,6 +185,16 @@ func (m *windowsApp) wndProc(hwnd w32.HWND, msg uint32, wParam, lParam uintptr)
|
|||
}
|
||||
}
|
||||
|
||||
// Reprocess and cache screens when display settings change
|
||||
if hwnd == m.mainThreadWindowHWND {
|
||||
if msg == w32.WM_DISPLAYCHANGE || (msg == w32.WM_SETTINGCHANGE && wParam == w32.SPI_SETWORKAREA) {
|
||||
err := m.processAndCacheScreens()
|
||||
if err != nil {
|
||||
m.parent.error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch msg {
|
||||
case w32.WM_SETTINGCHANGE:
|
||||
settingChanged := w32.UTF16PtrToString((*uint16)(unsafe.Pointer(lParam)))
|
||||
|
|
@ -313,11 +272,35 @@ func (m *windowsApp) unregisterWindow(w *windowsWebviewWindow) {
|
|||
}
|
||||
}
|
||||
|
||||
func setupDPIAwareness() error {
|
||||
// https://learn.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process
|
||||
// https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows
|
||||
|
||||
if w32.HasSetProcessDpiAwarenessContextFunc() {
|
||||
// This is most recent version with the best results
|
||||
// supported beginning with Windows 10, version 1703
|
||||
return w32.SetProcessDpiAwarenessContext(w32.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)
|
||||
}
|
||||
|
||||
if w32.HasSetProcessDpiAwarenessFunc() {
|
||||
// Supported beginning with Windows 8.1
|
||||
return w32.SetProcessDpiAwareness(w32.PROCESS_PER_MONITOR_DPI_AWARE)
|
||||
}
|
||||
|
||||
if w32.HasSetProcessDPIAwareFunc() {
|
||||
// If none of the above is supported, fallback to SetProcessDPIAware
|
||||
// which is supported beginning with Windows Vista
|
||||
return w32.SetProcessDPIAware()
|
||||
}
|
||||
|
||||
return fmt.Errorf("no DPI awareness method supported")
|
||||
}
|
||||
|
||||
func newPlatformApp(app *App) *windowsApp {
|
||||
err := w32.SetProcessDPIAware()
|
||||
|
||||
err := setupDPIAwareness()
|
||||
if err != nil {
|
||||
globalApplication.fatal("Fatal error in application initialisation: %s", err.Error())
|
||||
os.Exit(1)
|
||||
app.error(err.Error())
|
||||
}
|
||||
|
||||
result := &windowsApp{
|
||||
|
|
@ -327,6 +310,11 @@ func newPlatformApp(app *App) *windowsApp {
|
|||
systrayMap: make(map[w32.HWND]*windowsSystemTray),
|
||||
}
|
||||
|
||||
err = result.processAndCacheScreens()
|
||||
if err != nil {
|
||||
app.fatal(err.Error())
|
||||
}
|
||||
|
||||
result.init()
|
||||
result.initMainLoop()
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ typedef struct Screen {
|
|||
int w_height;
|
||||
int w_x;
|
||||
int w_y;
|
||||
float scale;
|
||||
float scaleFactor;
|
||||
double rotation;
|
||||
bool isPrimary;
|
||||
} Screen;
|
||||
|
|
@ -744,12 +744,12 @@ func getScreenByIndex(display *C.struct__GdkDisplay, index int) *Screen {
|
|||
}
|
||||
name := C.gdk_monitor_get_model(monitor)
|
||||
return &Screen{
|
||||
ID: fmt.Sprintf("%d", index),
|
||||
Name: C.GoString(name),
|
||||
IsPrimary: primary,
|
||||
Scale: float32(C.gdk_monitor_get_scale_factor(monitor)),
|
||||
X: int(geometry.x),
|
||||
Y: int(geometry.y),
|
||||
ID: fmt.Sprintf("%d", index),
|
||||
Name: C.GoString(name),
|
||||
IsPrimary: primary,
|
||||
ScaleFactor: float32(C.gdk_monitor_get_scale_factor(monitor)),
|
||||
X: int(geometry.x),
|
||||
Y: int(geometry.y),
|
||||
Size: Size{
|
||||
Height: int(geometry.height),
|
||||
Width: int(geometry.width),
|
||||
|
|
@ -760,6 +760,24 @@ func getScreenByIndex(display *C.struct__GdkDisplay, index int) *Screen {
|
|||
Height: int(geometry.height),
|
||||
Width: int(geometry.width),
|
||||
},
|
||||
PhysicalBounds: Rect{
|
||||
X: int(geometry.x),
|
||||
Y: int(geometry.y),
|
||||
Height: int(geometry.height),
|
||||
Width: int(geometry.width),
|
||||
},
|
||||
WorkArea: Rect{
|
||||
X: int(geometry.x),
|
||||
Y: int(geometry.y),
|
||||
Height: int(geometry.height),
|
||||
Width: int(geometry.width),
|
||||
},
|
||||
PhysicalWorkArea: Rect{
|
||||
X: int(geometry.x),
|
||||
Y: int(geometry.y),
|
||||
Height: int(geometry.height),
|
||||
Width: int(geometry.width),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -829,18 +847,18 @@ func getMousePosition() (int, int, *Screen) {
|
|||
monitor := C.gdk_display_get_monitor_at_point(defaultDisplay, x, y)
|
||||
geometry := C.GdkRectangle{}
|
||||
C.gdk_monitor_get_geometry(monitor, &geometry)
|
||||
scale := int(C.gdk_monitor_get_scale_factor(monitor))
|
||||
scaleFactor := int(C.gdk_monitor_get_scale_factor(monitor))
|
||||
return int(x), int(y), &Screen{
|
||||
ID: fmt.Sprintf("%d", 0), // A unique identifier for the display
|
||||
Name: C.GoString(C.gdk_monitor_get_model(monitor)), // The name of the display
|
||||
Scale: float32(scale), // The scale factor of the display
|
||||
X: int(geometry.x), // The x-coordinate of the top-left corner of the rectangle
|
||||
Y: int(geometry.y), // The y-coordinate of the top-left corner of the rectangle
|
||||
Size: Size{Width: int(geometry.width), Height: int(geometry.height)}, // The size of the display
|
||||
Bounds: Rect{}, // The bounds of the display
|
||||
WorkArea: Rect{}, // The work area of the display
|
||||
IsPrimary: false, // Whether this is the primary display
|
||||
Rotation: 0.0, // The rotation of the display
|
||||
ID: fmt.Sprintf("%d", 0), // A unique identifier for the display
|
||||
Name: C.GoString(C.gdk_monitor_get_model(monitor)), // The name of the display
|
||||
ScaleFactor: float32(scaleFactor), // The scale factor of the display
|
||||
X: int(geometry.x), // The x-coordinate of the top-left corner of the rectangle
|
||||
Y: int(geometry.y), // The y-coordinate of the top-left corner of the rectangle
|
||||
Size: Size{Width: int(geometry.width), Height: int(geometry.height)}, // The size of the display
|
||||
Bounds: Rect{}, // The bounds of the display
|
||||
WorkArea: Rect{}, // The work area of the display
|
||||
IsPrimary: false, // Whether this is the primary display
|
||||
Rotation: 0.0, // The rotation of the display
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -858,12 +876,12 @@ func (w *linuxWebviewWindow) destroy() {
|
|||
func (w *linuxWebviewWindow) fullscreen() {
|
||||
w.maximise()
|
||||
//w.lastWidth, w.lastHeight = w.size()
|
||||
x, y, width, height, scale := w.getCurrentMonitorGeometry()
|
||||
x, y, width, height, scaleFactor := w.getCurrentMonitorGeometry()
|
||||
if x == -1 && y == -1 && width == -1 && height == -1 {
|
||||
return
|
||||
}
|
||||
w.setMinMaxSize(0, 0, width*scale, height*scale)
|
||||
w.setSize(width*scale, height*scale)
|
||||
w.setMinMaxSize(0, 0, width*scaleFactor, height*scaleFactor)
|
||||
w.setSize(width*scaleFactor, height*scaleFactor)
|
||||
C.gtk_window_fullscreen(w.gtkWindow())
|
||||
w.setRelativePosition(0, 0)
|
||||
}
|
||||
|
|
@ -882,30 +900,30 @@ func (w *linuxWebviewWindow) getScreen() (*Screen, error) {
|
|||
// Get the current screen for the window
|
||||
monitor := w.getCurrentMonitor()
|
||||
name := C.gdk_monitor_get_model(monitor)
|
||||
mx, my, width, height, scale := w.getCurrentMonitorGeometry()
|
||||
mx, my, width, height, scaleFactor := w.getCurrentMonitorGeometry()
|
||||
return &Screen{
|
||||
ID: fmt.Sprintf("%d", w.id), // A unique identifier for the display
|
||||
Name: C.GoString(name), // The name of the display
|
||||
Scale: float32(scale), // The scale factor of the display
|
||||
X: mx, // The x-coordinate of the top-left corner of the rectangle
|
||||
Y: my, // The y-coordinate of the top-left corner of the rectangle
|
||||
Size: Size{Width: width, Height: height}, // The size of the display
|
||||
Bounds: Rect{}, // The bounds of the display
|
||||
WorkArea: Rect{}, // The work area of the display
|
||||
IsPrimary: false, // Whether this is the primary display
|
||||
Rotation: 0.0, // The rotation of the display
|
||||
ID: fmt.Sprintf("%d", w.id), // A unique identifier for the display
|
||||
Name: C.GoString(name), // The name of the display
|
||||
ScaleFactor: float32(scaleFactor), // The scale factor of the display
|
||||
X: mx, // The x-coordinate of the top-left corner of the rectangle
|
||||
Y: my, // The y-coordinate of the top-left corner of the rectangle
|
||||
Size: Size{Width: width, Height: height}, // The size of the display
|
||||
Bounds: Rect{}, // The bounds of the display
|
||||
WorkArea: Rect{}, // The work area of the display
|
||||
IsPrimary: false, // Whether this is the primary display
|
||||
Rotation: 0.0, // The rotation of the display
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) getCurrentMonitorGeometry() (x int, y int, width int, height int, scale int) {
|
||||
func (w *linuxWebviewWindow) getCurrentMonitorGeometry() (x int, y int, width int, height int, scaleFactor int) {
|
||||
monitor := w.getCurrentMonitor()
|
||||
if monitor == nil {
|
||||
return -1, -1, -1, -1, 1
|
||||
}
|
||||
var result C.GdkRectangle
|
||||
C.gdk_monitor_get_geometry(monitor, &result)
|
||||
scale = int(C.gdk_monitor_get_scale_factor(monitor))
|
||||
return int(result.x), int(result.y), int(result.width), int(result.height), scale
|
||||
scaleFactor = int(C.gdk_monitor_get_scale_factor(monitor))
|
||||
return int(result.x), int(result.y), int(result.width), int(result.height), scaleFactor
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) size() (int, int) {
|
||||
|
|
@ -1101,7 +1119,7 @@ func getPrimaryScreen() (*Screen, error) {
|
|||
monitor := C.gdk_display_get_primary_monitor(display)
|
||||
geometry := C.GdkRectangle{}
|
||||
C.gdk_monitor_get_geometry(monitor, &geometry)
|
||||
scale := int(C.gdk_monitor_get_scale_factor(monitor))
|
||||
scaleFactor := int(C.gdk_monitor_get_scale_factor(monitor))
|
||||
// get the name for the screen
|
||||
name := C.gdk_monitor_get_model(monitor)
|
||||
return &Screen{
|
||||
|
|
@ -1120,7 +1138,7 @@ func getPrimaryScreen() (*Screen, error) {
|
|||
Height: int(geometry.height),
|
||||
Width: int(geometry.width),
|
||||
},
|
||||
Scale: float32(scale),
|
||||
ScaleFactor: float32(scaleFactor),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -621,10 +621,10 @@ func getScreenByIndex(display pointer, index int) *Screen {
|
|||
}
|
||||
|
||||
return &Screen{
|
||||
IsPrimary: primary,
|
||||
Scale: 1.0,
|
||||
X: int(geometry.x),
|
||||
Y: int(geometry.y),
|
||||
IsPrimary: primary,
|
||||
ScaleFactor: 1.0,
|
||||
X: int(geometry.x),
|
||||
Y: int(geometry.y),
|
||||
Size: Size{
|
||||
Height: int(geometry.height),
|
||||
Width: int(geometry.width),
|
||||
|
|
@ -719,7 +719,7 @@ func windowGetCurrentMonitor(window pointer) pointer {
|
|||
return gdkDisplayGetMonitorAtWindow(display, window)
|
||||
}
|
||||
|
||||
func windowGetCurrentMonitorGeometry(window pointer) (x int, y int, width int, height int, scale int) {
|
||||
func windowGetCurrentMonitorGeometry(window pointer) (x int, y int, width int, height int, scaleFactor int) {
|
||||
monitor := windowGetCurrentMonitor(window)
|
||||
if monitor == 0 {
|
||||
return -1, -1, -1, -1, 1
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
package application
|
||||
|
||||
type Screen struct {
|
||||
ID string // A unique identifier for the display
|
||||
Name string // The name of the display
|
||||
Scale float32 // The scale factor of the display
|
||||
X int // The x-coordinate of the top-left corner of the rectangle
|
||||
Y int // The y-coordinate of the top-left corner of the rectangle
|
||||
Size Size // The size of the display
|
||||
Bounds Rect // The bounds of the display
|
||||
WorkArea Rect // The work area of the display
|
||||
IsPrimary bool // Whether this is the primary display
|
||||
Rotation float32 // The rotation of the display
|
||||
}
|
||||
|
||||
type Rect struct {
|
||||
X int
|
||||
Y int
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
type Size struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ typedef struct Screen {
|
|||
int w_height;
|
||||
int w_x;
|
||||
int w_y;
|
||||
float scale;
|
||||
float scaleFactor;
|
||||
double rotation;
|
||||
bool isPrimary;
|
||||
} Screen;
|
||||
|
|
@ -36,7 +36,7 @@ int GetNumScreens(){
|
|||
|
||||
Screen processScreen(NSScreen* screen){
|
||||
Screen returnScreen;
|
||||
returnScreen.scale = screen.backingScaleFactor;
|
||||
returnScreen.scaleFactor = screen.backingScaleFactor;
|
||||
|
||||
// screen bounds
|
||||
returnScreen.height = screen.frame.size.height;
|
||||
|
|
@ -137,17 +137,29 @@ func cScreenToScreen(screen C.Screen) *Screen {
|
|||
Height: int(screen.height),
|
||||
Width: int(screen.width),
|
||||
},
|
||||
PhysicalBounds: Rect{
|
||||
X: int(screen.x),
|
||||
Y: int(screen.y),
|
||||
Height: int(screen.height),
|
||||
Width: int(screen.width),
|
||||
},
|
||||
WorkArea: Rect{
|
||||
X: int(screen.w_x),
|
||||
Y: int(screen.w_y),
|
||||
Height: int(screen.w_height),
|
||||
Width: int(screen.w_width),
|
||||
},
|
||||
Scale: float32(screen.scale),
|
||||
ID: C.GoString(screen.id),
|
||||
Name: C.GoString(screen.name),
|
||||
IsPrimary: bool(screen.isPrimary),
|
||||
Rotation: float32(screen.rotation),
|
||||
PhysicalWorkArea: Rect{
|
||||
X: int(screen.w_x),
|
||||
Y: int(screen.w_y),
|
||||
Height: int(screen.w_height),
|
||||
Width: int(screen.w_width),
|
||||
},
|
||||
ScaleFactor: float32(screen.scaleFactor),
|
||||
ID: C.GoString(screen.id),
|
||||
Name: C.GoString(screen.name),
|
||||
IsPrimary: bool(screen.isPrimary),
|
||||
Rotation: float32(screen.rotation),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
88
v3/pkg/application/screen_windows.go
Normal file
88
v3/pkg/application/screen_windows.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
//go:build windows
|
||||
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func (m *windowsApp) processAndCacheScreens() error {
|
||||
allScreens, err := w32.GetAllScreens()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert result to []*Screen
|
||||
var screens []*Screen
|
||||
|
||||
for _, screen := range allScreens {
|
||||
x := int(screen.MONITORINFOEX.RcMonitor.Left)
|
||||
y := int(screen.MONITORINFOEX.RcMonitor.Top)
|
||||
right := int(screen.MONITORINFOEX.RcMonitor.Right)
|
||||
bottom := int(screen.MONITORINFOEX.RcMonitor.Bottom)
|
||||
width := right - x
|
||||
height := bottom - y
|
||||
|
||||
workArea := Rect{
|
||||
X: int(screen.MONITORINFOEX.RcWork.Left),
|
||||
Y: int(screen.MONITORINFOEX.RcWork.Top),
|
||||
Width: int(screen.MONITORINFOEX.RcWork.Right - screen.MONITORINFOEX.RcWork.Left),
|
||||
Height: int(screen.MONITORINFOEX.RcWork.Bottom - screen.MONITORINFOEX.RcWork.Top),
|
||||
}
|
||||
|
||||
screens = append(screens, &Screen{
|
||||
ID: hMonitorToScreenID(screen.HMonitor),
|
||||
Name: windows.UTF16ToString(screen.MONITORINFOEX.SzDevice[:]),
|
||||
X: x,
|
||||
Y: y,
|
||||
Size: Size{Width: width, Height: height},
|
||||
Bounds: Rect{X: x, Y: y, Width: width, Height: height},
|
||||
PhysicalBounds: Rect{X: x, Y: y, Width: width, Height: height},
|
||||
WorkArea: workArea,
|
||||
PhysicalWorkArea: workArea,
|
||||
IsPrimary: screen.IsPrimary,
|
||||
ScaleFactor: screen.ScaleFactor,
|
||||
Rotation: 0,
|
||||
})
|
||||
}
|
||||
|
||||
err = m.parent.screenManager.LayoutScreens(screens)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NOTE: should be moved to *App after DPI is implemented in all platforms
|
||||
func (m *windowsApp) getScreens() ([]*Screen, error) {
|
||||
return m.parent.screenManager.screens, nil
|
||||
}
|
||||
|
||||
// NOTE: should be moved to *App after DPI is implemented in all platforms
|
||||
func (m *windowsApp) getPrimaryScreen() (*Screen, error) {
|
||||
return m.parent.screenManager.primaryScreen, nil
|
||||
}
|
||||
|
||||
func getScreenForWindow(window *windowsWebviewWindow) (*Screen, error) {
|
||||
return ScreenNearestPhysicalRect(window.physicalBounds()), nil
|
||||
}
|
||||
|
||||
func getScreenForWindowHwnd(hwnd w32.HWND) (*Screen, error) {
|
||||
hMonitor := w32.MonitorFromWindow(hwnd, w32.MONITOR_DEFAULTTONEAREST)
|
||||
screenID := hMonitorToScreenID(hMonitor)
|
||||
for _, screen := range globalApplication.screenManager.screens {
|
||||
if screen.ID == screenID {
|
||||
return screen, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("screen not found for window")
|
||||
}
|
||||
|
||||
func hMonitorToScreenID(hMonitor uintptr) string {
|
||||
return strconv.Itoa(int(hMonitor))
|
||||
}
|
||||
868
v3/pkg/application/screenmanager.go
Normal file
868
v3/pkg/application/screenmanager.go
Normal file
|
|
@ -0,0 +1,868 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Heavily inspired by the Chromium project (Copyright 2015 The Chromium Authors)
|
||||
// Chromium License: https://chromium.googlesource.com/chromium/src/+/HEAD/LICENSE
|
||||
|
||||
type ScreenManager struct {
|
||||
screens []*Screen
|
||||
primaryScreen *Screen
|
||||
}
|
||||
|
||||
type Screen struct {
|
||||
ID string // A unique identifier for the display
|
||||
Name string // The name of the display
|
||||
ScaleFactor float32 // The scale factor of the display (DPI/96)
|
||||
X int // The x-coordinate of the top-left corner of the rectangle
|
||||
Y int // The y-coordinate of the top-left corner of the rectangle
|
||||
Size Size // The size of the display
|
||||
Bounds Rect // The bounds of the display
|
||||
PhysicalBounds Rect // The physical bounds of the display (before scaling)
|
||||
WorkArea Rect // The work area of the display
|
||||
PhysicalWorkArea Rect // The physical work area of the display (before scaling)
|
||||
IsPrimary bool // Whether this is the primary display
|
||||
Rotation float32 // The rotation of the display
|
||||
}
|
||||
|
||||
type Rect struct {
|
||||
X int
|
||||
Y int
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
type Point struct {
|
||||
X int
|
||||
Y int
|
||||
}
|
||||
type Size struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
type Alignment int
|
||||
type OffsetReference int
|
||||
|
||||
const (
|
||||
TOP Alignment = iota
|
||||
RIGHT
|
||||
BOTTOM
|
||||
LEFT
|
||||
)
|
||||
|
||||
const (
|
||||
BEGIN OffsetReference = iota // TOP or LEFT
|
||||
END // BOTTOM or RIGHT
|
||||
)
|
||||
|
||||
// ScreenPlacement specifies where the screen (S) is placed relative to
|
||||
// parent (P) screen. In the following example, (S) is RIGHT aligned to (P)
|
||||
// with a positive offset and a BEGIN (top) offset reference.
|
||||
//
|
||||
// . +------------+ +
|
||||
// . | | | offset
|
||||
// . | P | v
|
||||
// . | +--------+
|
||||
// . | | |
|
||||
// . +------------+ S |
|
||||
// . | |
|
||||
// . +--------+
|
||||
type ScreenPlacement struct {
|
||||
screen *Screen
|
||||
parent *Screen
|
||||
alignment Alignment
|
||||
offset int
|
||||
offsetReference OffsetReference
|
||||
}
|
||||
|
||||
func (r Rect) Origin() Point {
|
||||
return Point{
|
||||
X: r.X,
|
||||
Y: r.Y,
|
||||
}
|
||||
}
|
||||
|
||||
func (s Screen) Origin() Point {
|
||||
return Point{
|
||||
X: s.X,
|
||||
Y: s.Y,
|
||||
}
|
||||
}
|
||||
|
||||
func (r Rect) Corner() Point {
|
||||
return Point{
|
||||
X: r.right(),
|
||||
Y: r.bottom(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r Rect) InsideCorner() Point {
|
||||
return Point{
|
||||
X: r.right() - 1,
|
||||
Y: r.bottom() - 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (r Rect) right() int {
|
||||
return r.X + r.Width
|
||||
}
|
||||
|
||||
func (r Rect) bottom() int {
|
||||
return r.Y + r.Height
|
||||
}
|
||||
|
||||
func (s Screen) right() int {
|
||||
return s.Bounds.right()
|
||||
}
|
||||
|
||||
func (s Screen) bottom() int {
|
||||
return s.Bounds.bottom()
|
||||
}
|
||||
|
||||
func (s Screen) scale(value int, toDip bool) int {
|
||||
// Round up when scaling down and round down when scaling up.
|
||||
// This mix rounding strategy prevents drift over time when applying multiple scaling back and forth.
|
||||
// In addition, It has been shown that using this approach minimized rounding issues and improved overall
|
||||
// precision when converting between DIP and physical coordinates.
|
||||
if toDip {
|
||||
return int(math.Ceil(float64(value) / float64(s.ScaleFactor)))
|
||||
} else {
|
||||
return int(math.Floor(float64(value) * float64(s.ScaleFactor)))
|
||||
}
|
||||
}
|
||||
|
||||
func (r Rect) Size() Size {
|
||||
return Size{
|
||||
Width: r.Width,
|
||||
Height: r.Height,
|
||||
}
|
||||
}
|
||||
|
||||
func (r Rect) IsEmpty() bool {
|
||||
return r.Width <= 0 || r.Height <= 0
|
||||
}
|
||||
|
||||
func (r Rect) Contains(pt Point) bool {
|
||||
return pt.X >= r.X && pt.X < r.X+r.Width && pt.Y >= r.Y && pt.Y < r.Y+r.Height
|
||||
}
|
||||
|
||||
// Get intersection with another rect
|
||||
func (r Rect) Intersect(otherRect Rect) Rect {
|
||||
if r.IsEmpty() || otherRect.IsEmpty() {
|
||||
return Rect{}
|
||||
}
|
||||
|
||||
maxLeft := max(r.X, otherRect.X)
|
||||
maxTop := max(r.Y, otherRect.Y)
|
||||
minRight := min(r.right(), otherRect.right())
|
||||
minBottom := min(r.bottom(), otherRect.bottom())
|
||||
|
||||
if minRight > maxLeft && minBottom > maxTop {
|
||||
return Rect{
|
||||
X: maxLeft,
|
||||
Y: maxTop,
|
||||
Width: minRight - maxLeft,
|
||||
Height: minBottom - maxTop,
|
||||
}
|
||||
}
|
||||
return Rect{}
|
||||
}
|
||||
|
||||
// Check if screens intersects another screen
|
||||
func (s *Screen) intersects(otherScreen *Screen) bool {
|
||||
maxLeft := max(s.X, otherScreen.X)
|
||||
maxTop := max(s.Y, otherScreen.Y)
|
||||
minRight := min(s.right(), otherScreen.right())
|
||||
minBottom := min(s.bottom(), otherScreen.bottom())
|
||||
|
||||
return minRight > maxLeft && minBottom > maxTop
|
||||
}
|
||||
|
||||
// Get distance from another rect (squared)
|
||||
func (r Rect) distanceFromRectSquared(otherRect Rect) int {
|
||||
// If they intersect, return negative area of intersection
|
||||
intersection := r.Intersect(otherRect)
|
||||
if !intersection.IsEmpty() {
|
||||
return -(intersection.Width * intersection.Height)
|
||||
}
|
||||
|
||||
dX := max(0, max(r.X-otherRect.right(), otherRect.X-r.right()))
|
||||
dY := max(0, max(r.Y-otherRect.bottom(), otherRect.Y-r.bottom()))
|
||||
|
||||
// Distance squared
|
||||
return dX*dX + dY*dY
|
||||
}
|
||||
|
||||
// Apply screen placement
|
||||
func (p ScreenPlacement) apply() {
|
||||
parentBounds := p.parent.Bounds
|
||||
screenBounds := p.screen.Bounds
|
||||
|
||||
newX := parentBounds.X
|
||||
newY := parentBounds.Y
|
||||
offset := p.offset
|
||||
|
||||
if p.alignment == TOP || p.alignment == BOTTOM {
|
||||
if p.offsetReference == END {
|
||||
offset = parentBounds.Width - offset - screenBounds.Width
|
||||
}
|
||||
offset = min(offset, parentBounds.Width)
|
||||
offset = max(offset, -screenBounds.Width)
|
||||
newX += offset
|
||||
if p.alignment == TOP {
|
||||
newY -= screenBounds.Height
|
||||
} else {
|
||||
newY += parentBounds.Height
|
||||
}
|
||||
} else {
|
||||
if p.offsetReference == END {
|
||||
offset = parentBounds.Height - offset - screenBounds.Height
|
||||
}
|
||||
offset = min(offset, parentBounds.Height)
|
||||
offset = max(offset, -screenBounds.Height)
|
||||
newY += offset
|
||||
if p.alignment == LEFT {
|
||||
newX -= screenBounds.Width
|
||||
} else {
|
||||
newX += parentBounds.Width
|
||||
}
|
||||
}
|
||||
|
||||
p.screen.move(newX, newY)
|
||||
}
|
||||
|
||||
func (s *Screen) absoluteToRelativeDipPoint(dipPoint Point) Point {
|
||||
return Point{
|
||||
X: dipPoint.X - s.Bounds.X,
|
||||
Y: dipPoint.Y - s.Bounds.Y,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Screen) relativeToAbsoluteDipPoint(dipPoint Point) Point {
|
||||
return Point{
|
||||
X: dipPoint.X + s.Bounds.X,
|
||||
Y: dipPoint.Y + s.Bounds.Y,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Screen) absoluteToRelativePhysicalPoint(physicalPoint Point) Point {
|
||||
return Point{
|
||||
X: physicalPoint.X - s.PhysicalBounds.X,
|
||||
Y: physicalPoint.Y - s.PhysicalBounds.Y,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Screen) relativeToAbsolutePhysicalPoint(physicalPoint Point) Point {
|
||||
return Point{
|
||||
X: physicalPoint.X + s.PhysicalBounds.X,
|
||||
Y: physicalPoint.Y + s.PhysicalBounds.Y,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Screen) move(newX, newY int) {
|
||||
workAreaOffsetX := s.WorkArea.X - s.X
|
||||
workAreaOffsetY := s.WorkArea.Y - s.Y
|
||||
|
||||
s.X = newX
|
||||
s.Y = newY
|
||||
s.Bounds.X = newX
|
||||
s.Bounds.Y = newY
|
||||
s.WorkArea.X = newX + workAreaOffsetX
|
||||
s.WorkArea.Y = newY + workAreaOffsetY
|
||||
}
|
||||
|
||||
func (s *Screen) applyDPIScaling() {
|
||||
if s.ScaleFactor == 1 {
|
||||
return
|
||||
}
|
||||
workAreaOffsetX := s.WorkArea.X - s.Bounds.X
|
||||
workAreaOffsetY := s.WorkArea.Y - s.Bounds.Y
|
||||
|
||||
s.WorkArea.X = s.Bounds.X + s.scale(workAreaOffsetX, true)
|
||||
s.WorkArea.Y = s.Bounds.Y + s.scale(workAreaOffsetY, true)
|
||||
|
||||
s.Bounds.Width = s.scale(s.PhysicalBounds.Width, true)
|
||||
s.Bounds.Height = s.scale(s.PhysicalBounds.Height, true)
|
||||
s.WorkArea.Width = s.scale(s.PhysicalWorkArea.Width, true)
|
||||
s.WorkArea.Height = s.scale(s.PhysicalWorkArea.Height, true)
|
||||
|
||||
s.Size.Width = s.Bounds.Width
|
||||
s.Size.Height = s.Bounds.Height
|
||||
}
|
||||
|
||||
func (s *Screen) dipToPhysicalPoint(dipPoint Point, isCorner bool) Point {
|
||||
relativePoint := s.absoluteToRelativeDipPoint(dipPoint)
|
||||
scaledRelativePoint := Point{
|
||||
X: s.scale(relativePoint.X, false),
|
||||
Y: s.scale(relativePoint.Y, false),
|
||||
}
|
||||
// Align edge points (fixes rounding issues)
|
||||
edgeOffset := 1
|
||||
if isCorner {
|
||||
edgeOffset = 0
|
||||
}
|
||||
if relativePoint.X == s.Bounds.Width-edgeOffset {
|
||||
scaledRelativePoint.X = s.PhysicalBounds.Width - edgeOffset
|
||||
}
|
||||
if relativePoint.Y == s.Bounds.Height-edgeOffset {
|
||||
scaledRelativePoint.Y = s.PhysicalBounds.Height - edgeOffset
|
||||
}
|
||||
return s.relativeToAbsolutePhysicalPoint(scaledRelativePoint)
|
||||
}
|
||||
|
||||
func (s *Screen) physicalToDipPoint(physicalPoint Point, isCorner bool) Point {
|
||||
relativePoint := s.absoluteToRelativePhysicalPoint(physicalPoint)
|
||||
scaledRelativePoint := Point{
|
||||
X: s.scale(relativePoint.X, true),
|
||||
Y: s.scale(relativePoint.Y, true),
|
||||
}
|
||||
// Align edge points (fixes rounding issues)
|
||||
edgeOffset := 1
|
||||
if isCorner {
|
||||
edgeOffset = 0
|
||||
}
|
||||
if relativePoint.X == s.PhysicalBounds.Width-edgeOffset {
|
||||
scaledRelativePoint.X = s.Bounds.Width - edgeOffset
|
||||
}
|
||||
if relativePoint.Y == s.PhysicalBounds.Height-edgeOffset {
|
||||
scaledRelativePoint.Y = s.Bounds.Height - edgeOffset
|
||||
}
|
||||
return s.relativeToAbsoluteDipPoint(scaledRelativePoint)
|
||||
}
|
||||
|
||||
func (s *Screen) dipToPhysicalRect(dipRect Rect) Rect {
|
||||
origin := s.dipToPhysicalPoint(dipRect.Origin(), false)
|
||||
corner := s.dipToPhysicalPoint(dipRect.Corner(), true)
|
||||
|
||||
return Rect{
|
||||
X: origin.X,
|
||||
Y: origin.Y,
|
||||
Width: corner.X - origin.X,
|
||||
Height: corner.Y - origin.Y,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Screen) physicalToDipRect(physicalRect Rect) Rect {
|
||||
origin := s.physicalToDipPoint(physicalRect.Origin(), false)
|
||||
corner := s.physicalToDipPoint(physicalRect.Corner(), true)
|
||||
|
||||
return Rect{
|
||||
X: origin.X,
|
||||
Y: origin.Y,
|
||||
Width: corner.X - origin.X,
|
||||
Height: corner.Y - origin.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// Layout screens in the virtual space with DIP calculations and cache the screens
|
||||
// for future coordinate transformation between the physical and logical (DIP) space
|
||||
func (m *ScreenManager) LayoutScreens(screens []*Screen) error {
|
||||
if screens == nil || len(screens) == 0 {
|
||||
return fmt.Errorf("screens parameter is nil or empty")
|
||||
}
|
||||
m.screens = screens
|
||||
|
||||
err := m.calculateScreensDipCoordinates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ScreenManager) Screens() []*Screen {
|
||||
return m.screens
|
||||
}
|
||||
|
||||
func (m *ScreenManager) PrimaryScreen() *Screen {
|
||||
return m.primaryScreen
|
||||
}
|
||||
|
||||
// Reference: https://source.chromium.org/chromium/chromium/src/+/main:ui/display/win/screen_win.cc;l=317
|
||||
func (m *ScreenManager) calculateScreensDipCoordinates() error {
|
||||
remainingScreens := []*Screen{}
|
||||
|
||||
// Find the primary screen
|
||||
m.primaryScreen = nil
|
||||
for _, screen := range m.screens {
|
||||
if screen.IsPrimary {
|
||||
m.primaryScreen = screen
|
||||
} else {
|
||||
remainingScreens = append(remainingScreens, screen)
|
||||
}
|
||||
}
|
||||
if m.primaryScreen == nil {
|
||||
return fmt.Errorf("no primary screen found")
|
||||
} else if len(remainingScreens) != len(m.screens)-1 {
|
||||
return fmt.Errorf("invalid primary screen found")
|
||||
}
|
||||
|
||||
// Build screens tree using the primary screen as root
|
||||
screensPlacements := []ScreenPlacement{}
|
||||
availableParents := []*Screen{m.primaryScreen}
|
||||
for len(availableParents) > 0 {
|
||||
// Pop a parent
|
||||
end := len(availableParents) - 1
|
||||
parent := availableParents[end]
|
||||
availableParents = availableParents[:end]
|
||||
// Find touching screens
|
||||
for _, child := range m.findAndRemoveTouchingScreens(parent, &remainingScreens) {
|
||||
screenPlacement := m.calculateScreenPlacement(child, parent)
|
||||
screensPlacements = append(screensPlacements, screenPlacement)
|
||||
availableParents = append(availableParents, child)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply screens DPI scaling and placement starting with
|
||||
// the primary screen and then dependent screens
|
||||
m.primaryScreen.applyDPIScaling()
|
||||
for _, placement := range screensPlacements {
|
||||
placement.screen.applyDPIScaling()
|
||||
placement.apply()
|
||||
}
|
||||
|
||||
// Now that all the placements have been applied,
|
||||
// we must detect and fix any overlapping screens.
|
||||
m.deIntersectScreens(screensPlacements)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns a ScreenPlacement for |screen| relative to |parent|.
|
||||
// Note that ScreenPlacement's are always in DIPs, so this also performs the
|
||||
// required scaling.
|
||||
// References:
|
||||
// - https://github.com/chromium/chromium/blob/main/ui/display/win/scaling_util.h#L25
|
||||
// - https://github.com/chromium/chromium/blob/main/ui/display/win/scaling_util.cc#L142
|
||||
func (m *ScreenManager) calculateScreenPlacement(screen, parent *Screen) ScreenPlacement {
|
||||
// Examples (The offset is indicated by the arrow.):
|
||||
// Scaled and Unscaled Coordinates
|
||||
// +--------------+ + Since both screens are of the same scale
|
||||
// | | | factor, relative positions remain the same.
|
||||
// | Parent | V
|
||||
// | 1x +----------+
|
||||
// | | |
|
||||
// +--------------+ Screen |
|
||||
// | 1x |
|
||||
// +----------+
|
||||
//
|
||||
// Unscaled Coordinates
|
||||
// +--------------+ The 2x screen is offset to maintain a
|
||||
// | | similar neighboring relationship with the 1x
|
||||
// | Parent | parent. Screen's position is based off of the
|
||||
// | 1x +----------+ percentage position along its parent. This
|
||||
// | | | percentage position is preserved in the scaled
|
||||
// +--------------+ Screen | coordinates.
|
||||
// | 2x |
|
||||
// +----------+
|
||||
// Scaled Coordinates
|
||||
// +--------------+ +
|
||||
// | | |
|
||||
// | Parent | V
|
||||
// | 1x +-----+
|
||||
// | | S 2x|
|
||||
// +--------------+-----+
|
||||
//
|
||||
//
|
||||
// Unscaled Coordinates
|
||||
// +--------------+ The parent screen has a 2x scale factor.
|
||||
// | | The offset is adjusted to maintain the
|
||||
// | | relative positioning of the 1x screen in
|
||||
// | Parent +----------+ the scaled coordinate space. Screen's
|
||||
// | 2x | | position is based off of the percentage
|
||||
// | | Screen | position along its parent. This percentage
|
||||
// | | 1x | position is preserved in the scaled
|
||||
// +--------------+ | coordinates.
|
||||
// | |
|
||||
// +----------+
|
||||
// Scaled Coordinates
|
||||
// +-------+ +
|
||||
// | | V
|
||||
// | Parent+----------+
|
||||
// | 2x | |
|
||||
// +-------+ Screen |
|
||||
// | 1x |
|
||||
// | |
|
||||
// | |
|
||||
// +----------+
|
||||
//
|
||||
// Unscaled Coordinates
|
||||
// +----------+ In this case, parent lies between the top and
|
||||
// | | bottom of parent. The roles are reversed when
|
||||
// +-------+ | this occurs, and screen is placed to maintain
|
||||
// | | Screen | parent's relative position along screen.
|
||||
// | Parent| 1x |
|
||||
// | 2x | |
|
||||
// +-------+ |
|
||||
// +----------+
|
||||
// Scaled Coordinates
|
||||
// ^ +----------+
|
||||
// | | |
|
||||
// + +----+ |
|
||||
// |Prnt| Screen |
|
||||
// | 2x | 1x |
|
||||
// +----+ |
|
||||
// | |
|
||||
// +----------+
|
||||
//
|
||||
// Scaled and Unscaled Coordinates
|
||||
// +--------+ If the two screens are bottom aligned or
|
||||
// | | right aligned, the ScreenPlacement will
|
||||
// | +--------+ have an offset of 0 relative to the
|
||||
// | | | end of the screen.
|
||||
// | | |
|
||||
// +--------+--------+
|
||||
|
||||
placement := ScreenPlacement{
|
||||
screen: screen,
|
||||
parent: parent,
|
||||
alignment: m.getScreenAlignment(screen, parent),
|
||||
offset: 0,
|
||||
offsetReference: BEGIN,
|
||||
}
|
||||
|
||||
screenBegin, screenEnd := 0, 0
|
||||
parentBegin, parentEnd := 0, 0
|
||||
|
||||
switch placement.alignment {
|
||||
case TOP, BOTTOM:
|
||||
screenBegin = screen.X
|
||||
screenEnd = screen.right()
|
||||
parentBegin = parent.X
|
||||
parentEnd = parent.right()
|
||||
case LEFT, RIGHT:
|
||||
screenBegin = screen.Y
|
||||
screenEnd = screen.bottom()
|
||||
parentBegin = parent.Y
|
||||
parentEnd = parent.bottom()
|
||||
}
|
||||
|
||||
// Since we're calculating offsets, make everything relative to parentBegin
|
||||
parentEnd -= parentBegin
|
||||
screenBegin -= parentBegin
|
||||
screenEnd -= parentBegin
|
||||
parentBegin = 0
|
||||
|
||||
// There are a few ways lines can intersect:
|
||||
// End Aligned
|
||||
// SCREEN's offset is relative to the END (BOTTOM or RIGHT).
|
||||
// +-PARENT----------------+
|
||||
// +-SCREEN-------------+
|
||||
//
|
||||
// Positioning based off of |screenBegin|.
|
||||
// SCREEN's offset is simply a percentage of its position on PARENT.
|
||||
// +-PARENT----------------+
|
||||
// ^+-SCREEN------------+
|
||||
//
|
||||
// Positioning based off of |screenEnd|.
|
||||
// SCREEN's offset is dependent on the percentage of its end position on PARENT.
|
||||
// +-PARENT----------------+
|
||||
// +-SCREEN------------+^
|
||||
//
|
||||
// Positioning based off of |parentBegin| on SCREEN.
|
||||
// SCREEN's offset is dependent on the percentage of its position on PARENT.
|
||||
// +-PARENT----------------+
|
||||
// ^+-SCREEN--------------------------+
|
||||
|
||||
if screenEnd == parentEnd {
|
||||
placement.offsetReference = END
|
||||
placement.offset = 0
|
||||
} else if screenBegin >= parentBegin {
|
||||
placement.offsetReference = BEGIN
|
||||
placement.offset = m.scaleOffset(parentEnd, parent.ScaleFactor, screenBegin)
|
||||
} else if screenEnd <= parentEnd {
|
||||
placement.offsetReference = END
|
||||
placement.offset = m.scaleOffset(parentEnd, parent.ScaleFactor, parentEnd-screenEnd)
|
||||
} else {
|
||||
placement.offsetReference = BEGIN
|
||||
placement.offset = m.scaleOffset(screenEnd-screenBegin, screen.ScaleFactor, screenBegin)
|
||||
}
|
||||
|
||||
return placement
|
||||
}
|
||||
|
||||
// Get screen alignment relative to parent (TOP, RIGHT, BOTTOM, LEFT)
|
||||
func (m *ScreenManager) getScreenAlignment(screen, parent *Screen) Alignment {
|
||||
maxLeft := max(screen.X, parent.X)
|
||||
maxTop := max(screen.Y, parent.Y)
|
||||
minRight := min(screen.right(), parent.right())
|
||||
minBottom := min(screen.bottom(), parent.bottom())
|
||||
|
||||
// Corners touching
|
||||
if maxLeft == minRight && maxTop == minBottom {
|
||||
if screen.Y == maxTop {
|
||||
return BOTTOM
|
||||
} else if parent.X == maxLeft {
|
||||
return LEFT
|
||||
}
|
||||
return TOP
|
||||
}
|
||||
|
||||
// Vertical edge touching
|
||||
if maxLeft == minRight {
|
||||
if screen.X == maxLeft {
|
||||
return RIGHT
|
||||
} else {
|
||||
return LEFT
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal edge touching
|
||||
if maxTop == minBottom {
|
||||
if screen.Y == maxTop {
|
||||
return BOTTOM
|
||||
} else {
|
||||
return TOP
|
||||
}
|
||||
}
|
||||
|
||||
return -1 // Shouldn't be reached
|
||||
}
|
||||
|
||||
func (m *ScreenManager) deIntersectScreens(screensPlacements []ScreenPlacement) {
|
||||
parentIDMap := make(map[string]string)
|
||||
for _, placement := range screensPlacements {
|
||||
parentIDMap[placement.screen.ID] = placement.parent.ID
|
||||
}
|
||||
|
||||
treeDepthMap := make(map[string]int)
|
||||
for _, screen := range m.screens {
|
||||
id, ok, depth := screen.ID, true, 0
|
||||
const maxDepth = 100
|
||||
for id != m.primaryScreen.ID && depth < maxDepth {
|
||||
depth++
|
||||
id, ok = parentIDMap[id]
|
||||
if !ok {
|
||||
depth = maxDepth
|
||||
}
|
||||
}
|
||||
treeDepthMap[screen.ID] = depth
|
||||
}
|
||||
|
||||
sortedScreens := make([]*Screen, len(m.screens))
|
||||
copy(sortedScreens, m.screens)
|
||||
|
||||
// Sort the screens first by their depth in the screen hierarchy tree,
|
||||
// and then by distance from screen origin to primary origin. This way we
|
||||
// process the screens starting at the root (the primary screen), in the
|
||||
// order of their descendance spanning out from the primary screen.
|
||||
sort.Slice(sortedScreens, func(i, j int) bool {
|
||||
s1, s2 := m.screens[i], m.screens[j]
|
||||
s1_depth := treeDepthMap[s1.ID]
|
||||
s2_depth := treeDepthMap[s2.ID]
|
||||
|
||||
if s1_depth != s2_depth {
|
||||
return s1_depth < s2_depth
|
||||
}
|
||||
|
||||
// Distance squared
|
||||
s1_distance := s1.X*s1.X + s1.Y*s1.Y
|
||||
s2_distance := s2.X*s2.X + s2.Y*s2.Y
|
||||
if s1_distance != s2_distance {
|
||||
return s1_distance < s2_distance
|
||||
}
|
||||
|
||||
return s1.ID < s2.ID
|
||||
})
|
||||
|
||||
for i := 1; i < len(sortedScreens); i++ {
|
||||
targetScreen := sortedScreens[i]
|
||||
for j := 0; j < i; j++ {
|
||||
sourceScreen := sortedScreens[j]
|
||||
if targetScreen.intersects(sourceScreen) {
|
||||
m.fixScreenIntersection(targetScreen, sourceScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Offset the target screen along either X or Y axis away from the origin
|
||||
// so that it removes the intersection with the source screen
|
||||
// This function assume both screens already intersect.
|
||||
func (m *ScreenManager) fixScreenIntersection(targetScreen, sourceScreen *Screen) {
|
||||
offsetX, offsetY := 0, 0
|
||||
|
||||
if targetScreen.X >= 0 {
|
||||
offsetX = sourceScreen.right() - targetScreen.X
|
||||
} else {
|
||||
offsetX = -(targetScreen.right() - sourceScreen.X)
|
||||
}
|
||||
|
||||
if targetScreen.Y >= 0 {
|
||||
offsetY = sourceScreen.bottom() - targetScreen.Y
|
||||
} else {
|
||||
offsetY = -(targetScreen.bottom() - sourceScreen.Y)
|
||||
}
|
||||
|
||||
// Choose the smaller offset (X or Y)
|
||||
if math.Abs(float64(offsetX)) <= math.Abs(float64(offsetY)) {
|
||||
offsetY = 0
|
||||
} else {
|
||||
offsetX = 0
|
||||
}
|
||||
|
||||
// Apply the offset
|
||||
newX := targetScreen.X + offsetX
|
||||
newY := targetScreen.Y + offsetY
|
||||
targetScreen.move(newX, newY)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) findAndRemoveTouchingScreens(parent *Screen, screens *[]*Screen) []*Screen {
|
||||
touchingScreens := []*Screen{}
|
||||
remainingScreens := []*Screen{}
|
||||
|
||||
for _, screen := range *screens {
|
||||
if m.areScreensTouching(parent, screen) {
|
||||
touchingScreens = append(touchingScreens, screen)
|
||||
} else {
|
||||
remainingScreens = append(remainingScreens, screen)
|
||||
}
|
||||
}
|
||||
*screens = remainingScreens
|
||||
return touchingScreens
|
||||
}
|
||||
|
||||
func (m *ScreenManager) areScreensTouching(a, b *Screen) bool {
|
||||
maxLeft := max(a.X, b.X)
|
||||
maxTop := max(a.Y, b.Y)
|
||||
minRight := min(a.right(), b.right())
|
||||
minBottom := min(a.bottom(), b.bottom())
|
||||
return (maxLeft == minRight && maxTop <= minBottom) || (maxTop == minBottom && maxLeft <= minRight)
|
||||
}
|
||||
|
||||
// Scale |unscaledOffset| to the same relative position on |unscaledLength|
|
||||
// based off of |unscaledLength|'s |scaleFactor|
|
||||
func (m *ScreenManager) scaleOffset(unscaledLength int, scaleFactor float32, unscaledOffset int) int {
|
||||
scaledLength := float32(unscaledLength) / scaleFactor
|
||||
percent := float32(unscaledOffset) / float32(unscaledLength)
|
||||
return int(math.Floor(float64(scaledLength * percent)))
|
||||
}
|
||||
|
||||
func (m *ScreenManager) screenNearestPoint(point Point, isPhysical bool) *Screen {
|
||||
for _, screen := range m.screens {
|
||||
if isPhysical {
|
||||
if screen.PhysicalBounds.Contains(point) {
|
||||
return screen
|
||||
}
|
||||
} else {
|
||||
if screen.Bounds.Contains(point) {
|
||||
return screen
|
||||
}
|
||||
}
|
||||
}
|
||||
return m.primaryScreen
|
||||
}
|
||||
|
||||
func (m *ScreenManager) screenNearestRect(rect Rect, isPhysical bool, excludedScreens map[string]bool) *Screen {
|
||||
var nearestScreen *Screen
|
||||
var distance, nearestScreenDistance int
|
||||
for _, screen := range m.screens {
|
||||
if excludedScreens[screen.ID] {
|
||||
continue
|
||||
}
|
||||
if isPhysical {
|
||||
distance = rect.distanceFromRectSquared(screen.PhysicalBounds)
|
||||
} else {
|
||||
distance = rect.distanceFromRectSquared(screen.Bounds)
|
||||
}
|
||||
if nearestScreen == nil || distance < nearestScreenDistance {
|
||||
nearestScreen = screen
|
||||
nearestScreenDistance = distance
|
||||
}
|
||||
}
|
||||
if !isPhysical && len(excludedScreens) < len(m.screens)-1 {
|
||||
// Make sure to give the same screen that would be given by the physical rect
|
||||
// of this dip rect so transforming back and forth always gives the same result.
|
||||
// This is important because it could happen that a dip rect intersects Screen1
|
||||
// more than Screen2 but in the physical layout Screen2 will scale up or Screen1
|
||||
// will scale down causing the intersection area to change so transforming back
|
||||
// would give a different rect.
|
||||
physicalRect := nearestScreen.dipToPhysicalRect(rect)
|
||||
physicalRectScreen := m.screenNearestRect(physicalRect, true, nil)
|
||||
if nearestScreen != physicalRectScreen {
|
||||
if excludedScreens == nil {
|
||||
excludedScreens = make(map[string]bool)
|
||||
}
|
||||
excludedScreens[nearestScreen.ID] = true
|
||||
return m.screenNearestRect(rect, isPhysical, excludedScreens)
|
||||
}
|
||||
}
|
||||
return nearestScreen
|
||||
}
|
||||
|
||||
func (m *ScreenManager) DipToPhysicalPoint(dipPoint Point) Point {
|
||||
screen := m.ScreenNearestDipPoint(dipPoint)
|
||||
return screen.dipToPhysicalPoint(dipPoint, false)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) PhysicalToDipPoint(physicalPoint Point) Point {
|
||||
screen := m.ScreenNearestPhysicalPoint(physicalPoint)
|
||||
return screen.physicalToDipPoint(physicalPoint, false)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) DipToPhysicalRect(dipRect Rect) Rect {
|
||||
screen := m.ScreenNearestDipRect(dipRect)
|
||||
return screen.dipToPhysicalRect(dipRect)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) PhysicalToDipRect(physicalRect Rect) Rect {
|
||||
screen := m.ScreenNearestPhysicalRect(physicalRect)
|
||||
return screen.physicalToDipRect(physicalRect)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) ScreenNearestPhysicalPoint(physicalPoint Point) *Screen {
|
||||
return m.screenNearestPoint(physicalPoint, true)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) ScreenNearestDipPoint(dipPoint Point) *Screen {
|
||||
return m.screenNearestPoint(dipPoint, false)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) ScreenNearestPhysicalRect(physicalRect Rect) *Screen {
|
||||
return m.screenNearestRect(physicalRect, true, nil)
|
||||
}
|
||||
|
||||
func (m *ScreenManager) ScreenNearestDipRect(dipRect Rect) *Screen {
|
||||
return m.screenNearestRect(dipRect, false, nil)
|
||||
}
|
||||
|
||||
// ================================================================================================
|
||||
// Exported application-level methods for internal convenience and availability to application devs
|
||||
|
||||
func DipToPhysicalPoint(dipPoint Point) Point {
|
||||
return globalApplication.screenManager.DipToPhysicalPoint(dipPoint)
|
||||
}
|
||||
|
||||
func PhysicalToDipPoint(physicalPoint Point) Point {
|
||||
return globalApplication.screenManager.PhysicalToDipPoint(physicalPoint)
|
||||
}
|
||||
|
||||
func DipToPhysicalRect(dipRect Rect) Rect {
|
||||
return globalApplication.screenManager.DipToPhysicalRect(dipRect)
|
||||
}
|
||||
|
||||
func PhysicalToDipRect(physicalRect Rect) Rect {
|
||||
return globalApplication.screenManager.PhysicalToDipRect(physicalRect)
|
||||
}
|
||||
|
||||
func ScreenNearestPhysicalPoint(physicalPoint Point) *Screen {
|
||||
return globalApplication.screenManager.ScreenNearestPhysicalPoint(physicalPoint)
|
||||
}
|
||||
|
||||
func ScreenNearestDipPoint(dipPoint Point) *Screen {
|
||||
return globalApplication.screenManager.ScreenNearestDipPoint(dipPoint)
|
||||
}
|
||||
|
||||
func ScreenNearestPhysicalRect(physicalRect Rect) *Screen {
|
||||
return globalApplication.screenManager.ScreenNearestPhysicalRect(physicalRect)
|
||||
}
|
||||
|
||||
func ScreenNearestDipRect(dipRect Rect) *Screen {
|
||||
return globalApplication.screenManager.ScreenNearestDipRect(dipRect)
|
||||
}
|
||||
716
v3/pkg/application/screenmanager_test.go
Normal file
716
v3/pkg/application/screenmanager_test.go
Normal file
|
|
@ -0,0 +1,716 @@
|
|||
package application_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type ScreenDef struct {
|
||||
id int
|
||||
w, h int
|
||||
s float32
|
||||
parent ScreenDefParent
|
||||
name string
|
||||
}
|
||||
|
||||
type ScreenDefParent struct {
|
||||
id int
|
||||
align string
|
||||
offset int
|
||||
}
|
||||
|
||||
type ScreensLayout struct {
|
||||
name string
|
||||
screens []ScreenDef
|
||||
}
|
||||
|
||||
type ParsedLayout struct {
|
||||
name string
|
||||
screens []*application.Screen
|
||||
}
|
||||
|
||||
func exampleLayouts() []ParsedLayout {
|
||||
layouts := [][]ScreensLayout{
|
||||
{
|
||||
// Normal examples (demonstrate real life scenarios)
|
||||
{
|
||||
name: "Single 4k monitor",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Two monitors",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
||||
{id: 2, w: 1920, h: 1080, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI`},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Two monitors (2)",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1920, h: 1080, s: 1, name: `23" FHD 96DPI`},
|
||||
{id: 2, w: 1920, h: 1080, s: 1.25, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI (125%)`},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Three monitors",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
||||
{id: 2, w: 1920, h: 1080, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI`},
|
||||
{id: 3, w: 1920, h: 1080, s: 1.25, parent: ScreenDefParent{id: 1, align: "l", offset: 0}, name: `23" FHD 96DPI (125%)`},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Four monitors",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
||||
{id: 2, w: 1920, h: 1080, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI`},
|
||||
{id: 3, w: 1920, h: 1080, s: 1.25, parent: ScreenDefParent{id: 2, align: "b", offset: 0}, name: `23" FHD 96DPI (125%)`},
|
||||
{id: 4, w: 1080, h: 1920, s: 1, parent: ScreenDefParent{id: 1, align: "l", offset: 0}, name: `23" FHD (90deg)`},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Test cases examples (demonstrate the algorithm basics)
|
||||
{
|
||||
name: "Child scaled, Start offset",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1200, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: 600}, name: "Child"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Child scaled, End offset",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1200, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: -600}, name: "Child"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Parent scaled, Start offset percent",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1200, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 600}, name: "Child"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Parent scaled, End offset percent",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1200, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: -600}, name: "Child"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Parent scaled, Start align",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1100, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: "Child"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Parent scaled, End align",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1200, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: "Child"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Parent scaled, in-between",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5, name: "Parent"},
|
||||
{id: 2, w: 1200, h: 1500, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: -250}, name: "Child"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Edge cases examples
|
||||
{
|
||||
name: "Parent order (5 is parent of 4)",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1920, h: 1080, s: 1},
|
||||
{id: 2, w: 1024, h: 600, s: 1.25, parent: ScreenDefParent{id: 1, align: "r", offset: -200}},
|
||||
{id: 3, w: 800, h: 800, s: 1.25, parent: ScreenDefParent{id: 2, align: "b", offset: 0}},
|
||||
{id: 4, w: 800, h: 1080, s: 1.5, parent: ScreenDefParent{id: 2, align: "re", offset: 100}},
|
||||
{id: 5, w: 600, h: 600, s: 1, parent: ScreenDefParent{id: 3, align: "r", offset: 100}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "de-intersection reparent",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1920, h: 1080, s: 1},
|
||||
{id: 2, w: 1680, h: 1050, s: 1.25, parent: ScreenDefParent{id: 1, align: "r", offset: 10}},
|
||||
{id: 3, w: 1440, h: 900, s: 1.5, parent: ScreenDefParent{id: 1, align: "le", offset: 150}},
|
||||
{id: 4, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 3, align: "bc", offset: -200}},
|
||||
{id: 5, w: 1024, h: 768, s: 1.25, parent: ScreenDefParent{id: 4, align: "r", offset: 400}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "de-intersection (unattached child)",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1920, h: 1080, s: 1},
|
||||
{id: 2, w: 1024, h: 768, s: 1.5, parent: ScreenDefParent{id: 1, align: "le", offset: 10}},
|
||||
{id: 3, w: 1024, h: 768, s: 1.25, parent: ScreenDefParent{id: 2, align: "b", offset: 100}},
|
||||
{id: 4, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 3, align: "r", offset: 500}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiple de-intersection",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1920, h: 1080, s: 1},
|
||||
{id: 2, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 1, align: "be", offset: 0}},
|
||||
{id: 3, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 2, align: "b", offset: 300}},
|
||||
{id: 4, w: 1024, h: 768, s: 1.5, parent: ScreenDefParent{id: 2, align: "le", offset: 100}},
|
||||
{id: 5, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 4, align: "be", offset: 100}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Multiple de-intersection (left-side)",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1920, h: 1080, s: 1},
|
||||
{id: 2, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 1, align: "le", offset: 0}},
|
||||
{id: 3, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 2, align: "b", offset: 300}},
|
||||
{id: 4, w: 1024, h: 768, s: 1.5, parent: ScreenDefParent{id: 2, align: "le", offset: 100}},
|
||||
{id: 5, w: 1024, h: 768, s: 1, parent: ScreenDefParent{id: 4, align: "be", offset: 100}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Parent de-intersection child offset",
|
||||
screens: []ScreenDef{
|
||||
{id: 1, w: 1600, h: 1600, s: 1.5},
|
||||
{id: 2, w: 800, h: 800, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}},
|
||||
{id: 3, w: 800, h: 800, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 800}},
|
||||
{id: 4, w: 800, h: 1600, s: 1, parent: ScreenDefParent{id: 2, align: "r", offset: 0}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
parsedLayouts := []ParsedLayout{}
|
||||
|
||||
for _, section := range layouts {
|
||||
for _, layout := range section {
|
||||
parsedLayouts = append(parsedLayouts, parseLayout(layout))
|
||||
}
|
||||
}
|
||||
|
||||
return parsedLayouts
|
||||
}
|
||||
|
||||
// Parse screens layout from easy-to-define ScreenDef for testing to actual Screens layout
|
||||
func parseLayout(layout ScreensLayout) ParsedLayout {
|
||||
screens := []*application.Screen{}
|
||||
|
||||
for _, screen := range layout.screens {
|
||||
var x, y int
|
||||
w := screen.w
|
||||
h := screen.h
|
||||
|
||||
if screen.parent.id > 0 {
|
||||
idx := slices.IndexFunc(screens, func(s *application.Screen) bool { return s.ID == strconv.Itoa(screen.parent.id) })
|
||||
parent := screens[idx].Bounds
|
||||
offset := screen.parent.offset
|
||||
align := screen.parent.align
|
||||
align2 := ""
|
||||
|
||||
if len(align) == 2 {
|
||||
align2 = string(align[1])
|
||||
align = string(align[0])
|
||||
}
|
||||
|
||||
x = parent.X
|
||||
y = parent.Y
|
||||
// t: top, b: bottom, l: left, r: right, e: edge, c: corner
|
||||
if align == "t" || align == "b" {
|
||||
x += offset
|
||||
if align2 == "e" || align2 == "c" {
|
||||
x += parent.Width
|
||||
}
|
||||
if align2 == "e" {
|
||||
x -= w
|
||||
}
|
||||
if align == "t" {
|
||||
y -= h
|
||||
} else {
|
||||
y += parent.Height
|
||||
}
|
||||
} else {
|
||||
y += offset
|
||||
if align2 == "e" || align2 == "c" {
|
||||
y += parent.Height
|
||||
}
|
||||
if align2 == "e" {
|
||||
y -= h
|
||||
}
|
||||
if align == "l" {
|
||||
x -= w
|
||||
} else {
|
||||
x += parent.Width
|
||||
}
|
||||
}
|
||||
}
|
||||
name := screen.name
|
||||
if name == "" {
|
||||
name = "Display" + strconv.Itoa(screen.id)
|
||||
}
|
||||
screens = append(screens, &application.Screen{
|
||||
ID: strconv.Itoa(screen.id),
|
||||
Name: name,
|
||||
ScaleFactor: float32(math.Round(float64(screen.s)*100) / 100),
|
||||
X: x,
|
||||
Y: y,
|
||||
Size: application.Size{Width: w, Height: h},
|
||||
Bounds: application.Rect{X: x, Y: y, Width: w, Height: h},
|
||||
PhysicalBounds: application.Rect{X: x, Y: y, Width: w, Height: h},
|
||||
WorkArea: application.Rect{X: x, Y: y, Width: w, Height: h - int(40*screen.s)},
|
||||
PhysicalWorkArea: application.Rect{X: x, Y: y, Width: w, Height: h - int(40*screen.s)},
|
||||
IsPrimary: screen.id == 1,
|
||||
Rotation: 0,
|
||||
})
|
||||
}
|
||||
return ParsedLayout{
|
||||
name: layout.name,
|
||||
screens: screens,
|
||||
}
|
||||
}
|
||||
|
||||
func matchRects(r1, r2 application.Rect) error {
|
||||
threshold := 1.0
|
||||
if math.Abs(float64(r1.X-r2.X)) > threshold ||
|
||||
math.Abs(float64(r1.Y-r2.Y)) > threshold ||
|
||||
math.Abs(float64(r1.Width-r2.Width)) > threshold ||
|
||||
math.Abs(float64(r1.Height-r2.Height)) > threshold {
|
||||
return fmt.Errorf("%v != %v", r1, r2)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test screens layout (DPI transformation)
|
||||
func TestScreenManager_ScreensLayout(t *testing.T) {
|
||||
sm := application.ScreenManager{}
|
||||
|
||||
t.Run("Child scaled", func(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1},
|
||||
{id: 2, w: 1200, h: 1200, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: 600}},
|
||||
}})
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
|
||||
screens := sm.Screens()
|
||||
is.Equal(len(screens), 2) // 2 screens
|
||||
is.Equal(screens[0].PhysicalBounds, application.Rect{X: 0, Y: 0, Width: 1200, Height: 1200}) // Parent physical bounds
|
||||
is.Equal(screens[0].Bounds, screens[0].PhysicalBounds) // Parent no scaling
|
||||
is.Equal(screens[1].PhysicalBounds, application.Rect{X: 1200, Y: 600, Width: 1200, Height: 1200}) // Child physical bounds
|
||||
is.Equal(screens[1].Bounds, application.Rect{X: 1200, Y: 600, Width: 800, Height: 800}) // Child DIP bounds
|
||||
})
|
||||
|
||||
t.Run("Parent scaled", func(t *testing.T) {
|
||||
is := is.New(t)
|
||||
|
||||
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5},
|
||||
{id: 2, w: 1200, h: 1200, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 600}},
|
||||
}})
|
||||
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
|
||||
screens := sm.Screens()
|
||||
is.Equal(len(screens), 2) // 2 screens
|
||||
is.Equal(screens[0].PhysicalBounds, application.Rect{X: 0, Y: 0, Width: 1200, Height: 1200}) // Parent physical bounds
|
||||
is.Equal(screens[0].Bounds, application.Rect{X: 0, Y: 0, Width: 800, Height: 800}) // Parent DIP bounds
|
||||
is.Equal(screens[1].PhysicalBounds, application.Rect{X: 1200, Y: 600, Width: 1200, Height: 1200}) // Child physical bounds
|
||||
is.Equal(screens[1].Bounds, application.Rect{X: 800, Y: 400, Width: 1200, Height: 1200}) // Child DIP bounds
|
||||
})
|
||||
}
|
||||
|
||||
// Test basic transformation between physical and DIP coordinates
|
||||
func TestScreenManager_BasicTranformation(t *testing.T) {
|
||||
sm := application.ScreenManager{}
|
||||
is := is.New(t)
|
||||
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1},
|
||||
{id: 2, w: 1200, h: 1200, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: 600}},
|
||||
}})
|
||||
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
|
||||
pt := application.Point{X: 100, Y: 100}
|
||||
is.Equal(sm.DipToPhysicalPoint(pt), pt) // DipToPhysicalPoint screen1
|
||||
is.Equal(sm.PhysicalToDipPoint(pt), pt) // PhysicalToDipPoint screen1
|
||||
|
||||
ptDip := application.Point{X: 1300, Y: 700}
|
||||
ptPhysical := application.Point{X: 1350, Y: 750}
|
||||
is.Equal(sm.DipToPhysicalPoint(ptDip), ptPhysical) // DipToPhysicalPoint screen2
|
||||
is.Equal(sm.PhysicalToDipPoint(ptPhysical), ptDip) // PhysicalToDipPoint screen2
|
||||
|
||||
rect := application.Rect{X: 100, Y: 100, Width: 200, Height: 300}
|
||||
is.Equal(sm.DipToPhysicalRect(rect), rect) // DipToPhysicalRect screen1
|
||||
is.Equal(sm.PhysicalToDipRect(rect), rect) // DipToPhysicalRect screen1
|
||||
|
||||
rectDip := application.Rect{X: 1300, Y: 700, Width: 200, Height: 300}
|
||||
rectPhysical := application.Rect{X: 1350, Y: 750, Width: 300, Height: 450}
|
||||
is.Equal(sm.DipToPhysicalRect(rectDip), rectPhysical) // DipToPhysicalRect screen2
|
||||
is.Equal(sm.PhysicalToDipRect(rectPhysical), rectDip) // DipToPhysicalRect screen2
|
||||
|
||||
rectDip = application.Rect{X: 2200, Y: 250, Width: 200, Height: 300}
|
||||
rectPhysical = application.Rect{X: 2700, Y: 75, Width: 300, Height: 450}
|
||||
is.Equal(sm.DipToPhysicalRect(rectDip), rectPhysical) // DipToPhysicalRect outside screen2
|
||||
is.Equal(sm.PhysicalToDipRect(rectPhysical), rectDip) // DipToPhysicalRect outside screen2
|
||||
}
|
||||
|
||||
func TestScreenManager_PrimaryScreen(t *testing.T) {
|
||||
sm := application.ScreenManager{}
|
||||
is := is.New(t)
|
||||
|
||||
for _, layout := range exampleLayouts() {
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
is.Equal(sm.PrimaryScreen(), layout.screens[0]) // Primary screen
|
||||
}
|
||||
|
||||
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5},
|
||||
{id: 2, w: 1200, h: 1200, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 600}},
|
||||
}})
|
||||
|
||||
layout.screens[0], layout.screens[1] = layout.screens[1], layout.screens[0]
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
is.Equal(sm.PrimaryScreen(), layout.screens[1]) // Primary screen
|
||||
|
||||
layout.screens[1].IsPrimary = false
|
||||
err = sm.LayoutScreens(layout.screens)
|
||||
is.True(err != nil) // Should error when no primary screen found
|
||||
}
|
||||
|
||||
// Test edge alignment between transformation
|
||||
// (points and rects on the screen edge should transform to the same precise edge position)
|
||||
func TestScreenManager_EdgeAlign(t *testing.T) {
|
||||
sm := application.ScreenManager{}
|
||||
is := is.New(t)
|
||||
|
||||
for _, layout := range exampleLayouts() {
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
for _, screen := range sm.Screens() {
|
||||
ptOriginDip := screen.Bounds.Origin()
|
||||
ptOriginPhysical := screen.PhysicalBounds.Origin()
|
||||
ptCornerDip := screen.Bounds.InsideCorner()
|
||||
ptCornerPhysical := screen.PhysicalBounds.InsideCorner()
|
||||
|
||||
is.Equal(sm.DipToPhysicalPoint(ptOriginDip), ptOriginPhysical) // DipToPhysicalPoint Origin
|
||||
is.Equal(sm.PhysicalToDipPoint(ptOriginPhysical), ptOriginDip) // PhysicalToDipPoint Origin
|
||||
is.Equal(sm.DipToPhysicalPoint(ptCornerDip), ptCornerPhysical) // DipToPhysicalPoint Corner
|
||||
is.Equal(sm.PhysicalToDipPoint(ptCornerPhysical), ptCornerDip) // PhysicalToDipPoint Corner
|
||||
|
||||
rectOriginDip := application.Rect{X: ptOriginDip.X, Y: ptOriginDip.Y, Width: 100, Height: 100}
|
||||
rectOriginPhysical := application.Rect{X: ptOriginPhysical.X, Y: ptOriginPhysical.Y, Width: 100, Height: 100}
|
||||
rectCornerDip := application.Rect{X: ptCornerDip.X - 99, Y: ptCornerDip.Y - 99, Width: 100, Height: 100}
|
||||
rectCornerPhysical := application.Rect{X: ptCornerPhysical.X - 99, Y: ptCornerPhysical.Y - 99, Width: 100, Height: 100}
|
||||
|
||||
is.Equal(sm.DipToPhysicalRect(rectOriginDip).Origin(), rectOriginPhysical.Origin()) // DipToPhysicalRect Origin
|
||||
is.Equal(sm.PhysicalToDipRect(rectOriginPhysical).Origin(), rectOriginDip.Origin()) // PhysicalToDipRect Origin
|
||||
is.Equal(sm.DipToPhysicalRect(rectCornerDip).Corner(), rectCornerPhysical.Corner()) // DipToPhysicalRect Corner
|
||||
is.Equal(sm.PhysicalToDipRect(rectCornerPhysical).Corner(), rectCornerDip.Corner()) // PhysicalToDipRect Corner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScreenManager_ProbePoints(t *testing.T) {
|
||||
sm := application.ScreenManager{}
|
||||
is := is.New(t)
|
||||
threshold := 1.0
|
||||
steps := 3
|
||||
|
||||
for _, layout := range exampleLayouts() {
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
for _, screen := range sm.Screens() {
|
||||
for i := 0; i <= 1; i++ {
|
||||
isDip := (i == 0)
|
||||
|
||||
var b application.Rect
|
||||
if isDip {
|
||||
b = screen.Bounds
|
||||
} else {
|
||||
b = screen.PhysicalBounds
|
||||
}
|
||||
|
||||
xStep := b.Width / steps
|
||||
yStep := b.Height / steps
|
||||
if xStep < 1 {
|
||||
xStep = 1
|
||||
}
|
||||
if yStep < 1 {
|
||||
yStep = 1
|
||||
}
|
||||
pt := b.Origin()
|
||||
xDone := false
|
||||
yDone := false
|
||||
|
||||
for !yDone {
|
||||
if pt.Y > b.InsideCorner().Y {
|
||||
pt.Y = b.InsideCorner().Y
|
||||
yDone = true
|
||||
}
|
||||
|
||||
pt.X = b.X
|
||||
xDone = false
|
||||
|
||||
for !xDone {
|
||||
if pt.X > b.InsideCorner().X {
|
||||
pt.X = b.InsideCorner().X
|
||||
xDone = true
|
||||
}
|
||||
var ptDblTransformed application.Point
|
||||
|
||||
if isDip {
|
||||
ptDblTransformed = sm.PhysicalToDipPoint(sm.DipToPhysicalPoint(pt))
|
||||
} else {
|
||||
ptDblTransformed = sm.DipToPhysicalPoint(sm.PhysicalToDipPoint(pt))
|
||||
}
|
||||
|
||||
is.True(math.Abs(float64(ptDblTransformed.X-pt.X)) <= threshold)
|
||||
is.True(math.Abs(float64(ptDblTransformed.Y-pt.Y)) <= threshold)
|
||||
pt.X += xStep
|
||||
}
|
||||
pt.Y += yStep
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test transformation drift over time
|
||||
func TestScreenManager_TransformationDrift(t *testing.T) {
|
||||
sm := application.ScreenManager{}
|
||||
is := is.New(t)
|
||||
|
||||
for _, layout := range exampleLayouts() {
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
for _, screen := range sm.Screens() {
|
||||
rectPhysicalOriginal := application.Rect{
|
||||
X: screen.PhysicalBounds.X + 100,
|
||||
Y: screen.PhysicalBounds.Y + 100,
|
||||
Width: 123,
|
||||
Height: 123,
|
||||
}
|
||||
|
||||
// Slide the position to catch any rounding errors
|
||||
for i := 0; i < 10; i++ {
|
||||
rectPhysicalOriginal.X++
|
||||
rectPhysicalOriginal.Y++
|
||||
rectPhysical := rectPhysicalOriginal
|
||||
// Transform back and forth several times to make sure no drift is introduced over time
|
||||
for j := 0; j < 10; j++ {
|
||||
rectDip := sm.PhysicalToDipRect(rectPhysical)
|
||||
rectPhysical = sm.DipToPhysicalRect(rectDip)
|
||||
}
|
||||
is.NoErr(matchRects(rectPhysical, rectPhysicalOriginal))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScreenManager_ScreenNearestRect(t *testing.T) {
|
||||
sm := application.ScreenManager{}
|
||||
is := is.New(t)
|
||||
|
||||
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
||||
{id: 1, w: 3840, h: 2160, s: 163.0 / 96, name: `27" 4K UHD 163DPI`},
|
||||
{id: 2, w: 1920, h: 1080, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}, name: `23" FHD 96DPI`},
|
||||
{id: 3, w: 1920, h: 1080, s: 1.25, parent: ScreenDefParent{id: 1, align: "l", offset: 0}, name: `23" FHD 96DPI (125%)`},
|
||||
}})
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
|
||||
type Rects map[string][]application.Rect
|
||||
|
||||
t.Run("DIP rects", func(t *testing.T) {
|
||||
is := is.New(t)
|
||||
rects := Rects{
|
||||
"1": []application.Rect{
|
||||
{X: -150, Y: 260, Width: 400, Height: 300},
|
||||
{X: -250, Y: 750, Width: 400, Height: 300},
|
||||
{X: -450, Y: 950, Width: 400, Height: 300},
|
||||
{X: 800, Y: 1350, Width: 400, Height: 300},
|
||||
{X: 2000, Y: 100, Width: 400, Height: 300},
|
||||
{X: 2100, Y: 950, Width: 400, Height: 300},
|
||||
{X: 2350, Y: 1200, Width: 400, Height: 300},
|
||||
},
|
||||
"2": []application.Rect{
|
||||
{X: 2100, Y: 50, Width: 400, Height: 300},
|
||||
{X: 2150, Y: 950, Width: 400, Height: 300},
|
||||
{X: 2450, Y: 1150, Width: 400, Height: 300},
|
||||
{X: 4300, Y: 400, Width: 400, Height: 300},
|
||||
},
|
||||
"3": []application.Rect{
|
||||
{X: -2000, Y: 100, Width: 400, Height: 300},
|
||||
{X: -220, Y: 200, Width: 400, Height: 300},
|
||||
{X: -300, Y: 750, Width: 400, Height: 300},
|
||||
{X: -500, Y: 900, Width: 400, Height: 300},
|
||||
},
|
||||
}
|
||||
|
||||
for screenID, screenRects := range rects {
|
||||
for _, rect := range screenRects {
|
||||
screen := sm.ScreenNearestDipRect(rect)
|
||||
is.Equal(screen.ID, screenID)
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("Physical rects", func(t *testing.T) {
|
||||
is := is.New(t)
|
||||
rects := Rects{
|
||||
"1": []application.Rect{
|
||||
{X: -150, Y: 100, Width: 400, Height: 300},
|
||||
{X: -250, Y: 1500, Width: 400, Height: 300},
|
||||
{X: 3600, Y: 100, Width: 400, Height: 300},
|
||||
},
|
||||
"2": []application.Rect{
|
||||
{X: 3700, Y: 100, Width: 400, Height: 300},
|
||||
{X: 4000, Y: 1150, Width: 400, Height: 300},
|
||||
},
|
||||
"3": []application.Rect{
|
||||
{X: -250, Y: 100, Width: 400, Height: 300},
|
||||
{X: -300, Y: 950, Width: 400, Height: 300},
|
||||
{X: -1000, Y: 1000, Width: 400, Height: 300},
|
||||
},
|
||||
}
|
||||
|
||||
for screenID, screenRects := range rects {
|
||||
for _, rect := range screenRects {
|
||||
screen := sm.ScreenNearestPhysicalRect(rect)
|
||||
is.Equal(screen.ID, screenID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// DIP rect is near screen1 but when transformed becomes near screen2.
|
||||
// To have a consistent transformation back & forth, screen nearest physical rect
|
||||
// should be the one given by ScreenNearestDipRect
|
||||
t.Run("Edge case 1", func(t *testing.T) {
|
||||
is := is.New(t)
|
||||
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1},
|
||||
{id: 2, w: 1200, h: 1300, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: -20}},
|
||||
}})
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
|
||||
rectDip := application.Rect{X: 1020, Y: 800, Width: 400, Height: 300}
|
||||
rectPhysical := sm.DipToPhysicalRect(rectDip)
|
||||
|
||||
screenDip := sm.ScreenNearestDipRect(rectDip)
|
||||
screenPhysical := sm.ScreenNearestPhysicalRect(rectPhysical)
|
||||
is.Equal(screenDip.ID, "2") // screenDip
|
||||
is.Equal(screenPhysical.ID, "2") // screenPhysical
|
||||
|
||||
rectDblTransformed := sm.PhysicalToDipRect(rectPhysical)
|
||||
is.NoErr(matchRects(rectDblTransformed, rectDip)) // double transformation
|
||||
})
|
||||
}
|
||||
|
||||
// Unsolved edge cases
|
||||
func TestScreenManager_UnsolvedEdgeCases(t *testing.T) {
|
||||
sm := application.ScreenManager{}
|
||||
is := is.New(t)
|
||||
|
||||
// Edge case 1: invalid DIP rect location
|
||||
// there could be a setup where some dip rects locations are invalid, meaning that there's no
|
||||
// physical rect that could produce that dip rect at this location
|
||||
// Not sure how to solve this scenario
|
||||
t.Run("Edge case 1: invalid dip rect", func(t *testing.T) {
|
||||
t.Skip("Unsolved edge case")
|
||||
is := is.New(t)
|
||||
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1},
|
||||
{id: 2, w: 1200, h: 1100, s: 1.5, parent: ScreenDefParent{id: 1, align: "r", offset: 0}},
|
||||
}})
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
|
||||
rectDip := application.Rect{X: 1050, Y: 700, Width: 400, Height: 300}
|
||||
rectPhysical := sm.DipToPhysicalRect(rectDip)
|
||||
|
||||
screenDip := sm.ScreenNearestDipRect(rectDip)
|
||||
screenPhysical := sm.ScreenNearestPhysicalRect(rectPhysical)
|
||||
is.Equal(screenDip.ID, screenPhysical.ID)
|
||||
|
||||
rectDblTransformed := sm.PhysicalToDipRect(rectPhysical)
|
||||
is.NoErr(matchRects(rectDblTransformed, rectDip)) // double transformation
|
||||
})
|
||||
|
||||
// Edge case 2: physical rect that changes when double transformed
|
||||
// there could be a setup where a dip rect at some locations could be produced by two different physical rects
|
||||
// causing one of these physical rects to be changed to the other when double transformed
|
||||
// Not sure how to solve this scenario
|
||||
t.Run("Edge case 2: changed physical rect", func(t *testing.T) {
|
||||
t.Skip("Unsolved edge case")
|
||||
is := is.New(t)
|
||||
layout := parseLayout(ScreensLayout{screens: []ScreenDef{
|
||||
{id: 1, w: 1200, h: 1200, s: 1.5},
|
||||
{id: 2, w: 1200, h: 900, s: 1, parent: ScreenDefParent{id: 1, align: "r", offset: 0}},
|
||||
}})
|
||||
err := sm.LayoutScreens(layout.screens)
|
||||
is.NoErr(err)
|
||||
|
||||
rectPhysical := application.Rect{X: 1050, Y: 890, Width: 400, Height: 300}
|
||||
rectDblTransformed := sm.DipToPhysicalRect(sm.PhysicalToDipRect(rectPhysical))
|
||||
is.NoErr(matchRects(rectDblTransformed, rectPhysical)) // double transformation
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkScreenManager_LayoutScreens(b *testing.B) {
|
||||
sm := application.ScreenManager{}
|
||||
layouts := exampleLayouts()
|
||||
screens := layouts[3].screens
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
sm.LayoutScreens(screens)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkScreenManager_TransformPoint(b *testing.B) {
|
||||
sm := application.ScreenManager{}
|
||||
layouts := exampleLayouts()
|
||||
screens := layouts[3].screens
|
||||
sm.LayoutScreens(screens)
|
||||
|
||||
pt := application.Point{X: 500, Y: 500}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
sm.DipToPhysicalPoint(pt)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkScreenManager_TransformRect(b *testing.B) {
|
||||
sm := application.ScreenManager{}
|
||||
layouts := exampleLayouts()
|
||||
screens := layouts[3].screens
|
||||
sm.LayoutScreens(screens)
|
||||
|
||||
rect := application.Rect{X: 500, Y: 500, Width: 800, Height: 600}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
sm.DipToPhysicalRect(rect)
|
||||
}
|
||||
}
|
||||
|
|
@ -55,9 +55,10 @@ func (s *windowsSystemTray) positionWindow(window *WebviewWindow, offset int) er
|
|||
}
|
||||
|
||||
screenBounds := currentScreen.WorkArea
|
||||
windowBounds := window.Bounds()
|
||||
|
||||
newX := screenBounds.Width - window.Width()
|
||||
newY := screenBounds.Height - window.Height()
|
||||
newX := screenBounds.Width - windowBounds.Width - offset
|
||||
newY := screenBounds.Height - windowBounds.Height - offset
|
||||
|
||||
// systray icons in windows can either be in the taskbar
|
||||
// or in a flyout menu.
|
||||
|
|
@ -66,13 +67,18 @@ func (s *windowsSystemTray) positionWindow(window *WebviewWindow, offset int) er
|
|||
return err
|
||||
}
|
||||
|
||||
// we only need the traybounds if the icon is in the tray
|
||||
var trayBounds *Rect
|
||||
var centerAlignX, centerAlignY int
|
||||
|
||||
// we only need the traybounds if the icon is in the tray
|
||||
if iconIsInTrayBounds {
|
||||
trayBounds, err = s.bounds()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*trayBounds = PhysicalToDipRect(*trayBounds)
|
||||
centerAlignX = trayBounds.X + (trayBounds.Width / 2) - (windowBounds.Width / 2)
|
||||
centerAlignY = trayBounds.Y + (trayBounds.Height / 2) - (windowBounds.Height / 2)
|
||||
}
|
||||
|
||||
taskbarBounds := w32.GetTaskbarPosition()
|
||||
|
|
@ -82,26 +88,28 @@ func (s *windowsSystemTray) positionWindow(window *WebviewWindow, offset int) er
|
|||
// to adjust the position so the window is centered on the icon
|
||||
switch taskbarBounds.UEdge {
|
||||
case w32.ABE_LEFT:
|
||||
if iconIsInTrayBounds && trayBounds.Y-(window.Height()/2) >= 0 {
|
||||
newY = trayBounds.Y - (window.Height() / 2)
|
||||
if iconIsInTrayBounds && centerAlignY <= newY {
|
||||
newY = centerAlignY
|
||||
}
|
||||
window.SetRelativePosition(offset, newY)
|
||||
newX = screenBounds.X + offset
|
||||
case w32.ABE_TOP:
|
||||
if iconIsInTrayBounds && trayBounds.X-(window.Width()/2) <= newX {
|
||||
newX = trayBounds.X - (window.Width() / 2)
|
||||
if iconIsInTrayBounds && centerAlignX <= newX {
|
||||
newX = centerAlignX
|
||||
}
|
||||
window.SetRelativePosition(newX, offset)
|
||||
newY = screenBounds.Y + offset
|
||||
case w32.ABE_RIGHT:
|
||||
if iconIsInTrayBounds && trayBounds.Y-(window.Height()/2) <= newY {
|
||||
newY = trayBounds.Y - (window.Height() / 2)
|
||||
if iconIsInTrayBounds && centerAlignY <= newY {
|
||||
newY = centerAlignY
|
||||
}
|
||||
window.SetRelativePosition(screenBounds.Width-window.Width()-offset, newY)
|
||||
case w32.ABE_BOTTOM:
|
||||
if iconIsInTrayBounds && trayBounds.X-(window.Width()/2) <= newX {
|
||||
newX = trayBounds.X - (window.Width() / 2)
|
||||
if iconIsInTrayBounds && centerAlignX <= newX {
|
||||
newX = centerAlignX
|
||||
}
|
||||
window.SetRelativePosition(newX, screenBounds.Height-window.Height()-offset)
|
||||
}
|
||||
newPos := currentScreen.relativeToAbsoluteDipPoint(Point{X: newX, Y: newY})
|
||||
windowBounds.X = newPos.X
|
||||
windowBounds.Y = newPos.Y
|
||||
window.SetBounds(windowBounds)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +150,7 @@ func (s *windowsSystemTray) iconIsInTrayBounds() (bool, error) {
|
|||
|
||||
func (s *windowsSystemTray) getScreen() (*Screen, error) {
|
||||
// Get the screen for this systray
|
||||
return getScreen(s.hwnd)
|
||||
return getScreenForWindowHwnd(s.hwnd)
|
||||
}
|
||||
|
||||
func (s *windowsSystemTray) setMenu(menu *Menu) {
|
||||
|
|
@ -257,7 +265,6 @@ func (s *windowsSystemTray) updateIcon() {
|
|||
if !w32.ShellNotifyIcon(w32.NIM_MODIFY, &nid) {
|
||||
panic(syscall.GetLastError())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *windowsSystemTray) newNotifyIconData() w32.NOTIFYICONDATA {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ type (
|
|||
size() (int, int)
|
||||
width() int
|
||||
height() int
|
||||
relativePosition() (int, int)
|
||||
destroy()
|
||||
reload()
|
||||
forceReload()
|
||||
|
|
@ -57,7 +56,6 @@ type (
|
|||
close()
|
||||
zoom()
|
||||
setHTML(html string)
|
||||
setRelativePosition(x int, y int)
|
||||
on(eventID uint)
|
||||
minimise()
|
||||
unminimise()
|
||||
|
|
@ -82,8 +80,14 @@ type (
|
|||
startResize(border string) error
|
||||
print() error
|
||||
setEnabled(enabled bool)
|
||||
physicalBounds() Rect
|
||||
setPhysicalBounds(physicalBounds Rect)
|
||||
bounds() Rect
|
||||
setBounds(bounds Rect)
|
||||
position() (int, int)
|
||||
setPosition(x int, y int)
|
||||
relativePosition() (int, int)
|
||||
setRelativePosition(x int, y int)
|
||||
flash(enabled bool)
|
||||
handleKeyEvent(acceleratorString string)
|
||||
getBorderSizes() *LRTB
|
||||
|
|
@ -804,7 +808,73 @@ func (w *WebviewWindow) Height() int {
|
|||
return InvokeSyncWithResult(w.impl.height)
|
||||
}
|
||||
|
||||
// RelativePosition returns the relative position of the window to the screen
|
||||
// PhysicalBounds returns the physical bounds of the window
|
||||
func (w *WebviewWindow) PhysicalBounds() Rect {
|
||||
if w.impl == nil && !w.isDestroyed() {
|
||||
return Rect{}
|
||||
}
|
||||
var rect Rect
|
||||
InvokeSync(func() {
|
||||
rect = w.impl.physicalBounds()
|
||||
})
|
||||
return rect
|
||||
}
|
||||
|
||||
// SetPhysicalBounds sets the physical bounds of the window
|
||||
func (w *WebviewWindow) SetPhysicalBounds(physicalBounds Rect) {
|
||||
if w.impl == nil && !w.isDestroyed() {
|
||||
return
|
||||
}
|
||||
InvokeSync(func() {
|
||||
w.impl.setPhysicalBounds(physicalBounds)
|
||||
})
|
||||
}
|
||||
|
||||
// Bounds returns the DIP bounds of the window
|
||||
func (w *WebviewWindow) Bounds() Rect {
|
||||
if w.impl == nil && !w.isDestroyed() {
|
||||
return Rect{}
|
||||
}
|
||||
var rect Rect
|
||||
InvokeSync(func() {
|
||||
rect = w.impl.bounds()
|
||||
})
|
||||
return rect
|
||||
}
|
||||
|
||||
// SetBounds sets the DIP bounds of the window
|
||||
func (w *WebviewWindow) SetBounds(bounds Rect) {
|
||||
if w.impl == nil && !w.isDestroyed() {
|
||||
return
|
||||
}
|
||||
InvokeSync(func() {
|
||||
w.impl.setBounds(bounds)
|
||||
})
|
||||
}
|
||||
|
||||
// Position returns the absolute position of the window
|
||||
func (w *WebviewWindow) Position() (int, int) {
|
||||
if w.impl == nil && !w.isDestroyed() {
|
||||
return 0, 0
|
||||
}
|
||||
var x, y int
|
||||
InvokeSync(func() {
|
||||
x, y = w.impl.position()
|
||||
})
|
||||
return x, y
|
||||
}
|
||||
|
||||
// SetPosition sets the absolute position of the window.
|
||||
func (w *WebviewWindow) SetPosition(x int, y int) {
|
||||
if w.impl == nil && !w.isDestroyed() {
|
||||
return
|
||||
}
|
||||
InvokeSync(func() {
|
||||
w.impl.setPosition(x, y)
|
||||
})
|
||||
}
|
||||
|
||||
// RelativePosition returns the position of the window relative to the screen WorkArea on which it is
|
||||
func (w *WebviewWindow) RelativePosition() (int, int) {
|
||||
if w.impl == nil && !w.isDestroyed() {
|
||||
return 0, 0
|
||||
|
|
@ -816,16 +886,16 @@ func (w *WebviewWindow) RelativePosition() (int, int) {
|
|||
return x, y
|
||||
}
|
||||
|
||||
// Position returns the absolute position of the window to the screen
|
||||
func (w *WebviewWindow) Position() (int, int) {
|
||||
if w.impl == nil && !w.isDestroyed() {
|
||||
return 0, 0
|
||||
// SetRelativePosition sets the position of the window relative to the screen WorkArea on which it is.
|
||||
func (w *WebviewWindow) SetRelativePosition(x, y int) Window {
|
||||
w.options.X = x
|
||||
w.options.Y = y
|
||||
if w.impl != nil {
|
||||
InvokeSync(func() {
|
||||
w.impl.setRelativePosition(x, y)
|
||||
})
|
||||
}
|
||||
var x, y int
|
||||
InvokeSync(func() {
|
||||
x, y = w.impl.position()
|
||||
})
|
||||
return x, y
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) Destroy() {
|
||||
|
|
@ -948,18 +1018,6 @@ func (w *WebviewWindow) SetHTML(html string) Window {
|
|||
return w
|
||||
}
|
||||
|
||||
// SetRelativePosition sets the position of the window.
|
||||
func (w *WebviewWindow) SetRelativePosition(x, y int) Window {
|
||||
w.options.X = x
|
||||
w.options.Y = y
|
||||
if w.impl != nil {
|
||||
InvokeSync(func() {
|
||||
w.impl.setRelativePosition(x, y)
|
||||
})
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// Minimise minimises the window.
|
||||
func (w *WebviewWindow) Minimise() Window {
|
||||
if w.impl == nil && !w.isDestroyed() {
|
||||
|
|
@ -1193,16 +1251,6 @@ func (w *WebviewWindow) SetEnabled(enabled bool) {
|
|||
})
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) SetPosition(x int, y int) {
|
||||
// set absolute position
|
||||
if w.impl == nil && !w.isDestroyed() {
|
||||
return
|
||||
}
|
||||
InvokeSync(func() {
|
||||
w.impl.setPosition(x, y)
|
||||
})
|
||||
}
|
||||
|
||||
func (w *WebviewWindow) processKeyBinding(acceleratorString string) bool {
|
||||
// Check menu bindings
|
||||
if w.menuBindings != nil {
|
||||
|
|
|
|||
|
|
@ -1312,6 +1312,38 @@ func (w *macosWebviewWindow) position() (int, int) {
|
|||
return int(x), int(y)
|
||||
}
|
||||
|
||||
func (w *macosWebviewWindow) bounds() Rect {
|
||||
// DOTO: do it in a single step + proper DPI scaling
|
||||
var x, y, width, height C.int
|
||||
InvokeSync(func() {
|
||||
C.windowGetPosition(w.nsWindow, &x, &y)
|
||||
C.windowGetSize(w.nsWindow, &width, &height)
|
||||
})
|
||||
|
||||
return Rect{
|
||||
X: int(x),
|
||||
Y: int(y),
|
||||
Width: int(width),
|
||||
Height: int(height),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *macosWebviewWindow) setBounds(bounds Rect) {
|
||||
// DOTO: do it in a single step + proper DPI scaling
|
||||
C.windowSetPosition(w.nsWindow, C.int(bounds.X), C.int(bounds.Y))
|
||||
C.windowSetSize(w.nsWindow, C.int(bounds.Width), C.int(bounds.Height))
|
||||
}
|
||||
|
||||
func (w *macosWebviewWindow) physicalBounds() Rect {
|
||||
// TODO: proper DPI scaling
|
||||
return w.bounds()
|
||||
}
|
||||
|
||||
func (w *macosWebviewWindow) setPhysicalBounds(physicalBounds Rect) {
|
||||
// TODO: proper DPI scaling
|
||||
w.setBounds(physicalBounds)
|
||||
}
|
||||
|
||||
func (w *macosWebviewWindow) destroy() {
|
||||
w.parent.markAsDestroyed()
|
||||
C.windowDestroy(w.nsWindow)
|
||||
|
|
|
|||
|
|
@ -105,8 +105,8 @@ func (w *linuxWebviewWindow) setMaximiseButtonEnabled(enabled bool) {
|
|||
}
|
||||
|
||||
func (w *linuxWebviewWindow) disableSizeConstraints() {
|
||||
x, y, width, height, scale := w.getCurrentMonitorGeometry()
|
||||
w.setMinMaxSize(x, y, width*scale, height*scale)
|
||||
x, y, width, height, scaleFactor := w.getCurrentMonitorGeometry()
|
||||
w.setMinMaxSize(x, y, width*scaleFactor, height*scaleFactor)
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) unminimise() {
|
||||
|
|
@ -204,6 +204,36 @@ func (w *linuxWebviewWindow) setPosition(x int, y int) {
|
|||
w.move(x, y)
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) bounds() Rect {
|
||||
// DOTO: do it in a single step + proper DPI scaling
|
||||
x, y := w.position()
|
||||
width, height := w.size()
|
||||
|
||||
return Rect{
|
||||
X: x,
|
||||
Y: y,
|
||||
Width: width,
|
||||
Height: height,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) setBounds(bounds Rect) {
|
||||
// DOTO: do it in a single step + proper DPI scaling
|
||||
w.move(bounds.X, bounds.Y)
|
||||
w.setSize(bounds.Width, bounds.Height)
|
||||
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) physicalBounds() Rect {
|
||||
// TODO: proper DPI scaling
|
||||
return w.bounds()
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) setPhysicalBounds(physicalBounds Rect) {
|
||||
// TODO: proper DPI scaling
|
||||
w.setBounds(physicalBounds)
|
||||
}
|
||||
|
||||
func (w *linuxWebviewWindow) run() {
|
||||
for eventId := range w.parent.eventListeners {
|
||||
w.on(eventId)
|
||||
|
|
|
|||
|
|
@ -138,19 +138,6 @@ type WebviewWindowOptions struct {
|
|||
IgnoreMouseEvents bool
|
||||
}
|
||||
|
||||
var WebviewWindowDefaults = &WebviewWindowOptions{
|
||||
Title: "",
|
||||
Width: 800,
|
||||
Height: 600,
|
||||
URL: "",
|
||||
BackgroundColour: RGBA{
|
||||
Red: 255,
|
||||
Green: 255,
|
||||
Blue: 255,
|
||||
Alpha: 255,
|
||||
},
|
||||
}
|
||||
|
||||
type RGBA struct {
|
||||
Red, Green, Blue, Alpha uint8
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import (
|
|||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
|
|
@ -49,6 +48,7 @@ type windowsWebviewWindow struct {
|
|||
hwnd w32.HWND
|
||||
menu *Win32Menu
|
||||
currentlyOpenContextMenu *Win32Menu
|
||||
ignoreDPIChangeResizing bool
|
||||
|
||||
// Fullscreen flags
|
||||
isCurrentlyFullscreen bool
|
||||
|
|
@ -103,34 +103,6 @@ func (w *windowsWebviewWindow) handleKeyEvent(_ string) {
|
|||
// Unused on windows
|
||||
}
|
||||
|
||||
// getBorderSizes returns the extended border size for the window
|
||||
func (w *windowsWebviewWindow) getBorderSizes() *LRTB {
|
||||
var result LRTB
|
||||
var frame w32.RECT
|
||||
w32.DwmGetWindowAttribute(w.hwnd, w32.DWMWA_EXTENDED_FRAME_BOUNDS, unsafe.Pointer(&frame), unsafe.Sizeof(frame))
|
||||
rect := w32.GetWindowRect(w.hwnd)
|
||||
result.Left = int(frame.Left - rect.Left)
|
||||
result.Top = int(frame.Top - rect.Top)
|
||||
result.Right = int(rect.Right - frame.Right)
|
||||
result.Bottom = int(rect.Bottom - frame.Bottom)
|
||||
return &result
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setPosition(x int, y int) {
|
||||
// Set the window's absolute position
|
||||
borderSize := w.getBorderSizes()
|
||||
w32.SetWindowPos(w.hwnd, 0, x-borderSize.Left, y-borderSize.Top, 0, 0, w32.SWP_NOSIZE|w32.SWP_NOZORDER)
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) position() (int, int) {
|
||||
rect := w32.GetWindowRect(w.hwnd)
|
||||
borderSizes := w.getBorderSizes()
|
||||
x := int(rect.Left) + borderSizes.Left
|
||||
y := int(rect.Top) + borderSizes.Top
|
||||
left, right := w.scaleToDefaultDPI(x, y)
|
||||
return left, right
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setEnabled(enabled bool) {
|
||||
w32.EnableWindow(w.hwnd, enabled)
|
||||
}
|
||||
|
|
@ -166,13 +138,6 @@ func (w *windowsWebviewWindow) setTitle(title string) {
|
|||
w32.SetWindowText(w.hwnd, title)
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setSize(width, height int) {
|
||||
rect := w32.GetWindowRect(w.hwnd)
|
||||
width, height = w.scaleWithWindowDPI(width, height)
|
||||
w32.MoveWindow(w.hwnd, int(rect.Left), int(rect.Top), width, height, true)
|
||||
w.chromium.Resize()
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setAlwaysOnTop(alwaysOnTop bool) {
|
||||
w32.SetWindowPos(w.hwnd,
|
||||
lo.Ternary(alwaysOnTop, w32.HWND_TOPMOST, w32.HWND_NOTOPMOST),
|
||||
|
|
@ -250,9 +215,21 @@ func (w *windowsWebviewWindow) run() {
|
|||
exStyle = options.Windows.ExStyle
|
||||
}
|
||||
|
||||
// ToDo: X, Y should also be scaled, should it be always relative to the main monitor?
|
||||
var startX, _ = lo.Coalesce(options.X, w32.CW_USEDEFAULT)
|
||||
var startY, _ = lo.Coalesce(options.Y, w32.CW_USEDEFAULT)
|
||||
bounds := Rect{
|
||||
X: options.X,
|
||||
Y: options.Y,
|
||||
Width: options.Width,
|
||||
Height: options.Height,
|
||||
}
|
||||
initialScreen := ScreenNearestDipRect(bounds)
|
||||
physicalBounds := initialScreen.dipToPhysicalRect(bounds)
|
||||
|
||||
// Default window position applied by the system
|
||||
// TODO: provide a way to set (0,0) as an initial position?
|
||||
if options.X == 0 && options.Y == 0 {
|
||||
physicalBounds.X = w32.CW_USEDEFAULT
|
||||
physicalBounds.Y = w32.CW_USEDEFAULT
|
||||
}
|
||||
|
||||
var appMenu w32.HMENU
|
||||
|
||||
|
|
@ -279,10 +256,10 @@ func (w *windowsWebviewWindow) run() {
|
|||
w32.MustStringToUTF16Ptr(globalApplication.options.Windows.WndClass),
|
||||
w32.MustStringToUTF16Ptr(options.Title),
|
||||
style,
|
||||
startX,
|
||||
startY,
|
||||
w32.CW_USEDEFAULT,
|
||||
w32.CW_USEDEFAULT,
|
||||
physicalBounds.X,
|
||||
physicalBounds.Y,
|
||||
physicalBounds.Width,
|
||||
physicalBounds.Height,
|
||||
parent,
|
||||
appMenu,
|
||||
w32.GetModuleHandle(""),
|
||||
|
|
@ -292,7 +269,12 @@ func (w *windowsWebviewWindow) run() {
|
|||
panic("Unable to create window")
|
||||
}
|
||||
|
||||
w.setSize(options.Width, options.Height)
|
||||
// Ensure correct window size in case the scale factor of current screen is different from the initial one.
|
||||
// This could happen when using the default window position and the window launches on a secondary monitor.
|
||||
currentScreen, _ := w.getScreen()
|
||||
if currentScreen.ScaleFactor != initialScreen.ScaleFactor {
|
||||
w.setSize(options.Width, options.Height)
|
||||
}
|
||||
|
||||
w.setupChromium()
|
||||
|
||||
|
|
@ -422,48 +404,114 @@ func (w *windowsWebviewWindow) enableSizeConstraints() {
|
|||
}
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) size() (int, int) {
|
||||
rect := w32.GetWindowRect(w.hwnd)
|
||||
width := int(rect.Right - rect.Left)
|
||||
height := int(rect.Bottom - rect.Top)
|
||||
// Scaling appears to give invalid results...
|
||||
//width, height = w.scaleToDefaultDPI(width, height)
|
||||
return width, height
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) update() {
|
||||
w32.UpdateWindow(w.hwnd)
|
||||
}
|
||||
|
||||
// getBorderSizes returns the extended border size for the window
|
||||
func (w *windowsWebviewWindow) getBorderSizes() *LRTB {
|
||||
var result LRTB
|
||||
var frame w32.RECT
|
||||
w32.DwmGetWindowAttribute(w.hwnd, w32.DWMWA_EXTENDED_FRAME_BOUNDS, unsafe.Pointer(&frame), unsafe.Sizeof(frame))
|
||||
rect := w32.GetWindowRect(w.hwnd)
|
||||
result.Left = int(frame.Left - rect.Left)
|
||||
result.Top = int(frame.Top - rect.Top)
|
||||
result.Right = int(rect.Right - frame.Right)
|
||||
result.Bottom = int(rect.Bottom - frame.Bottom)
|
||||
return &result
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) physicalBounds() Rect {
|
||||
// var rect w32.RECT
|
||||
// // Get the extended frame bounds instead of the window rect to offset the invisible borders in Windows 10
|
||||
// w32.DwmGetWindowAttribute(w.hwnd, w32.DWMWA_EXTENDED_FRAME_BOUNDS, unsafe.Pointer(&rect), unsafe.Sizeof(rect))
|
||||
rect := w32.GetWindowRect(w.hwnd)
|
||||
return Rect{
|
||||
X: int(rect.Left),
|
||||
Y: int(rect.Top),
|
||||
Width: int(rect.Right - rect.Left),
|
||||
Height: int(rect.Bottom - rect.Top),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setPhysicalBounds(physicalBounds Rect) {
|
||||
// // Offset invisible borders
|
||||
// borderSize := w.getBorderSizes()
|
||||
// physicalBounds.X -= borderSize.Left
|
||||
// physicalBounds.Y -= borderSize.Top
|
||||
// physicalBounds.Width += borderSize.Left + borderSize.Right
|
||||
// physicalBounds.Height += borderSize.Top + borderSize.Bottom
|
||||
|
||||
// Set flag to ignore resizing the window with DPI change because we already calculated the correct size
|
||||
// for the target position, this prevents double resizing issue when the window is moved between screens
|
||||
previousFlag := w.ignoreDPIChangeResizing
|
||||
w.ignoreDPIChangeResizing = true
|
||||
w32.SetWindowPos(w.hwnd, 0, physicalBounds.X, physicalBounds.Y, physicalBounds.Width, physicalBounds.Height, w32.SWP_NOZORDER|w32.SWP_NOACTIVATE)
|
||||
w.ignoreDPIChangeResizing = previousFlag
|
||||
}
|
||||
|
||||
// Get window dip bounds
|
||||
func (w *windowsWebviewWindow) bounds() Rect {
|
||||
return PhysicalToDipRect(w.physicalBounds())
|
||||
}
|
||||
|
||||
// Set window dip bounds
|
||||
func (w *windowsWebviewWindow) setBounds(bounds Rect) {
|
||||
w.setPhysicalBounds(DipToPhysicalRect(bounds))
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) size() (int, int) {
|
||||
bounds := w.bounds()
|
||||
return bounds.Width, bounds.Height
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) width() int {
|
||||
width, _ := w.size()
|
||||
return width
|
||||
return w.bounds().Width
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) height() int {
|
||||
_, height := w.size()
|
||||
return height
|
||||
return w.bounds().Height
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setSize(width, height int) {
|
||||
bounds := w.bounds()
|
||||
bounds.Width = width
|
||||
bounds.Height = height
|
||||
|
||||
w.setBounds(bounds)
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) position() (int, int) {
|
||||
bounds := w.bounds()
|
||||
return bounds.X, bounds.Y
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setPosition(x int, y int) {
|
||||
bounds := w.bounds()
|
||||
bounds.X = x
|
||||
bounds.Y = y
|
||||
|
||||
w.setBounds(bounds)
|
||||
}
|
||||
|
||||
// Get window position relative to the screen WorkArea on which it is
|
||||
func (w *windowsWebviewWindow) relativePosition() (int, int) {
|
||||
// Get monitor for window
|
||||
monitor := w32.MonitorFromWindow(w.hwnd, w32.MONITOR_DEFAULTTONEAREST)
|
||||
var monitorInfo w32.MONITORINFO
|
||||
monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo))
|
||||
w32.GetMonitorInfo(monitor, &monitorInfo)
|
||||
screen, _ := w.getScreen()
|
||||
pos := screen.absoluteToRelativeDipPoint(w.bounds().Origin())
|
||||
// Relative to WorkArea origin
|
||||
pos.X -= (screen.WorkArea.X - screen.X)
|
||||
pos.Y -= (screen.WorkArea.Y - screen.Y)
|
||||
return pos.X, pos.Y
|
||||
}
|
||||
|
||||
// Get window rect
|
||||
rect := w32.GetWindowRect(w.hwnd)
|
||||
|
||||
// Calculate relative position
|
||||
x := int(rect.Left) - int(monitorInfo.RcWork.Left)
|
||||
y := int(rect.Top) - int(monitorInfo.RcWork.Top)
|
||||
|
||||
borderSize := w.getBorderSizes()
|
||||
x += borderSize.Left
|
||||
y += borderSize.Top
|
||||
|
||||
return w.scaleToDefaultDPI(x, y)
|
||||
// Set window position relative to the screen WorkArea on which it is
|
||||
func (w *windowsWebviewWindow) setRelativePosition(x int, y int) {
|
||||
screen, _ := w.getScreen()
|
||||
pos := screen.relativeToAbsoluteDipPoint(Point{X: x, Y: y})
|
||||
// Relative to WorkArea origin
|
||||
pos.X += (screen.WorkArea.X - screen.X)
|
||||
pos.Y += (screen.WorkArea.Y - screen.Y)
|
||||
w.setPosition(pos.X, pos.Y)
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) destroy() {
|
||||
|
|
@ -542,16 +590,6 @@ func (w *windowsWebviewWindow) setHTML(html string) {
|
|||
w.execJS(fmt.Sprintf("document.documentElement.innerHTML = %q;", html))
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setRelativePosition(x int, y int) {
|
||||
//x, y = w.scaleWithWindowDPI(x, y)
|
||||
info := w32.GetMonitorInfoForWindow(w.hwnd)
|
||||
workRect := info.RcWork
|
||||
borderSize := w.getBorderSizes()
|
||||
x -= borderSize.Left
|
||||
y -= borderSize.Top
|
||||
w32.SetWindowPos(w.hwnd, w32.HWND_TOP, int(workRect.Left)+x, int(workRect.Top)+y, 0, 0, w32.SWP_NOSIZE)
|
||||
}
|
||||
|
||||
// on is used to indicate that a particular event should be listened for
|
||||
func (w *windowsWebviewWindow) on(_ uint) {
|
||||
// We don't need to worry about this in Windows as we do not need
|
||||
|
|
@ -840,45 +878,9 @@ func (w *windowsWebviewWindow) hide() {
|
|||
w32.ShowWindow(w.hwnd, w32.SW_HIDE)
|
||||
}
|
||||
|
||||
func getScreen(hwnd w32.HWND) (*Screen, error) {
|
||||
hMonitor := w32.MonitorFromWindow(hwnd, w32.MONITOR_DEFAULTTONEAREST)
|
||||
var mi w32.MONITORINFOEX
|
||||
mi.CbSize = uint32(unsafe.Sizeof(mi))
|
||||
w32.GetMonitorInfoEx(hMonitor, &mi)
|
||||
var thisScreen Screen
|
||||
thisScreen.X = int(mi.RcMonitor.Left)
|
||||
thisScreen.Y = int(mi.RcMonitor.Top)
|
||||
thisScreen.Size = Size{
|
||||
Width: int(mi.RcMonitor.Right - mi.RcMonitor.Left),
|
||||
Height: int(mi.RcMonitor.Bottom - mi.RcMonitor.Top),
|
||||
}
|
||||
thisScreen.Bounds = Rect{
|
||||
X: int(mi.RcMonitor.Left),
|
||||
Y: int(mi.RcMonitor.Top),
|
||||
Width: int(mi.RcMonitor.Right - mi.RcMonitor.Left),
|
||||
Height: int(mi.RcMonitor.Bottom - mi.RcMonitor.Top),
|
||||
}
|
||||
thisScreen.WorkArea = Rect{
|
||||
X: int(mi.RcWork.Left),
|
||||
Y: int(mi.RcWork.Top),
|
||||
Width: int(mi.RcWork.Right - mi.RcWork.Left),
|
||||
Height: int(mi.RcWork.Bottom - mi.RcWork.Top),
|
||||
}
|
||||
thisScreen.ID = strconv.Itoa(int(hMonitor))
|
||||
thisScreen.Name = string(utf16.Decode(mi.SzDevice[:]))
|
||||
var xdpi, ydpi w32.UINT
|
||||
w32.GetDPIForMonitor(hMonitor, w32.MDT_EFFECTIVE_DPI, &xdpi, &ydpi)
|
||||
thisScreen.Scale = float32(xdpi) / 96.0
|
||||
thisScreen.IsPrimary = mi.DwFlags&w32.MONITORINFOF_PRIMARY != 0
|
||||
|
||||
// TODO: Get screen rotation
|
||||
|
||||
return &thisScreen, nil
|
||||
}
|
||||
|
||||
// Get the screen for the current window
|
||||
func (w *windowsWebviewWindow) getScreen() (*Screen, error) {
|
||||
return getScreen(w.hwnd)
|
||||
return getScreenForWindow(w)
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setFrameless(b bool) {
|
||||
|
|
@ -1081,6 +1083,11 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
|
|||
mmi := (*w32.MINMAXINFO)(unsafe.Pointer(lparam))
|
||||
hasConstraints := false
|
||||
options := w.parent.options
|
||||
// Using ScreenManager to get the closest screen and scale according to its DPI is problematic
|
||||
// here because in multi-monitor setup, when dragging the window between monitors with the mouse
|
||||
// on the side with the higher DPI, the DPI change point is offset beyond the mid point, causing
|
||||
// wrong scaling and unwanted resizing when using the monitor DPI. To avoid this issue, we use
|
||||
// scaleWithWindowDPI() instead which retrieves the correct DPI with GetDpiForWindow().
|
||||
if options.MinWidth > 0 || options.MinHeight > 0 {
|
||||
hasConstraints = true
|
||||
|
||||
|
|
@ -1108,14 +1115,16 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
|
|||
}
|
||||
|
||||
case w32.WM_DPICHANGED:
|
||||
newWindowSize := (*w32.RECT)(unsafe.Pointer(lparam))
|
||||
w32.SetWindowPos(w.hwnd,
|
||||
uintptr(0),
|
||||
int(newWindowSize.Left),
|
||||
int(newWindowSize.Top),
|
||||
int(newWindowSize.Right-newWindowSize.Left),
|
||||
int(newWindowSize.Bottom-newWindowSize.Top),
|
||||
w32.SWP_NOZORDER|w32.SWP_NOACTIVATE)
|
||||
if !w.ignoreDPIChangeResizing {
|
||||
newWindowRect := (*w32.RECT)(unsafe.Pointer(lparam))
|
||||
w32.SetWindowPos(w.hwnd,
|
||||
uintptr(0),
|
||||
int(newWindowRect.Left),
|
||||
int(newWindowRect.Top),
|
||||
int(newWindowRect.Right-newWindowRect.Left),
|
||||
int(newWindowRect.Bottom-newWindowRect.Top),
|
||||
w32.SWP_NOZORDER|w32.SWP_NOACTIVATE)
|
||||
}
|
||||
w.parent.emit(events.Common.WindowDPIChanged)
|
||||
}
|
||||
|
||||
|
|
@ -1181,30 +1190,38 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
|
|||
// Make sure to use the provided RECT to get the monitor, because during maximizig there might be
|
||||
// a wrong monitor returned in multiscreen mode when using MonitorFromWindow.
|
||||
// See: https://github.com/MicrosoftEdge/WebView2Feedback/issues/2549
|
||||
monitor := w32.MonitorFromRect(rgrc, w32.MONITOR_DEFAULTTONULL)
|
||||
screen := ScreenNearestPhysicalRect(Rect{
|
||||
X: int(rgrc.Left),
|
||||
Y: int(rgrc.Top),
|
||||
Width: int(rgrc.Right - rgrc.Left),
|
||||
Height: int(rgrc.Bottom - rgrc.Top),
|
||||
})
|
||||
|
||||
var monitorInfo w32.MONITORINFO
|
||||
monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo))
|
||||
if monitor != 0 && w32.GetMonitorInfo(monitor, &monitorInfo) {
|
||||
*rgrc = monitorInfo.RcWork
|
||||
rect := screen.PhysicalWorkArea
|
||||
|
||||
maxWidth := options.MaxWidth
|
||||
maxHeight := options.MaxHeight
|
||||
if maxWidth > 0 || maxHeight > 0 {
|
||||
var dpiX, dpiY uint
|
||||
w32.GetDPIForMonitor(monitor, w32.MDT_EFFECTIVE_DPI, &dpiX, &dpiY)
|
||||
maxWidth := options.MaxWidth
|
||||
maxHeight := options.MaxHeight
|
||||
|
||||
maxWidth := int32(ScaleWithDPI(maxWidth, dpiX))
|
||||
if maxWidth > 0 && rgrc.Right-rgrc.Left > maxWidth {
|
||||
rgrc.Right = rgrc.Left + maxWidth
|
||||
}
|
||||
|
||||
maxHeight := int32(ScaleWithDPI(maxHeight, dpiY))
|
||||
if maxHeight > 0 && rgrc.Bottom-rgrc.Top > maxHeight {
|
||||
rgrc.Bottom = rgrc.Top + maxHeight
|
||||
}
|
||||
if maxWidth > 0 {
|
||||
maxWidth = screen.scale(maxWidth, false)
|
||||
if rect.Width > maxWidth {
|
||||
rect.Width = maxWidth
|
||||
}
|
||||
}
|
||||
|
||||
if maxHeight > 0 {
|
||||
maxHeight = screen.scale(maxHeight, false)
|
||||
if rect.Height > maxHeight {
|
||||
rect.Height = maxHeight
|
||||
}
|
||||
}
|
||||
|
||||
*rgrc = w32.RECT{
|
||||
Left: int32(rect.X),
|
||||
Top: int32(rect.Y),
|
||||
Right: int32(rect.X + rect.Width),
|
||||
Bottom: int32(rect.Y + rect.Height),
|
||||
}
|
||||
w.chromium.SetPadding(edge.Rect{})
|
||||
} else {
|
||||
// This is needed to workaround the resize flickering in frameless mode with WindowDecorations
|
||||
|
|
@ -1225,7 +1242,7 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
|
|||
|
||||
func (w *windowsWebviewWindow) DPI() (w32.UINT, w32.UINT) {
|
||||
if w32.HasGetDpiForWindowFunc() {
|
||||
// GetDpiForWindow is supported beginning with Windows 10, 1607 and is the most accureate
|
||||
// GetDpiForWindow is supported beginning with Windows 10, 1607 and is the most accurate
|
||||
// one, especially it is consistent with the WM_DPICHANGED event.
|
||||
dpi := w32.GetDpiForWindow(w.hwnd)
|
||||
return dpi, dpi
|
||||
|
|
@ -1258,12 +1275,8 @@ func (w *windowsWebviewWindow) scaleWithWindowDPI(width, height int) (int, int)
|
|||
return scaledWidth, scaledHeight
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) scaleToDefaultDPI(width, height int) (int, int) {
|
||||
dpix, dpiy := w.DPI()
|
||||
scaledWidth := ScaleToDefaultDPI(width, dpix)
|
||||
scaledHeight := ScaleToDefaultDPI(height, dpiy)
|
||||
|
||||
return scaledWidth, scaledHeight
|
||||
func ScaleWithDPI(pixels int, dpi uint) int {
|
||||
return (pixels * int(dpi)) / 96
|
||||
}
|
||||
|
||||
func (w *windowsWebviewWindow) setWindowMask(imageData []byte) {
|
||||
|
|
@ -1739,14 +1752,6 @@ func (w *windowsWebviewWindow) setMinimiseButtonEnabled(enabled bool) {
|
|||
w.setStyle(enabled, w32.WS_MINIMIZEBOX)
|
||||
}
|
||||
|
||||
func ScaleWithDPI(pixels int, dpi uint) int {
|
||||
return (pixels * int(dpi)) / 96
|
||||
}
|
||||
|
||||
func ScaleToDefaultDPI(pixels int, dpi uint) int {
|
||||
return (pixels * 96) / int(dpi)
|
||||
}
|
||||
|
||||
func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (w32.HICON, error) {
|
||||
var err error
|
||||
var result w32.HICON
|
||||
|
|
|
|||
|
|
@ -72,6 +72,23 @@ const (
|
|||
IMAGE_ENHMETAFILE = 3
|
||||
)
|
||||
|
||||
// SetProcessDpiAwareness constants
|
||||
const (
|
||||
PROCESS_DPI_UNAWARE = 0
|
||||
PROCESS_SYSTEM_DPI_AWARE = 1
|
||||
PROCESS_PER_MONITOR_DPI_AWARE = 2
|
||||
)
|
||||
|
||||
// SetProcessDpiAwarenessContext constants
|
||||
// Credit: https://github.com/ncruces/zenity
|
||||
const (
|
||||
DPI_AWARENESS_CONTEXT_UNAWARE = ^uintptr(1) + 1
|
||||
DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = ^uintptr(2) + 1
|
||||
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = ^uintptr(3) + 1
|
||||
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = ^uintptr(4) + 1
|
||||
DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED = ^uintptr(5) + 1
|
||||
)
|
||||
|
||||
// ShowWindow constants
|
||||
const (
|
||||
SW_HIDE = 0
|
||||
|
|
|
|||
|
|
@ -10,11 +10,12 @@ import (
|
|||
|
||||
type Screen struct {
|
||||
MONITORINFOEX
|
||||
Name string
|
||||
IsPrimary bool
|
||||
IsCurrent bool
|
||||
Scale float32
|
||||
Rotation float32
|
||||
HMonitor uintptr
|
||||
Name string
|
||||
IsPrimary bool
|
||||
IsCurrent bool
|
||||
ScaleFactor float32
|
||||
Rotation float32
|
||||
}
|
||||
|
||||
type DISPLAY_DEVICE struct {
|
||||
|
|
@ -71,8 +72,17 @@ func GetRotationForMonitor(displayName [32]uint16) (float32, error) {
|
|||
}
|
||||
|
||||
func GetAllScreens() ([]*Screen, error) {
|
||||
var monitorList []MONITORINFOEX
|
||||
var result []*Screen
|
||||
var errMessage string
|
||||
|
||||
// Get cursor position to determine the current monitor
|
||||
var cursor POINT
|
||||
ret, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&cursor)))
|
||||
if ret == 0 {
|
||||
return nil, fmt.Errorf("GetCursorPos failed")
|
||||
}
|
||||
|
||||
// Enumerate the monitors
|
||||
enumFunc := func(hMonitor uintptr, hdc uintptr, lprcMonitor *RECT, lParam uintptr) uintptr {
|
||||
monitor := MONITORINFOEX{
|
||||
MONITORINFO: MONITORINFO{
|
||||
|
|
@ -82,72 +92,51 @@ func GetAllScreens() ([]*Screen, error) {
|
|||
}
|
||||
ret, _, _ := procGetMonitorInfo.Call(hMonitor, uintptr(unsafe.Pointer(&monitor)))
|
||||
if ret == 0 {
|
||||
return 1 // Continue enumeration
|
||||
errMessage = "GetMonitorInfo failed"
|
||||
return 0 // Stop enumeration
|
||||
}
|
||||
|
||||
monitorList = append(monitorList, monitor)
|
||||
return 1 // Continue enumeration
|
||||
}
|
||||
|
||||
ret, _, _ := procEnumDisplayMonitors.Call(0, 0, syscall.NewCallback(enumFunc), 0)
|
||||
if ret == 0 {
|
||||
return nil, fmt.Errorf("EnumDisplayMonitors failed")
|
||||
}
|
||||
|
||||
// Get the active screen
|
||||
var pt POINT
|
||||
ret, _, _ = procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
|
||||
if ret == 0 {
|
||||
return nil, fmt.Errorf("GetCursorPos failed")
|
||||
}
|
||||
|
||||
hMonitor, _, _ := procMonitorFromPoint.Call(uintptr(unsafe.Pointer(&pt)), MONITOR_DEFAULTTONEAREST)
|
||||
if hMonitor == 0 {
|
||||
return nil, fmt.Errorf("MonitorFromPoint failed")
|
||||
}
|
||||
|
||||
var monitorInfo MONITORINFO
|
||||
monitorInfo.CbSize = uint32(unsafe.Sizeof(monitorInfo))
|
||||
ret, _, _ = procGetMonitorInfo.Call(hMonitor, uintptr(unsafe.Pointer(&monitorInfo)))
|
||||
if ret == 0 {
|
||||
return nil, fmt.Errorf("GetMonitorInfo failed")
|
||||
}
|
||||
|
||||
var result []*Screen
|
||||
|
||||
// Iterate through the screens and set the active one
|
||||
for _, monitor := range monitorList {
|
||||
thisContainer := &Screen{
|
||||
screen := &Screen{
|
||||
MONITORINFOEX: monitor,
|
||||
HMonitor: hMonitor,
|
||||
IsPrimary: monitor.DwFlags == MONITORINFOF_PRIMARY,
|
||||
IsCurrent: rectContainsPoint(monitor.RcMonitor, cursor),
|
||||
}
|
||||
thisContainer.IsCurrent = equalRect(monitor.RcMonitor, monitorInfo.RcMonitor)
|
||||
thisContainer.IsPrimary = monitor.DwFlags == MONITORINFOF_PRIMARY
|
||||
|
||||
// Get monitor name
|
||||
name, err := getMonitorName(syscall.UTF16ToString(monitor.SzDevice[:]))
|
||||
if err != nil {
|
||||
name = ""
|
||||
if err == nil {
|
||||
screen.Name = name
|
||||
}
|
||||
|
||||
// Get DPI for monitor
|
||||
var dpiX, dpiY uint
|
||||
ret = GetDPIForMonitor(hMonitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY)
|
||||
if ret != S_OK {
|
||||
return nil, fmt.Errorf("GetDpiForMonitor failed")
|
||||
errMessage = "GetDpiForMonitor failed"
|
||||
return 0 // Stop enumeration
|
||||
}
|
||||
// Convert to float32
|
||||
thisContainer.Scale = float32(dpiX) / 96.0
|
||||
// Convert to scale factor
|
||||
screen.ScaleFactor = float32(dpiX) / 96.0
|
||||
|
||||
// Get rotation of monitor
|
||||
rot, err := GetRotationForMonitor(monitor.SzDevice)
|
||||
if err != nil {
|
||||
rot = 0
|
||||
if err == nil {
|
||||
screen.Rotation = rot
|
||||
}
|
||||
thisContainer.Rotation = rot
|
||||
thisContainer.Name = name
|
||||
result = append(result, thisContainer)
|
||||
|
||||
result = append(result, screen)
|
||||
return 1 // Continue enumeration
|
||||
}
|
||||
|
||||
ret, _, _ = procEnumDisplayMonitors.Call(0, 0, syscall.NewCallback(enumFunc), 0)
|
||||
if ret == 0 {
|
||||
return nil, fmt.Errorf("EnumDisplayMonitors failed: %s", errMessage)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func equalRect(a RECT, b RECT) bool {
|
||||
return a.Left == b.Left && a.Top == b.Top && a.Right == b.Right && a.Bottom == b.Bottom
|
||||
func rectContainsPoint(r RECT, p POINT) bool {
|
||||
return p.X >= r.Left && p.X < r.Right && p.Y >= r.Top && p.Y < r.Bottom
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
package w32
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
|
@ -10,9 +11,23 @@ import (
|
|||
var (
|
||||
modshcore = syscall.NewLazyDLL("shcore.dll")
|
||||
|
||||
procGetDpiForMonitor = modshcore.NewProc("GetDpiForMonitor")
|
||||
procGetDpiForMonitor = modshcore.NewProc("GetDpiForMonitor")
|
||||
procSetProcessDpiAwareness = modshcore.NewProc("SetProcessDpiAwareness")
|
||||
)
|
||||
|
||||
func HasSetProcessDpiAwarenessFunc() bool {
|
||||
err := procSetProcessDpiAwareness.Find()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func SetProcessDpiAwareness(val uint) error {
|
||||
status, r, err := procSetProcessDpiAwareness.Call(uintptr(val))
|
||||
if status != S_OK {
|
||||
return fmt.Errorf("procSetProcessDpiAwareness failed %d: %v %v", status, r, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func HasGetDPIForMonitorFunc() bool {
|
||||
err := procGetDpiForMonitor.Find()
|
||||
return err == nil
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ package w32
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/sys/windows"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// From MSDN: Windows Data Types
|
||||
|
|
@ -678,6 +679,7 @@ type NOTIFYICONDATA struct {
|
|||
}
|
||||
|
||||
const SPI_GETNOTIFYWINDOWRECT = 0x0040
|
||||
const SPI_SETWORKAREA = 0x002F
|
||||
|
||||
// Taskbar constants
|
||||
const ABM_GETTASKBARPOS = 0x00000005
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ package w32
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/sys/windows"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -136,6 +137,7 @@ var (
|
|||
procGetDpiForSystem = moduser32.NewProc("GetDpiForSystem")
|
||||
procGetDpiForWindow = moduser32.NewProc("GetDpiForWindow")
|
||||
procSetProcessDPIAware = moduser32.NewProc("SetProcessDPIAware")
|
||||
procSetProcessDpiAwarenessContext = moduser32.NewProc("SetProcessDpiAwarenessContext")
|
||||
procEnumDisplayMonitors = moduser32.NewProc("EnumDisplayMonitors")
|
||||
procEnumDisplayDevices = moduser32.NewProc("EnumDisplayDevicesW")
|
||||
procEnumDisplaySettings = moduser32.NewProc("EnumDisplaySettingsW")
|
||||
|
|
@ -360,6 +362,11 @@ func GetDpiForWindow(hwnd HWND) UINT {
|
|||
return uint(dpi)
|
||||
}
|
||||
|
||||
func HasSetProcessDPIAwareFunc() bool {
|
||||
err := procSetProcessDPIAware.Find()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func GetClassName(hwnd HWND) string {
|
||||
var buf [256]uint16
|
||||
procGetClassName.Call(
|
||||
|
|
@ -378,6 +385,19 @@ func SetProcessDPIAware() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func HasSetProcessDpiAwarenessContextFunc() bool {
|
||||
err := procSetProcessDpiAwarenessContext.Find()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func SetProcessDpiAwarenessContext(ctx uintptr) error {
|
||||
status, r, err := procSetProcessDpiAwarenessContext.Call(ctx)
|
||||
if status == 0 {
|
||||
return fmt.Errorf("SetProcessDpiAwarenessContext failed %d: %v %v", status, r, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetForegroundWindow() HWND {
|
||||
ret, _, _ := procGetForegroundWindow.Call()
|
||||
return HWND(ret)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue