Skip to content

Commit

Permalink
Fix hang and crispier look (#418)
Browse files Browse the repository at this point in the history
* This print seems to cause hanging when resizing on FF.

* Crispier look by aligning the canvas pixels to screen

* Better determination of canvas size, scrispier graphics

* Guards to prevent tiny or negative-area widgets and daemon while loops

* Include test function for pixel snapping

* minor tweaks

* delay processing a resize

* Few more tweaks

* add comments
  • Loading branch information
almarklein authored Nov 7, 2023
1 parent ec075d5 commit dcfb0bc
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 49 deletions.
61 changes: 50 additions & 11 deletions timetagger/app/front.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ def set_width_mode():
entries = []
add = lambda x: entries.push(x)
content_div.classList.forEach(add)
print(entries)
for entry in entries:
if "width-" in entry:
content_div.classList.remove(entry)
Expand Down Expand Up @@ -240,7 +239,7 @@ def on_resize(self):

x0 = 0
x1 = margin
x2 = x1 + records_width
x2 = x1 + max(0, records_width)
x3 = x2 + margin2
x4 = self.w - margin
x5 = self.w # noqa
Expand All @@ -252,7 +251,7 @@ def on_resize(self):

y0 = 0
y1 = self.grid_round(140)
y3 = self.grid_round(self.h - 15)
y3 = self.grid_round(max(y1 + 40, self.h - 15))

self.widgets["TopWidget"].rect = x0, y0, x5, y1
self.widgets["RecordsWidget"].rect = x1, y1, x2, y3
Expand Down Expand Up @@ -599,7 +598,7 @@ def get_ticks(self, npixels):
# Define ticks
ticks = []
minor_ticks = []
maxi = 2 * npixels / min_distance
maxi = min(2 * npixels / min_distance, 99)
t = dt.floor(t1 - 0.1 * nsecs, delta)
iter = -1
while iter < maxi: # just to be safe
Expand All @@ -614,7 +613,9 @@ def get_ticks(self, npixels):
t_new = dt.add(t, delta)
# Minor ticks
t_minor = dt.add(t, minor_delta)
while (t_new - t_minor) > 0:
iter_minor = -1
while iter_minor < 20 and (t_new - t_minor) > 0:
iter_minor += 1
pix_pos = (t_minor - t1) * npixels / nsecs
minor_ticks.push((pix_pos, t_minor))
t_minor_new = dt.add(t_minor, minor_delta)
Expand All @@ -636,7 +637,9 @@ def get_ticks(self, npixels):
# Add minor ticks at duplicate hour
d_minor = dt.add(t_new, minor_delta) - t_new
t_minor = tb + d_minor
while t_minor < tc:
iter_minor = -1
while iter_minor < 20 and t_minor < tc:
iter_minor += 1
pix_pos = (t_minor - t1) * npixels / nsecs + 3
minor_ticks.push((pix_pos, t_minor))
t_minor += d_minor
Expand Down Expand Up @@ -905,7 +908,11 @@ def on_init(self):
self._button_pressed = None
self._current_scale = {}
self._sync_feedback_xy = 0, 0, 0
window.setInterval(self._draw_sync_feedback_callback, 100)

# Periodically draw the sync feedback icon. Make sure to do it via requestAnimationFrame
window.setInterval(
window.requestAnimationFrame, 100, self._draw_sync_feedback_callback
)

# For navigation with keys. Listen to canvas events, and window events (in
# case canvas does not have focus), but don't listen for events from dialogs.
Expand All @@ -916,6 +923,10 @@ def on_draw(self, ctx, menu_only=False):
self._picker.clear()
x1, y1, x2, y2 = self.rect

# Guard for small screen space during resize
if x2 - x1 < 50 or y2 - y1 < 20:
return

y4 = y2 # noqa - bottom
y2 = y1 + 60
y3 = y2 + 20
Expand Down Expand Up @@ -1149,18 +1160,18 @@ def _draw_menu_button(self, ctx, x1, y1, x2, y2):

def _draw_sync_feedback(self, ctx, x1, y1, radius):
self._sync_feedback_xy = x1, y1, radius
return self._draw_sync_feedback_work()
return self._draw_sync_feedback_work(ctx)

def _draw_sync_feedback_callback(self):
self._draw_sync_feedback_work(False)
ctx = self._canvas.node.getContext("2d")
self._draw_sync_feedback_work(ctx, False)

def _draw_sync_feedback_work(self, register=True):
def _draw_sync_feedback_work(self, ctx, register=True):
PSCRIPT_OVERLOAD = False # noqa

if window.document.hidden:
return

ctx = self._canvas.node.getContext("2d")
x, y, radius = self._sync_feedback_xy

state = window.store.state
Expand Down Expand Up @@ -1620,6 +1631,10 @@ def on_draw(self, ctx):
x1, y1, x2, y2 = self.rect
self._picker.clear()

# Guard for small screen space during resize
if y2 - y1 < 20:
return

# If too little space, only draw button to expand
if x2 - x1 <= 50:
width = 30
Expand Down Expand Up @@ -3001,6 +3016,13 @@ def on_init(self):

def on_draw(self, ctx):
x1, y1, x2, y2 = self.rect

# Guard for small screen space during resize
if y2 - y1 < 20:
return

# return self._draw_test_grid()

self._picker.clear()

# If too little space, only draw button to expand
Expand Down Expand Up @@ -3060,6 +3082,23 @@ def on_draw(self, ctx):
# ctx.fillStyle = COLORS.prim2_clr
# ctx.fillText(self._help_text, x2 - 10, 90)

def _draw_test_grid(self, ctx):
x1, y1, x2, y2 = self.rect

x1, x2, x3 = int(x1), int((x1 + x2) / 2), int(x2)
y1, y2, y3 = int(y1), int((y1 + y2) / 2), int(y2)

ctx.strokeStyle = "#000"
ctx.lineWidth = 2
for i in range((x2 - x1) / 4):
ctx.moveTo(x1 + i * 4, y1)
ctx.lineTo(x1 + i * 4, y3)
ctx.stroke()
for i in range((y2 - y1) / 4):
ctx.moveTo(x1, y2 + i * 4)
ctx.lineTo(x3, y2 + i * 4)
ctx.stroke()

def _draw_stats(self, ctx, x1, y1, x2, y2):
PSCRIPT_OVERLOAD = False # noqa

Expand Down
90 changes: 52 additions & 38 deletions timetagger/app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ def fit_font_size(ctx, available_width, font, text, maxsize=100):
# Takes 2-5 iters, smaller available_width -> faster iteration
size = maxsize
width = available_width + 2
while width > available_width and size > 4:
iter = 0 # failsafe
while iter < 9 and width > available_width and size > 4:
iter += 1
new_size = int(1.1 * size * available_width / width)
size = new_size if new_size < size else size - 1
ctx.font = str(size) + "px " + font
Expand Down Expand Up @@ -460,20 +462,10 @@ def positions_mean_and_std(positions):
return avg_pos, std_pos


def get_pixel_ratio(ctx):
def get_pixel_ratio():
"""Get the ratio of logical pixel to screen pixel."""
PSCRIPT_OVERLOAD = False # noqa

dpr = window.devicePixelRatio or 1
bsr = (
ctx.webkitBackingStorePixelRatio
or ctx.mozBackingStorePixelRatio
or ctx.msBackingStorePixelRatio
or ctx.oBackingStorePixelRatio
or ctx.backingStorePixelRatio
or 1
)
return dpr / bsr
return window.devicePixelRatio or 1


def create_pointer_event(node, e):
Expand Down Expand Up @@ -777,8 +769,9 @@ def _init_events(self):
self.node.addEventListener("touchmove", self._on_js_touch_event, 0)

# Keep track of window size
window.addEventListener("resize", self._on_js_resize_event, False)
window.setTimeout(self._on_js_resize_event, 10)
# window.addEventListener("resize", self._on_js_resize_event, False)
self._resize_observer = window.ResizeObserver(self._on_resize_observer)
self._resize_observer.observe(self.node, {"box": "device-pixel-content-box"})

def _prevent_default_event(self, e):
"""Prevent the default action of an event unless all modifier
Expand Down Expand Up @@ -839,21 +832,39 @@ def _on_js_touch_event(self, e):
}.get(e.type[5:])
self.on_pointer(ev)

def _on_js_resize_event(self):
"""Ensure that the canvas has the correct size and dpi."""
ctx = self.node.getContext("2d")
self.pixel_ratio = get_pixel_ratio(ctx)

# A line-width of 2 is great to have crisp images. For uneven line widths
# one needs to offset 0.5 * pixel_ratio. But, that line-width must be
# snapped to a width matching the pixel_ratio! pfew!
def _on_resize_observer(self, entries):
# The resize observer allows us to get the extact size of the element
# in physical pixels, something we cannot do with a normal resize event.
# See https://web.dev/articles/device-pixel-content-box
#
# On Firefox I've seen the app hang due to window resizing. I have not
# been able to find a singular reason, but its important that we update
# the canvas physical size (i.e. call _apply_new_size) directly. I also
# found that drawing stuff outside a requested animation frame is
# dangerous. See https://github.com/almarklein/timetagger/pull/418
entry = entries.find(lambda entry: entry.target is self.node)
psize = [
entry.devicePixelContentBoxSize[0].inlineSize,
entry.devicePixelContentBoxSize[0].blockSize,
]
self._apply_new_size(psize)
self._draw() # draw directly to prevent flicker (and maybe even hanging)

def _apply_new_size(self, psize):
# This is called JIT right before a draw, when a resize has happened

# Get and store pixel ratio
self.pixel_ratio = ratio = get_pixel_ratio()
# Use that to obtain the real number of logical pixels
lsize = psize[0] / ratio, psize[1] / ratio
# Apply
self.node.width = psize[0]
self.node.height = psize[1]
self.w, self.h = lsize[0], lsize[1]
# Calculate linewidth param
self.grid_linewidth2 = min(self.pixel_ratio, self.grid_round(1)) * 2

self.w, self.h = self.node.clientWidth, self.node.clientHeight
self.node.width = self.w * self.pixel_ratio
self.node.height = self.h * self.pixel_ratio
self.update()
self.on_resize(True) # draw asap to avoid flicker
# Update
self.on_resize()

def grid_round(self, x):
"""Round a value to the screen pixel grid."""
Expand All @@ -873,8 +884,8 @@ def _draw_tick(self):

def update(self, asap=True):
"""Schedule an update."""
# The extra setTimeout is to make sure that there is time for the
# browser to process events (like scrolling).
# The optional extra setTimeout is to make sure that there is
# time for the browser to process events (like scrolling).
if not self._pending_draw:
self._pending_draw = True
if asap:
Expand All @@ -884,22 +895,25 @@ def update(self, asap=True):

def _draw(self):
"""The entry draw function, called by the browser."""

# Reset flag
self._pending_draw = False

# Is the element even visible
if self.node.style.display == "none":
return # Hidden
elif self.w <= 0 or self.h <= 0:
if self.w <= 0 or self.h <= 0:
return # Probably still initializing

ctx = self.node.getContext("2d")
# Some bookkeeping
self._tooltips.clear()

# Prepare hidpi mode for canvas (flush state just in case)
for i in range(4):
ctx.restore()
# Prepare context
ctx = self.node.getContext("2d")
ctx.restore() # undo last
ctx.save()
ctx.scale(self.pixel_ratio, self.pixel_ratio)

self._tooltips.clear()

# Draw
self.on_draw(ctx)

Expand Down

0 comments on commit dcfb0bc

Please sign in to comment.