diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index c564d3de..6d4ebc83 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -18,6 +18,7 @@ High level chart-widget apis. ''' +from __future__ import annotations from typing import Optional from PyQt5 import QtCore, QtWidgets @@ -68,7 +69,7 @@ log = get_logger(__name__) class GodWidget(QWidget): ''' "Our lord and savior, the holy child of window-shua, there is no - widget above thee." - 6||6 + widget above thee." - 6|6 The highest level composed widget which contains layouts for organizing charts as well as other sub-widgets used to control or @@ -104,8 +105,8 @@ class GodWidget(QWidget): # self.init_strategy_ui() # self.vbox.addLayout(self.hbox) - self._chart_cache = {} - self.linkedsplits: 'LinkedSplits' = None + self._chart_cache: dict[str, LinkedSplits] = {} + self.linkedsplits: Optional[LinkedSplits] = None # assigned in the startup func `_async_main()` self._root_n: trio.Nursery = None @@ -135,7 +136,7 @@ class GodWidget(QWidget): def set_chart_symbol( self, symbol_key: str, # of form . - linkedsplits: 'LinkedSplits', # type: ignore + linkedsplits: LinkedSplits, # type: ignore ) -> None: # re-sort org cache symbol list in LIFO order @@ -146,20 +147,20 @@ class GodWidget(QWidget): def get_chart_symbol( self, symbol_key: str, - ) -> 'LinkedSplits': # type: ignore + + ) -> LinkedSplits: # type: ignore return self._chart_cache.get(symbol_key) async def load_symbol( self, - providername: str, symbol_key: str, loglevel: str, - reset: bool = False, ) -> trio.Event: - '''Load a new contract into the charting app. + ''' + Load a new contract into the charting app. Expects a ``numpy`` structured array containing all the ohlcv fields. @@ -178,6 +179,7 @@ class GodWidget(QWidget): # XXX: this is CRITICAL especially with pixel buffer caching self.linkedsplits.hide() + self.linkedsplits.unfocus() # XXX: pretty sure we don't need this # remove any existing plots? @@ -202,6 +204,11 @@ class GodWidget(QWidget): ) self.set_chart_symbol(fqsn, linkedsplits) + self.vbox.addWidget(linkedsplits) + + linkedsplits.show() + linkedsplits.focus() + await trio.sleep(0) else: # symbol is already loaded and ems ready @@ -215,21 +222,17 @@ class GodWidget(QWidget): # also switch it over to the new chart's interal-layout # self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_pane) chart = linkedsplits.chart - await chart.resume_all_feeds() - # chart is already in memory so just focus it - if self.linkedsplits: - self.linkedsplits.unfocus() + # chart is already in memory so just focus it + linkedsplits.show() + linkedsplits.focus() + await trio.sleep(0) - self.vbox.addWidget(linkedsplits) - - linkedsplits.show() - linkedsplits.focus() + # resume feeds *after* rendering chart view asap + chart.resume_all_feeds() self.linkedsplits = linkedsplits - symbol = linkedsplits.symbol - if symbol is not None: self.window.setWindowTitle( f'{symbol.key}@{symbol.brokers} ' @@ -239,7 +242,8 @@ class GodWidget(QWidget): return order_mode_started def focus(self) -> None: - '''Focus the top level widget which in turn focusses the chart + ''' + Focus the top level widget which in turn focusses the chart ala "view mode". ''' @@ -247,9 +251,19 @@ class GodWidget(QWidget): self.clearFocus() self.linkedsplits.chart.setFocus() + def resizeEvent(self, event: QtCore.QEvent) -> None: + ''' + Top level god widget resize handler. + + Where we do UX magic to make things not suck B) + + ''' + log.debug('god widget resize') + class ChartnPane(QFrame): - '''One-off ``QFrame`` composite which pairs a chart + ''' + One-off ``QFrame`` composite which pairs a chart + sidepane (often a ``FieldsForm`` + other widgets if provided) forming a, sort of, "chart row" with a side panel for configuration and display of off-chart data. @@ -280,8 +294,6 @@ class ChartnPane(QFrame): hbox.setContentsMargins(0, 0, 0, 0) hbox.setSpacing(3) - # self.setMaximumWidth() - class LinkedSplits(QWidget): ''' @@ -349,7 +361,7 @@ class LinkedSplits(QWidget): if not prop: # proportion allocated to consumer subcharts if ln < 2: - prop = 1/(.666 * 6) + prop = 1/3 elif ln >= 2: prop = 3/8 @@ -379,16 +391,21 @@ class LinkedSplits(QWidget): style: str = 'bar', ) -> 'ChartPlotWidget': - """Start up and show main (price) chart and all linked subcharts. + '''Start up and show main (price) chart and all linked subcharts. The data input struct array must include OHLC fields. - """ + + ''' # add crosshairs self.cursor = Cursor( linkedsplits=self, digits=symbol.tick_size_digits, ) + # NOTE: atm the first (and only) OHLC price chart for the symbol + # is given a special reference but in the future there shouldn't + # be no distinction since we will have multiple symbols per + # view as part of "aggregate feeds". self.chart = self.add_plot( name=symbol.key, @@ -430,9 +447,7 @@ class LinkedSplits(QWidget): **cpw_kwargs, ) -> 'ChartPlotWidget': - '''Add (sub)plots to chart widget by name. - - If ``name`` == ``"main"`` the chart will be the the primary view. + '''Add (sub)plots to chart widget by key. ''' if self.chart is None and not _is_main: @@ -457,7 +472,10 @@ class LinkedSplits(QWidget): self.xaxis.hide() self.xaxis = xaxis - qframe = ChartnPane(sidepane=sidepane, parent=self.splitter) + qframe = ChartnPane( + sidepane=sidepane, + parent=self.splitter, + ) cpw = ChartPlotWidget( # this name will be used to register the primary @@ -551,17 +569,23 @@ class LinkedSplits(QWidget): else: assert style == 'bar', 'main chart must be OHLC' + self.resize_sidepanes() return cpw def resize_sidepanes( self, ) -> None: - '''Size all sidepanes based on the OHLC "main" plot. + ''' + Size all sidepanes based on the OHLC "main" plot and its + sidepane width. ''' - for name, cpw in self.subplots.items(): - cpw.sidepane.setMinimumWidth(self.chart.sidepane.width()) - cpw.sidepane.setMaximumWidth(self.chart.sidepane.width()) + main_chart = self.chart + if main_chart: + sp_w = main_chart.sidepane.width() + for name, cpw in self.subplots.items(): + cpw.sidepane.setMinimumWidth(sp_w) + cpw.sidepane.setMaximumWidth(sp_w) class ChartPlotWidget(pg.PlotWidget): @@ -600,12 +624,18 @@ class ChartPlotWidget(pg.PlotWidget): view_color: str = 'papas_special', pen_color: str = 'bracket', + # TODO: load from config + use_open_gl: bool = False, + static_yrange: Optional[tuple[float, float]] = None, **kwargs, ): - """Configure chart display settings. - """ + ''' + Configure initial display settings and connect view callback + handlers. + + ''' self.view_color = view_color self.pen_color = pen_color @@ -614,9 +644,9 @@ class ChartPlotWidget(pg.PlotWidget): # parent=None, # plotItem=None, # antialias=True, - # useOpenGL=True, **kwargs ) + self.useOpenGL(use_open_gl) self.name = name self.data_key = data_key self.linked = linkedsplits @@ -665,13 +695,13 @@ class ChartPlotWidget(pg.PlotWidget): # for when the splitter(s) are resized self._vb.sigResized.connect(self._set_yrange) - async def resume_all_feeds(self): + def resume_all_feeds(self): for feed in self._feeds.values(): - await feed.resume() + self.linked.godwidget._root_n.start_soon(feed.resume) - async def pause_all_feeds(self): + def pause_all_feeds(self): for feed in self._feeds.values(): - await feed.pause() + self.linked.godwidget._root_n.start_soon(feed.pause) @property def view(self) -> ChartView: @@ -910,51 +940,55 @@ class ChartPlotWidget(pg.PlotWidget): def update_ohlc_from_array( self, - name: str, + + graphics_name: str, array: np.ndarray, **kwargs, - ) -> pg.GraphicsObject: - """Update the named internal graphics from ``array``. - """ + ) -> pg.GraphicsObject: + '''Update the named internal graphics from ``array``. + + ''' self._arrays['ohlc'] = array - graphics = self._graphics[name] + graphics = self._graphics[graphics_name] graphics.update_from_array(array, **kwargs) return graphics def update_curve_from_array( self, - name: str, + graphics_name: str, array: np.ndarray, array_key: Optional[str] = None, - **kwargs, ) -> pg.GraphicsObject: - """Update the named internal graphics from ``array``. + '''Update the named internal graphics from ``array``. - """ + ''' + assert len(array) + data_key = array_key or graphics_name - data_key = array_key or name - if name not in self._overlays: + if graphics_name not in self._overlays: self._arrays['ohlc'] = array else: self._arrays[data_key] = array - curve = self._graphics[name] + curve = self._graphics[graphics_name] - if len(array): - # TODO: we should instead implement a diff based - # "only update with new items" on the pg.PlotCurveItem - # one place to dig around this might be the `QBackingStore` - # https://doc.qt.io/qt-5/qbackingstore.html - # curve.setData(y=array[name], x=array['index'], **kwargs) - curve.update_from_array( - x=array['index'], - y=array[data_key], - **kwargs - ) + # NOTE: back when we weren't implementing the curve graphics + # ourselves you'd have updates using this method: + # curve.setData(y=array[graphics_name], x=array['index'], **kwargs) + + # NOTE: graphics **must** implement a diff based update + # operation where an internal ``FastUpdateCurve._xrange`` is + # used to determine if the underlying path needs to be + # pre/ap-pended. + curve.update_from_array( + x=array['index'], + y=array[data_key], + **kwargs + ) return curve @@ -964,7 +998,11 @@ class ChartPlotWidget(pg.PlotWidget): yrange: Optional[tuple[float, float]] = None, range_margin: float = 0.06, - bars_range: Optional[tuple[int, int, int, int]] = None + bars_range: Optional[tuple[int, int, int, int]] = None, + + # flag to prevent triggering sibling charts from the same linked + # set from recursion errors. + autoscale_linked_plots: bool = True, ) -> None: '''Set the viewable y-range based on embedded data. @@ -991,50 +1029,33 @@ class ChartPlotWidget(pg.PlotWidget): l, lbar, rbar, r = bars_range or self.bars_range() - # TODO: we need a loop for auto-scaled subplots to all - # be triggered by one another - if self.name != 'volume': - vlm_chart = self.linked.subplots.get('volume') - if vlm_chart: - vlm_chart._set_yrange(bars_range=(l, lbar, rbar, r)) - # curve = vlm_chart._graphics['volume'] - # if rbar - lbar < 1500: - # # print('small range') - # curve._fill = True - # else: - # curve._fill = False + if autoscale_linked_plots: + # avoid recursion by sibling plots + linked = self.linked + plots = list(linked.subplots.copy().values()) + main = linked.chart + if main: + plots.append(main) - # figure out x-range in view such that user can scroll "off" - # the data set up to the point where ``_min_points_to_show`` - # are left. - # view_len = r - l + for chart in plots: + if chart and not chart._static_yrange: + chart._set_yrange( + bars_range=(l, lbar, rbar, r), + autoscale_linked_plots=False, + ) # TODO: logic to check if end of bars in view # extra = view_len - _min_points_to_show - # begin = self._arrays['ohlc'][0]['index'] - extra - # # end = len(self._arrays['ohlc']) - 1 + extra # end = self._arrays['ohlc'][-1]['index'] - 1 + extra - # XXX: test code for only rendering lines for the bars in view. - # This turns out to be very very poor perf when scaling out to - # many bars (think > 1k) on screen. - # name = self.name - # bars = self._graphics[self.name] - # bars.draw_lines( - # istart=max(lbar, l), iend=min(rbar, r), just_history=True) - # bars_len = rbar - lbar # log.debug( # f"\nl: {l}, lbar: {lbar}, rbar: {rbar}, r: {r}\n" # f"view_len: {view_len}, bars_len: {bars_len}\n" # f"begin: {begin}, end: {end}, extra: {extra}" # ) - # self._set_xlimits(begin, end) - - # TODO: this should be some kind of numpy view api - # bars = self._arrays['ohlc'][lbar:rbar] a = self._arrays['ohlc'] ifirst = a[0]['index'] diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 43085b7d..4c4aed1f 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -36,7 +36,7 @@ import trio from .. import brokers from .._cacheables import maybe_open_context -from ..trionics import async_enter_all +from tractor.trionics import gather_contexts from ..data.feed import open_feed, Feed from ._chart import ( ChartPlotWidget, @@ -61,7 +61,10 @@ log = get_logger(__name__) _quote_throttle_rate: int = 58 # Hz -def try_read(array: np.ndarray) -> Optional[np.ndarray]: +def try_read( + array: np.ndarray + +) -> Optional[np.ndarray]: ''' Try to read the last row from a shared mem array or ``None`` if the array read returns a zero-length array result. @@ -85,10 +88,9 @@ def try_read(array: np.ndarray) -> Optional[np.ndarray]: # something we need anyway, maybe there should be some kind of # signal that a prepend is taking place and this consumer can # respond (eg. redrawing graphics) accordingly. - log.warning(f'Read-race on shm array: {graphics_name}@{shm.token}') - # the array read was emtpy - return None + # the array read was emtpy + return None def update_fsp_chart( @@ -101,8 +103,10 @@ def update_fsp_chart( array = shm.array last_row = try_read(array) + # guard against unreadable case if not last_row: + log.warning(f'Read-race on shm array: {graphics_name}@{shm.token}') return # update graphics @@ -175,7 +179,6 @@ def chart_maxmin( async def update_chart_from_quotes( - linked: LinkedSplits, stream: tractor.MsgStream, ohlcv: np.ndarray, @@ -247,24 +250,23 @@ async def update_chart_from_quotes( chart.show() last_quote = time.time() - # NOTE: all code below this loop is expected to be synchronous - # and thus draw instructions are not picked up jntil the next - # wait / iteration. async for quotes in stream: now = time.time() - quote_period = now - last_quote - quote_rate = round(1/quote_period, 1) if quote_period else float('inf') + quote_period = time.time() - last_quote + quote_rate = round( + 1/quote_period, 1) if quote_period > 0 else float('inf') + if ( quote_period <= 1/_quote_throttle_rate - and quote_rate > _quote_throttle_rate + 2 + and quote_rate > _quote_throttle_rate * 1.5 ): log.warning(f'High quote rate {symbol.key}: {quote_rate}') last_quote = now # chart isn't active/shown so skip render cycle and pause feed(s) if chart.linked.isHidden(): - await chart.pause_all_feeds() + chart.pause_all_feeds() continue for sym, quote in quotes.items(): @@ -454,7 +456,8 @@ def maybe_mk_fsp_shm( readonly: bool = True, ) -> (ShmArray, bool): - '''Allocate a single row shm array for an symbol-fsp pair if none + ''' + Allocate a single row shm array for an symbol-fsp pair if none exists, otherwise load the shm already existing for that token. ''' @@ -481,7 +484,6 @@ def maybe_mk_fsp_shm( @acm async def open_fsp_sidepane( - linked: LinkedSplits, conf: dict[str, dict[str, str]], @@ -570,6 +572,7 @@ async def open_fsp_cluster( async def maybe_open_fsp_cluster( workers: int = 2, **kwargs, + ) -> AsyncGenerator[int, dict[str, tractor.Portal]]: kwargs.update( @@ -589,7 +592,6 @@ async def maybe_open_fsp_cluster( async def start_fsp_displays( - cluster_map: dict[str, tractor.Portal], linkedsplits: LinkedSplits, fsps: dict[str, str], @@ -599,11 +601,9 @@ async def start_fsp_displays( group_status_key: str, loglevel: str, - # this con - display_in_own_task: bool = False, - ) -> None: - '''Create sub-actors (under flat tree) + ''' + Create sub-actors (under flat tree) for each entry in config and attach to local graphics update tasks. Pass target entrypoint and historical data. @@ -623,7 +623,7 @@ async def start_fsp_displays( for (display_name, conf), (name, portal) in zip( fsps.items(), - # rr to cluster for now.. + # round robin to cluster for now.. cycle(cluster_map.items()), ): func_name = conf['func_name'] @@ -668,9 +668,7 @@ async def start_fsp_displays( async def update_chart_from_fsp( - portal: tractor.Portal, - linkedsplits: LinkedSplits, brokermod: ModuleType, sym: str, @@ -687,7 +685,8 @@ async def update_chart_from_fsp( profiler: pg.debug.Profiler, ) -> None: - '''FSP stream chart update loop. + ''' + FSP stream chart update loop. This is called once for each entry in the fsp config map. @@ -792,9 +791,7 @@ async def update_chart_from_fsp( level_line(chart, 80, orient_v='top') chart._set_yrange() - - done() - chart.linked.resize_sidepanes() + done() # status updates profiler(f'fsp:{func_name} starting update loop') profiler.finish() @@ -912,7 +909,6 @@ def has_vlm(ohlcv: ShmArray) -> bool: @acm async def maybe_open_vlm_display( - linked: LinkedSplits, ohlcv: ShmArray, @@ -926,16 +922,14 @@ async def maybe_open_vlm_display( shm, opened = maybe_mk_fsp_shm( linked.symbol.key, - '$_vlm', + 'vlm', readonly=True, ) async with open_fsp_sidepane( linked, { 'vlm': { - 'params': { - 'price_func': { 'default_value': 'chl3', # tell target ``Edit`` widget to not allow @@ -963,9 +957,6 @@ async def maybe_open_vlm_display( # we do this internally ourselves since # the curve item internals are pretty convoluted. style='step', - - # original pyqtgraph flag for reference - # stepMode=True, ) # XXX: ONLY for sub-chart fsps, overlays have their @@ -992,25 +983,14 @@ async def maybe_open_vlm_display( # size view to data once at outset chart._set_yrange() - # size pain to parent chart - # TODO: this appears to nearly fix a bug where the vlm sidepane - # could be sized correctly nearly immediately (since the - # order pane is already sized), right now it doesn't seem to - # fully align until the VWAP fsp-actor comes up... - await trio.sleep(0) - chart.linked.resize_sidepanes() - await trio.sleep(0) - yield chart async def display_symbol_data( - godwidget: GodWidget, provider: str, sym: str, loglevel: str, - order_mode_started: trio.Event, ) -> None: @@ -1037,8 +1017,7 @@ async def display_symbol_data( # group_key=loading_sym_key, # ) - async with async_enter_all( - open_feed( + async with open_feed( provider, [sym], loglevel=loglevel, @@ -1046,11 +1025,8 @@ async def display_symbol_data( # limit to at least display's FPS # avoiding needless Qt-in-guest-mode context switches tick_throttle=_quote_throttle_rate, - ), - maybe_open_fsp_cluster(), - - ) as (feed, cluster_map): + ) as feed: ohlcv: ShmArray = feed.shm bars = ohlcv.array symbol = feed.symbols[sym] @@ -1102,38 +1078,38 @@ async def display_symbol_data( # TODO: eventually we'll support some kind of n-compose syntax fsp_conf = { - 'dolla_vlm': { - 'func_name': 'dolla_vlm', - 'zero_on_step': True, - 'params': { - 'price_func': { - 'default_value': 'chl3', - # tell target ``Edit`` widget to not allow - # edits for now. - 'widget_kwargs': {'readonly': True}, - }, - }, - 'chart_kwargs': {'style': 'step'} - }, + # 'dolla_vlm': { + # 'func_name': 'dolla_vlm', + # 'zero_on_step': True, + # 'params': { + # 'price_func': { + # 'default_value': 'chl3', + # # tell target ``Edit`` widget to not allow + # # edits for now. + # 'widget_kwargs': {'readonly': True}, + # }, + # }, + # 'chart_kwargs': {'style': 'step'} + # }, - 'rsi': { - 'func_name': 'rsi', # literal python func ref lookup name + # 'rsi': { + # 'func_name': 'rsi', # literal python func ref lookup name - # map of parameters to place on the fsp sidepane widget - # which should map to dynamic inputs available to the - # fsp function at runtime. - 'params': { - 'period': { - 'default_value': 14, - 'widget_kwargs': {'readonly': True}, - }, - }, + # # map of parameters to place on the fsp sidepane widget + # # which should map to dynamic inputs available to the + # # fsp function at runtime. + # 'params': { + # 'period': { + # 'default_value': 14, + # 'widget_kwargs': {'readonly': True}, + # }, + # }, - # ``ChartPlotWidget`` options passthrough - 'chart_kwargs': { - 'static_yrange': (0, 100), - }, - }, + # # ``ChartPlotWidget`` options passthrough + # 'chart_kwargs': { + # 'static_yrange': (0, 100), + # }, + # }, } if has_vlm(ohlcv): # and provider != 'binance': @@ -1158,10 +1134,15 @@ async def display_symbol_data( await trio.sleep(0) vlm_chart = None - async with ( - trio.open_nursery() as ln, - maybe_open_vlm_display(linkedsplits, ohlcv) as vlm_chart, - ): + + async with gather_contexts( + ( + trio.open_nursery(), + maybe_open_vlm_display(linkedsplits, ohlcv), + maybe_open_fsp_cluster(), + ) + ) as (ln, vlm_chart, cluster_map): + # load initial fsp chain (otherwise known as "indicators") ln.start_soon( start_fsp_displays, @@ -1175,7 +1156,7 @@ async def display_symbol_data( loglevel, ) - # start graphics update loop(s)after receiving first live quote + # start graphics update loop after receiving first live quote ln.start_soon( update_chart_from_quotes, linkedsplits, @@ -1201,4 +1182,10 @@ async def display_symbol_data( order_mode_started ) ): + # let Qt run to render all widgets and make sure the + # sidepanes line up vertically. + await trio.sleep(0) + linkedsplits.resize_sidepanes() + + # let the app run. await trio.sleep_forever() diff --git a/piker/ui/_forms.py b/piker/ui/_forms.py index 45d68317..72053716 100644 --- a/piker/ui/_forms.py +++ b/piker/ui/_forms.py @@ -732,7 +732,7 @@ def mk_order_pane_layout( ) -> FieldsForm: - font_size: int = _font.px_size - 1 + font_size: int = _font.px_size - 2 # TODO: maybe just allocate the whole fields form here # and expect an async ctx entry? diff --git a/piker/ui/_interaction.py b/piker/ui/_interaction.py index 97222065..9f33253d 100644 --- a/piker/ui/_interaction.py +++ b/piker/ui/_interaction.py @@ -341,7 +341,14 @@ class ChartView(ViewBox): **kwargs, ): - super().__init__(parent=parent, **kwargs) + super().__init__( + parent=parent, + # TODO: look into the default view padding + # support that might replace somem of our + # ``ChartPlotWidget._set_yrange()` + # defaultPadding=0., + **kwargs + ) # disable vertical scrolling self.setMouseEnabled(x=True, y=False) @@ -533,7 +540,6 @@ class ChartView(ViewBox): # self.updateScaleBox(ev.buttonDownPos(), ev.pos()) else: # default bevavior: click to pan view - tr = self.childGroup.transform() tr = fn.invertQTransform(tr) tr = tr.map(dif*mask) - tr.map(Point(0, 0)) diff --git a/piker/ui/_style.py b/piker/ui/_style.py index 34b5cb01..1b63e08f 100644 --- a/piker/ui/_style.py +++ b/piker/ui/_style.py @@ -110,7 +110,7 @@ class DpiAwareFont: mx_dpi = max(pdpi, ldpi) mn_dpi = min(pdpi, ldpi) - scale = round(ldpi/pdpi) + scale = round(ldpi/pdpi, ndigits=2) if mx_dpi <= 97: # for low dpi use larger font sizes inches = _font_sizes['lo'][self._font_size] @@ -121,17 +121,29 @@ class DpiAwareFont: dpi = mn_dpi # dpi is likely somewhat scaled down so use slightly larger font size - if scale > 1 and self._font_size: - # TODO: this denominator should probably be determined from + if scale >= 1.1 and self._font_size: + + # no idea why + if 1.2 <= scale: + mult = 1.0375 + + if scale >= 1.5: + mult = 1.375 + + # TODO: this multiplier should probably be determined from # relative aspect ratios or something? - inches = inches * (1 / scale) * (1 + 6/16) - dpi = mx_dpi + inches *= mult + # TODO: we might want to fiddle with incrementing font size by + # +1 for the edge cases above. it seems doing it via scaling is + # always going to hit that error in range mapping from inches: + # float to px size: int. self._font_inches = inches - font_size = math.floor(inches * dpi) - log.debug( - f"\nscreen:{screen.name()} with pDPI: {pdpi}, lDPI: {ldpi}" + + log.info( + f"screen:{screen.name()}]\n" + f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n" f"\nOur best guess font size is {font_size}\n" ) # apply the size