Skip to content

Instantly share code, notes, and snippets.

@ibrahemesam
Last active July 10, 2023 02:44
Show Gist options
  • Save ibrahemesam/715fb80876226eba1977dbc98217bd96 to your computer and use it in GitHub Desktop.
Save ibrahemesam/715fb80876226eba1977dbc98217bd96 to your computer and use it in GitHub Desktop.
pywebview: Resizable Frameless Window
# 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