Last active
July 10, 2023 02:44
-
-
Save ibrahemesam/715fb80876226eba1977dbc98217bd96 to your computer and use it in GitHub Desktop.
pywebview: Resizable Frameless Window
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# NB: requirements: $ pip install websocket-server mouse | |
# NB: I used WebSocket instead of default js_api's BottleServer | |
# to reduce CPU usage by sending short msgs instead of full http requests ( with http headers ) | |
import webview | |
__import__('logging').getLogger('pywebview').disabled = True # disable logger | |
from websocket_server import WebsocketServer | |
from mouse import get_position as mouse_get_position | |
FixPoint = webview.window.FixPoint | |
FixPoint_NORTH = FixPoint.NORTH | |
FixPoint_SOUTH = FixPoint.SOUTH | |
FixPoint_EAST = FixPoint.EAST | |
FixPoint_WEST = FixPoint.WEST | |
FixPoint_NORTH_EAST = FixPoint.NORTH | FixPoint.EAST | |
FixPoint_NORTH_WEST = FixPoint.NORTH | FixPoint.WEST | |
FixPoint_SOUTH_EAST = FixPoint.SOUTH | FixPoint.EAST | |
FixPoint_SOUTH_WEST = FixPoint.SOUTH | FixPoint.WEST | |
INFINITY = float('inf') | |
class ResizeableFramlessWindow: | |
js = r""" | |
((ws_url, GRID_SIZE = 10, GRID_COLOR = 'transparent') => { | |
/* connect to python ws backend */ | |
var ws = new WebSocket(ws_url); | |
/* setup elements */ | |
var css = document.createElement('style'); | |
css.id = "pywebview-resize-style" | |
css.innerHTML = ` | |
:fullscreen .pywebview-resize-grid { | |
display: none; | |
} | |
body:has(.pywebview-resize-grid.active) .pywebview-resize-grid:not(.active) { | |
display: none; | |
} | |
`; | |
document.body.appendChild(css); | |
var eastGrid = document.createElement('div') | |
eastGrid.className = 'pywebview-resize-grid'; | |
eastGrid.style = ` | |
position: fixed; | |
top: ${GRID_SIZE}px; | |
bottom: ${GRID_SIZE}px; | |
height: calc(100vh - ${GRID_SIZE*2}px); | |
right: 0; | |
width: ${GRID_SIZE}px; | |
background-color: ${GRID_COLOR}; | |
cursor: e-resize; | |
` | |
document.body.appendChild(eastGrid) | |
var westGrid = document.createElement('div') | |
westGrid.className = 'pywebview-resize-grid'; | |
westGrid.style = ` | |
position: fixed; | |
top: ${GRID_SIZE}px; | |
bottom: ${GRID_SIZE}px; | |
height: calc(100vh - ${GRID_SIZE*2}px); | |
left: 0; | |
width: ${GRID_SIZE}px; | |
background-color: ${GRID_COLOR}; | |
cursor: w-resize; | |
` | |
document.body.appendChild(westGrid) | |
var northGrid = document.createElement('div') | |
northGrid.className = 'pywebview-resize-grid'; | |
northGrid.style = ` | |
position: fixed; | |
right: ${GRID_SIZE}px; | |
left: ${GRID_SIZE}px; | |
width: calc(100vw - ${GRID_SIZE*2}px); | |
top: 0; | |
height: ${GRID_SIZE}px; | |
background-color: ${GRID_COLOR}; | |
cursor: n-resize; | |
` | |
document.body.appendChild(northGrid) | |
var southGrid = document.createElement('div') | |
southGrid.className = 'pywebview-resize-grid'; | |
southGrid.style = ` | |
position: fixed; | |
right: ${GRID_SIZE}px; | |
left: ${GRID_SIZE}px; | |
width: calc(100vw - ${GRID_SIZE*2}px); | |
bottom: 0; | |
height: ${GRID_SIZE}px; | |
background-color: ${GRID_COLOR}; | |
cursor: s-resize; | |
` | |
document.body.appendChild(southGrid) | |
var neGrid = document.createElement('div') | |
neGrid.className = 'pywebview-resize-grid'; | |
neGrid.style = ` | |
position: fixed; | |
top: 0; | |
height: ${GRID_SIZE}px; | |
right: 0; | |
width: ${GRID_SIZE}px; | |
background-color: ${GRID_COLOR}; | |
cursor: ne-resize; | |
` | |
document.body.appendChild(neGrid) | |
var nwGrid = document.createElement('div') | |
nwGrid.className = 'pywebview-resize-grid'; | |
nwGrid.style = ` | |
position: fixed; | |
top: 0; | |
left: 0; | |
height: ${GRID_SIZE}px; | |
width: ${GRID_SIZE}px; | |
background-color: ${GRID_COLOR}; | |
cursor: nw-resize; | |
` | |
document.body.appendChild(nwGrid) | |
var seGrid = document.createElement('div') | |
seGrid.className = 'pywebview-resize-grid'; | |
seGrid.style = ` | |
position: fixed; | |
right: 0; | |
bottom: 0; | |
height: ${GRID_SIZE}px; | |
width: ${GRID_SIZE}px; | |
background-color: ${GRID_COLOR}; | |
cursor: se-resize; | |
` | |
document.body.appendChild(seGrid) | |
var swGrid = document.createElement('div') | |
swGrid.className = 'pywebview-resize-grid'; | |
swGrid.style = ` | |
position: fixed; | |
left: 0; | |
bottom: 0; | |
height: ${GRID_SIZE}px; | |
width: ${GRID_SIZE}px; | |
background-color: ${GRID_COLOR}; | |
cursor: sw-resize; | |
` | |
document.body.appendChild(swGrid) | |
/* setup grid events */ | |
var onmousemove = () => ws.send('m'); | |
window.addEventListener("pointerup", (evt) => { | |
var activeGrid = document.querySelector('div.pywebview-resize-grid.active') | |
activeGrid.classList.toggle('active'); | |
activeGrid.releasePointerCapture(evt.pointerId); | |
window.removeEventListener('mousemove', onmousemove); | |
}); | |
nwGrid.addEventListener("pointerdown", (evt)=>{ | |
nwGrid.classList.toggle('active'); | |
nwGrid.setPointerCapture(evt.pointerId); | |
ws.send(0) | |
window.addEventListener('mousemove', onmousemove); | |
}); | |
northGrid.addEventListener("pointerdown", (evt)=>{ | |
northGrid.classList.toggle('active'); | |
northGrid.setPointerCapture(evt.pointerId); | |
ws.send(1) | |
window.addEventListener('mousemove', onmousemove); | |
}); | |
neGrid.addEventListener("pointerdown", (evt)=>{ | |
neGrid.classList.toggle('active'); | |
neGrid.setPointerCapture(evt.pointerId); | |
ws.send(2) | |
window.addEventListener('mousemove', onmousemove); | |
}); | |
westGrid.addEventListener("pointerdown", (evt)=>{ | |
westGrid.classList.toggle('active'); | |
westGrid.setPointerCapture(evt.pointerId); | |
ws.send(3) | |
window.addEventListener('mousemove', onmousemove); | |
}); | |
eastGrid.addEventListener("pointerdown", (evt)=>{ | |
eastGrid.classList.toggle('active'); | |
eastGrid.setPointerCapture(evt.pointerId); | |
ws.send(4) | |
window.addEventListener('mousemove', onmousemove); | |
}); | |
swGrid.addEventListener("pointerdown", (evt)=>{ | |
swGrid.classList.toggle('active'); | |
swGrid.setPointerCapture(evt.pointerId); | |
ws.send(5) | |
window.addEventListener('mousemove', onmousemove); | |
}); | |
southGrid.addEventListener("pointerdown", (evt)=>{ | |
southGrid.classList.toggle('active'); | |
southGrid.setPointerCapture(evt.pointerId); | |
ws.send(6) | |
window.addEventListener('mousemove', onmousemove); | |
}); | |
seGrid.addEventListener("pointerdown", (evt)=>{ | |
seGrid.classList.toggle('active'); | |
seGrid.setPointerCapture(evt.pointerId); | |
ws.send(7) | |
window.addEventListener('mousemove', onmousemove); | |
}); | |
})""" | |
def __init__( | |
self, window, min_width=200, max_width=INFINITY, min_height=100, max_height=INFINITY, | |
# WebsocketServer args | |
host='localhost', port=0, | |
# NB: key & cert are required for SSL | |
# which is required get JS to connect to WebsocketServer | |
# when webview's URL is https | |
key=None, cert=None, | |
): | |
self.min_width = min_width | |
self.max_width = max_width | |
self.min_height = min_height | |
self.max_height = max_height | |
self.window = window | |
self.window_resize = window.resize | |
self.last_mouse_pos = () | |
self.server = WebsocketServer(host=host, port=port, key=key, cert=cert) | |
self.server.set_fn_new_client(lambda c, s: self.server.deny_new_connections()) | |
self.server.set_fn_message_received(self.message_received) | |
self.server.run_forever(threaded=True) | |
self.server_host = host | |
if key and cert: | |
self.server_protocol = 'wss://' | |
else: | |
self.server_protocol = 'ws://' | |
window.events.loaded += self.on_window_load | |
def on_window_load(self): | |
window = self.window | |
window.events.loaded -= self.on_window_load | |
window.events.maximized += self.go_fullscreen | |
window.__original__toggle_fullscreen = window.toggle_fullscreen | |
window.toggle_fullscreen = self.toggle_fullscreen | |
self.width = window.width; | |
self.height = window.height; | |
self.resize_methods = ( | |
self.nw_resize, self.n_resize, self.ne_resize, | |
self.w_resize, self.e_resize, | |
self.sw_resize, self.s_resize, self.se_resize, | |
) | |
window.evaluate_js(f'{self.js}(ws_url="{self.server_protocol}{self.server_host}:{self.server.port}");') | |
def go_fullscreen(self): | |
self.window.evaluate_js('document.body.requestFullscreen();') | |
def close_fullscreen(self): | |
self.window.evaluate_js('document.exitFullscreen();') | |
def toggle_fullscreen(self): | |
self.window.evaluate_js( | |
'document.fullscreenElement ? document.exitFullscreen() : document.body.requestFullscreen();' | |
) | |
self.window.__original__toggle_fullscreen() | |
def message_received(self, client, server, msg): | |
if msg == 'm': | |
self.resize() | |
return | |
self.last_mouse_pos = mouse_get_position() | |
self.resize = self.resize_methods[int(msg)] | |
def e_resize(self): # east resize | |
mouse_x, mouse_y = mouse_get_position() | |
self.width += mouse_x - self.last_mouse_pos[0] | |
if self.check_width(self.width): | |
self.window_resize(self.width, self.height, FixPoint_WEST) | |
self.last_mouse_pos = (mouse_x, mouse_y) | |
def w_resize(self): # west resize | |
mouse_x, mouse_y = mouse_get_position() | |
self.width -= mouse_x - self.last_mouse_pos[0] | |
if self.check_width(self.width): | |
self.window_resize(self.width, self.height, FixPoint_EAST) | |
self.last_mouse_pos = (mouse_x, mouse_y) | |
def n_resize(self): # north resize | |
mouse_x, mouse_y = mouse_get_position() | |
self.height -= mouse_y - self.last_mouse_pos[1] | |
if self.check_height(self.height): | |
self.window_resize(self.width, self.height, FixPoint_SOUTH) | |
self.last_mouse_pos = (mouse_x, mouse_y) | |
def s_resize(self): # south resize | |
mouse_x, mouse_y = mouse_get_position() | |
self.height += mouse_y - self.last_mouse_pos[1] | |
if self.check_height(self.height): | |
self.window_resize(self.width, self.height, FixPoint_NORTH) | |
self.last_mouse_pos = (mouse_x, mouse_y) | |
def ne_resize(self): # north-east resize | |
mouse_x, mouse_y = mouse_get_position() | |
self.width += mouse_x - self.last_mouse_pos[0] | |
self.height -= mouse_y - self.last_mouse_pos[1] | |
cw = self.check_width(self.width) | |
ch = self.check_height(self.height) | |
if cw and ch: | |
self.window_resize(self.width, self.height, FixPoint_SOUTH_WEST) | |
self.last_mouse_pos = (mouse_x, mouse_y) | |
elif cw: | |
self.window_resize(self.width, self.height, FixPoint_WEST) | |
self.last_mouse_pos = (mouse_x, self.last_mouse_pos[1]) | |
elif ch: | |
self.window_resize(self.width, self.height, FixPoint_SOUTH) | |
self.last_mouse_pos = (self.last_mouse_pos[0], mouse_y) | |
def nw_resize(self): # north-west resize | |
mouse_x, mouse_y = mouse_get_position() | |
self.width -= mouse_x - self.last_mouse_pos[0] | |
self.height -= mouse_y - self.last_mouse_pos[1] | |
cw = self.check_width(self.width) | |
ch = self.check_height(self.height) | |
if cw and ch: | |
self.window_resize(self.width, self.height, FixPoint_SOUTH_EAST) | |
self.last_mouse_pos = (mouse_x, mouse_y) | |
elif cw: | |
self.window_resize(self.width, self.height, FixPoint_EAST) | |
self.last_mouse_pos = (mouse_x, self.last_mouse_pos[1]) | |
elif ch: | |
self.window_resize(self.width, self.height, FixPoint_SOUTH) | |
self.last_mouse_pos = (self.last_mouse_pos[0], mouse_y) | |
def se_resize(self): # south-east resize | |
mouse_x, mouse_y = mouse_get_position() | |
self.width += mouse_x - self.last_mouse_pos[0] | |
self.height += mouse_y - self.last_mouse_pos[1] | |
cw = self.check_width(self.width) | |
ch = self.check_height(self.height) | |
if cw and ch: | |
self.window_resize(self.width, self.height, FixPoint_NORTH_WEST) | |
self.last_mouse_pos = (mouse_x, mouse_y) | |
elif cw: | |
self.window_resize(self.width, self.height, FixPoint_WEST) | |
self.last_mouse_pos = (mouse_x, self.last_mouse_pos[1]) | |
elif ch: | |
self.window_resize(self.width, self.height, FixPoint_NORTH) | |
self.last_mouse_pos = (self.last_mouse_pos[0], mouse_y) | |
def sw_resize(self): # south-west resize | |
mouse_x, mouse_y = mouse_get_position() | |
self.width -= mouse_x - self.last_mouse_pos[0] | |
self.height += mouse_y - self.last_mouse_pos[1] | |
cw = self.check_width(self.width) | |
ch = self.check_height(self.height) | |
if cw and ch: | |
self.window_resize(self.width, self.height, FixPoint_NORTH_EAST) | |
self.last_mouse_pos = (mouse_x, mouse_y) | |
elif cw: | |
self.window_resize(self.width, self.height, FixPoint_EAST) | |
self.last_mouse_pos = (mouse_x, self.last_mouse_pos[1]) | |
elif ch: | |
self.window_resize(self.width, self.height, FixPoint_NORTH) | |
self.last_mouse_pos = (self.last_mouse_pos[0], mouse_y) | |
def check_width(self, width): | |
if self.min_width <= width <= self.max_width: | |
return True | |
else: | |
self.width = self.window.width | |
return False | |
def check_height(self, height): | |
if self.min_height <= height <= self.max_height: | |
return True | |
else: | |
self.height = self.window.height | |
return False | |
@staticmethod | |
def patch_webview_module(): | |
import webview | |
webview.__original__create_window = webview.create_window | |
def _create_window(**kwargs): # NB: function must be called with only kwargs | |
kwargs_keys = kwargs.keys() | |
if 'frameless' in kwargs_keys: | |
if kwargs['frameless']: | |
if not 'resizable' in kwargs_keys: | |
kwargs['resizable'] = True # default value is True | |
if not 'min_size' in kwargs_keys: | |
kwargs['min_size'] = (200, 100) # default value is (200, 100) | |
if kwargs['resizable']: | |
kwargs['easy_drag'] = False # easy_drag must be off | |
win = webview.__original__create_window(**kwargs) | |
min_width, min_height = kwargs['min_size'] | |
win.__ResizeableFramlessWindow_instance = ResizeableFramlessWindow( | |
win, min_width=min_width, min_height=min_height, | |
) | |
return win | |
return webview.__original__create_window(**kwargs) | |
webview.create_window = _create_window | |
ResizeableFramlessWindow.patch_webview_module() | |
win = webview.create_window(title='', url="about:blank", frameless=True) | |
try: | |
webview.start() | |
except KeyboardInterrupt: | |
print('', end='\r') | |
exit() | |
# TODO: | |
# on maximized => hide grids | |
# on un-maximized => show grids | |
# NB: these functions are not implemented yet in pywebview |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment