1616from importlib .resources import files as resource_files
1717
1818import anywidget
19- import numpy as np
20- from IPython .display import display
19+ from IPython .display import display , HTML
2120from traitlets import Bool , Dict , Int , Unicode
2221
23- from ._utils import array2compressed , RFBOutputContext , Snapshot
22+ from ._utils import array2compressed , RFBOutputContext
2423
2524
2625def _load_js_and_css ():
@@ -105,7 +104,7 @@ def __init__(self, *args, **kwargs):
105104 display (self ._output_context )
106105 # Init attributes for drawing
107106 self ._rfb_last_frame = None
108- self ._rfb_pending_display = None
107+ self ._rfb_pending_snapshot_display = None
109108 self ._rfb_draw_requested = False
110109 self ._rfb_frame_index = 0
111110 self ._rfb_last_confirmed_index = 0
@@ -122,21 +121,6 @@ def __init__(self, *args, **kwargs):
122121 names = ["_frame_feedback" , "_has_visible_views" ],
123122 )
124123
125- def _repr_mimebundle_ (self , ** kwargs ):
126- # Use default
127- result = anywidget .AnyWidget ._repr_mimebundle_ (self , ** kwargs )
128- # Get dict to add more data
129- data = None
130- if isinstance (result , tuple ):
131- data = result [0 ]
132- elif isinstance (result , dict ):
133- data = result
134- # Add initial snapshot if we have it
135- if data and self ._rfb_pending_display and self ._rfb_last_frame is not None :
136- data ["text/html" ] = self .snapshot ()._repr_html_ ()
137- self ._rfb_pending_display = None # no need to reload
138- return result
139-
140124 def print (self , * args , ** kwargs ):
141125 """Print to the widget's output area (for debugging purposes).
142126
@@ -155,7 +139,7 @@ def close(self, *args, **kwargs):
155139 # When the widget is closed, we notify by creating a close event. The
156140 # same event is emitted from JS when the model is closed in the client.
157141 anywidget .AnyWidget .close (self , * args , ** kwargs )
158- self ._rfb_handle_msg (self , {"type" : "close" , "event_type" : "close" }, [])
142+ self ._rfb_handle_msg (self , {"type" : "close" }, [])
159143
160144 def _rfb_handle_msg (self , widget , content , buffers ):
161145 """Receive custom messages and filter our events."""
@@ -193,43 +177,60 @@ def _rfb_handle_msg(self, widget, content, buffers):
193177
194178 # ---- drawing
195179
196- def display (self ):
197- """Display the widget.
198-
199- The benefit of using this (instead of using the widget as the last expression of a cell)
200- is that an html snapshot is added to the output. This happens either directly,
201- or on the next drawn frame (if no frames have been drawn yet).
202- """
203- self ._rfb_pending_display = True
204- id = display (self , display_id = True )
205- if self ._rfb_pending_display : # i.e. is not set to None by __repr__
206- self ._rfb_pending_display = id
207-
208180 def snapshot (self , pixel_ratio = None ):
209- """Create a snapshot of the current state of the widget .
181+ """Render a frame and include the resulting image in the output .
210182
211- Returns an ``IPython DisplayObject`` that can simply be used as
212- a cell output. The display object has a ``data`` attribute that holds
213- the image array data (typically a numpy array).
183+ An initial placeholder output is produced, which is replaced by an html
184+ ``<img>`` as soon as the next frame is rendered.
214185
215- The ``pixel_ratio`` argument is deprecated and ignored.
186+ If the widget is not displayed yet, a resize event is emitted to mimic a widget
187+ size. This happens at most once in the widget's lifetime. It will use the
188+ ``css_width`` and ``css_height`` if they are expressed in ``px``, and otherwise
189+ default to 500 or 300 pixels respectively. The ``pixel_ratio`` argument is then
190+ used to calculate the physical size.
216191 """
217- # Get the current size
218- ref_resize_event = self ._rfb_last_resize_event
219- if ref_resize_event :
220- w = ref_resize_event ["width" ]
221- h = ref_resize_event ["height" ]
222- else :
192+ if self ._rfb_last_resize_event is None :
223193 css_width , css_height = self .css_width , self .css_height
224194 w = float (css_width [:- 2 ]) if css_width .endswith ("px" ) else 500
225195 h = float (css_height [:- 2 ]) if css_height .endswith ("px" ) else 300
226- # Get last frame or single-pixel image
227- array = self ._rfb_last_frame
228- if array is None :
229- array = np .ones ((1 , 1 , 3 ), np .uint8 ) * 127
230- # Super-weird, but it looks like nbsphinx only selects the text/html field when we use a css class
231- # that starts with 'snapshot-'. Is this some upstream hack to make jupyter-rfb work, that we don't know of?
232- return Snapshot (array , w , h , "snapshot" , f"snapshot-rfb model{ self ._model_id } " )
196+ r = float (pixel_ratio ) if pixel_ratio is not None else 1.0
197+ pw , ph = int (w * r ), (h * r )
198+ event = {
199+ "type" : "resize" ,
200+ "width" : pw / r ,
201+ "height" : ph / r ,
202+ "pwidth" : pw ,
203+ "pheight" : ph ,
204+ "ratio" : r ,
205+ "timestamp" : 0 ,
206+ }
207+ self ._rfb_handle_msg (self , event , [])
208+
209+ self ._rfb_pending_snapshot_display = display (
210+ HTML (
211+ "<div style='display: inline-block; padding: 5px; border-radius: 5px; background:#ddd; color:#000'>pending screenshot ...</span>"
212+ ),
213+ display_id = True ,
214+ )
215+ self .request_draw ()
216+
217+ # Note: It could be that _replace_snapshot() is called directly,
218+ # (and _rfb_pending_snapshot_display set to None). But it could
219+ # also be that it is called later. In any case, we just return None
220+
221+ def _replace_snapshot (self , array ):
222+ pending_display = self ._rfb_pending_snapshot_display
223+ self ._rfb_pending_snapshot_display = None
224+
225+ event = self ._rfb_last_resize_event or {}
226+ w = event .get ("width" , array .shape [1 ])
227+ h = event .get ("height" , array .shape [0 ])
228+
229+ mimetype , data = array2compressed (array , 70 )
230+ src = f"data:image/{ mimetype } ;base64," + encodebytes (data ).decode ()
231+ html = f"<img src='{ src } ' style='width:{ w } px;height:{ h } px;' />"
232+
233+ pending_display .update (HTML (html ))
233234
234235 def request_draw (self ):
235236 """Schedule a new draw. This method itself returns immediately.
@@ -279,7 +280,7 @@ def _rfb_maybe_draw(self):
279280 should_draw = (
280281 self ._rfb_draw_requested
281282 and frames_in_flight < self .max_buffered_frames
282- and self ._has_visible_views
283+ and ( self ._has_visible_views or self . _rfb_pending_snapshot_display )
283284 )
284285 # Do the draw if we should.
285286 if should_draw :
@@ -359,9 +360,8 @@ def _rfb_send_frame(self, array, is_lossless_redraw=False):
359360 self ._rfb_last_confirmed_index = self ._rfb_frame_index - 1
360361
361362 # Reload the output if we did not have a frame when the widget was first loaded
362- if self ._rfb_pending_display is not None :
363- if self ._rfb_last_resize_event is not None :
364- self ._rfb_pending_display .update (self ) # -> calls _repr_mimebundle_
363+ if self ._rfb_pending_snapshot_display is not None :
364+ self ._replace_snapshot (array )
365365
366366 # Compose message and send
367367 msg = dict (
0 commit comments