PreviousUpNext

15.4.1485  src/lib/x-kit/widget/old/fancy/graphviz/graphviz-widget.pkg

## graphviz-widget.pkg

# Compiled by:
#     src/lib/x-kit/widget/xkit-widget.sublib

# This package gets used in:
#     src/lib/x-kit/widget/old/fancy/graphviz/scrollable-graphviz-widget.pkg

stipulate
    include package   threadkit;                                        # threadkit                     is from   src/lib/src/lib/thread-kit/src/core-thread-kit/threadkit.pkg
    #
    package fil =  file__premicrothread;                                # file__premicrothread          is from   src/lib/std/src/posix/file--premicrothread.pkg
    package f8b =  eight_byte_float;                                    # eight_byte_float              is from   src/lib/std/eight-byte-float.pkg
    package g2d =  geometry2d;                                          # geometry2d                    is from   src/lib/std/2d/geometry2d.pkg
    package gf  =  geometry2d_float;                                    # geometry2d_float              is from   src/lib/std/2d/geometry2d-float.pkg
    package bs  =  beta2_spline;                                        # beta2_spline                  is from   src/lib/x-kit/draw/beta2-spline.pkg
    #
    package xc  =  xclient;                                             # xclient                       is from   src/lib/x-kit/xclient/xclient.pkg
    #
    package wg  =  widget;                                              # widget                        is from   src/lib/x-kit/widget/old/basic/widget.pkg
    package gm  =  get_mouse_selection;                                 # get_mouse_selection           is from   src/lib/x-kit/widget/old/fancy/graphviz/get-mouse-selection.pkg
    package pu  =  popup_menu;                                          # popup_menu                    is from   src/lib/x-kit/widget/old/menu/popup-menu.pkg
    package pg  =  planar_graphtree;                                    # planar_graphtree              is from   src/lib/std/dot/planar-graphtree.pkg
    package ffc =  font_family_cache;                                   # font_family_cache             is from   src/lib/x-kit/widget/old/fancy/graphviz/font-family-cache.pkg
    #
    package xtr =  xlogger;                                             # xlogger                       is from   src/lib/x-kit/xclient/src/stuff/xlogger.pkg
herein


    package   graphviz_widget
    : (weak)  Graphviz_Widget                                           # Graphviz_Widget               is from   src/lib/x-kit/widget/old/fancy/graphviz/graphviz-widget.api
    {
        exception ERROR(String);

        truncate =  f8b::truncate;

        Viewdim
            =
            VIEWDIM
              { min:    Int,
                size:   Int,
                total:  Int
              };

        Viewnode
            =
            { node:   pg::Node,
              bbox:   g2d::Box,                                         # "bbox" == "bounding box"
              draw:  (xc::Drawable, xc::Pen) -> g2d::Box -> Void,
              fill:  (xc::Drawable, xc::Pen) -> g2d::Box -> Void,
              label:  String
            };

        Graph_To_Window_Space_Coordinate_Transform =  gf::Point ->  g2d::Point;
        Window_To_Graph_Space_Coordinate_Transform =  g2d::Point ->  gf::Point;

        Plea_Mail
            = SET_VERTICAL_VIEW   Int
            | SET_HORIZONTAL_VIEW Int
            | DELETE
            ;

        Graphviz_Widget
            =
            GRAPHVIZ_WIDGET
              { widget:                 wg::Widget,
                graph:                  pg::Traitful_Graph,
                plea_slot:              Mailslot( Plea_Mail ),
                #
                to_scrollbars_slot:     Mailslot  { horizontal:  Viewdim,
                                                    vertical:    Viewdim
                                                  }
              };

        View_Data
            =
            { graph_to_window_space:            Graph_To_Window_Space_Coordinate_Transform,             #  Graph -> Window   space coordinate transformation function.
              window_to_graph_space:            Window_To_Graph_Space_Coordinate_Transform,             #  Window -> Graph   space coordinate transformation function.
              # 
              visible_nodes:    List( Viewnode ),       # List of visible nodes. 
              font:             Null_Or( xc::Font ),    # Font scaled to view. 
              picked_node:      Null_Or( pg::Node )     # Picked node. 
            };

        fun map_box_from_window_to_graph_space (projfn:  Window_To_Graph_Space_Coordinate_Transform)  box
            =
            {   (projfn (g2d::box::upperleft   box)) ->  { x=>ul_x, y=>ul_y };                  # "ul" == "upper left"
                (projfn (g2d::box::lowerright1 box)) ->  { x=>lr_x, y=>lr_y };                  # "lr" == "lower right"

                gf::BOX { x => ul_x,
                          y => ul_y,
                          #
                          wide => lr_x - ul_x,
                          high => lr_y - ul_y
                        };
            };

        fun map_box_from_graph_to_window_space (projfn:  Graph_To_Window_Space_Coordinate_Transform)  box
            =
            {   (projfn ( gf::upperleft_of_box box)) ->  { col=>ul_x, row=>ul_y };              # "ul" == "upper left"
                (projfn (gf::lowerright_of_box box)) ->  { col=>lr_x, row=>lr_y };              # "lr" == "lower right"

                { col  => ul_x,
                  row  => ul_y,
                  #
                  wide => lr_x - ul_x,
                  high => lr_y - ul_y
                };
            };

        # Curried fn to write text in given window,
        # centered in the given box, and using largest
        # font so that text fits in the box.
        #
        fun put_text (window, THE font, black_pen)              # + below '\\' args.
                =>
                {   (xc::font_high  font)
                        ->
                        { ascent, descent };

                    font_high = ascent + descent;

                    draw_text =  xc::draw_transparent_string
                                   (xc::drawable_of_window  window)
                                   black_pen
                                   font;

                    \\ (text, { col, row, wide, high } )
                        =
                        {   slen = xc::text_width  font  text;

                            col = col +          (wide - slen     ) / 2;
                            row = row + ascent + (high - font_high) / 2;

                            draw_text ({ col, row }, text);
                        };
                };

            put_text (_, NULL, _)
                =>
                (\\ _ = ());
        end;


        # Generate the draw-outline and draw-filled-shape
        # functions for an ellipse, diamond or box:
        # 
        stipulate
            fun diamond_of_box ({ col, row, wide, high } )
                = 
                {   midx = col + wide / 2;
                    midy = row + high / 2;

                    startp = { col, row=>midy };

                    [ startp,
                      { col => midx,       row             },
                      { col => col + wide, row => midy     },
                      { col => midx,       row => row+high },
                      startp
                    ];
                };

            full_angle = 360 * 64;

            fun draw_ellipse drawfn (drawable, pen) ({ col, row, wide, high } )
                =
                drawfn drawable pen ({ col, row, wide, high, angle1=>0, angle2=>full_angle } );


            fun draw_diamond (drawable, pen) box
                =
                xc::draw_lines  drawable  pen  (diamond_of_box  box);


            fun fill_diamond (drawable, pen) box
                =
                xc::fill_polygon
                    drawable
                    pen
                    { verts =>  diamond_of_box  box,
                      shape =>  xc::CONVEX_SHAPE
                    };


            fun draw_box drawfn (drawable, pen) box
                =
                drawfn drawable pen box;
        herein
            fun get_draw_fns dot_graphtree_traits::ELLIPSE =>  (draw_ellipse xc::draw_arc,  draw_ellipse  xc::fill_arc);
                get_draw_fns dot_graphtree_traits::DIAMOND =>  (draw_diamond,               fill_diamond);
                get_draw_fns _                             =>  (draw_box  xc::draw_box,     draw_box  xc::fill_box);
            end;
        end;

        # Return the smallest box containing
        # given box with the same origin, and
        # same wide/high ratio as given template.
        #
        fun make_box
              { containing  =>  gf::BOX { wide, high, x, y },           # Given box.
                shaped_like =>  { wide=>tx, high=>ty, ...  }: g2d::Box  # Given template.
              }
            =
            {   tmpy = (wide * (f8b::from_int ty)) / (f8b::from_int tx);

                if (tmpy >= high)   gf::BOX { x, y, wide, high => tmpy };
                else                gf::BOX { x, y, wide => (high * (f8b::from_int tx)) / (f8b::from_int ty), high };
                fi;
            };

        # Resize viewport onto graph in response
        # (e.g.) to a resize of the toplevel window.
        #
        fun resize_box
            ( graph_bbox,
              gf::BOX { x=>new_x, y=>new_y,  wide=>new_wide, high=>new_high },
              gf::BOX { x=>old_x, y=>old_y,  wide=>old_wide, high=>old_high }
            )
            =
            {   graph_bbox ->  gf::BOX { x=>bb_x, y=>bb_y, wide=>bb_wide, high=>bb_high };              # "bb" == "bounding box"

                new_y = if (new_high >= bb_high)
                            #
                            bb_y - (new_high - bb_high) / 2.0;
                        else
                            ybot = old_y + old_high;
                            ytop = ybot  - new_high;

                            if (ytop < bb_y)  (ybot <= bb_y          )  ??  bb_y                   ::  ytop;
                            else              (ytop >= bb_y + bb_high)  ??  bb_y+bb_high-new_high  ::  ytop;
                            fi;
                        fi;

                new_x = if (new_wide >= bb_wide)
                            #
                            bb_x - (new_wide - bb_wide) / 2.0;
                        else
                            xl = old_x;
                            xr = old_x + new_wide;

                            if (xl < bb_x)   if (xr <= bb_x        )  bb_x;                  else xl; fi;
                            else             if (xl >= bb_x+bb_wide)  bb_x+bb_wide-new_wide; else xl; fi;
                            fi;
                        fi;

                gf::BOX { x => new_x,
                          y => new_y,
                          #
                          wide => new_wide,
                          high => new_high
                        };
            };

        # Update position of scrollbar thumbs
        # to reflect resized window or such:
        #
        fun set_scrollbars
                (to_scrollbars_slot, gf::BOX { x=>minx, y=>miny, wide, high } )
                visible_box                                                                     # Currently visible part of full graph.
            =
            {   fun max (a: Float, b) =  if (a > b)  a;  else  b;  fi;
                fun min (a: Float, b) =  if (a < b)  a;  else  b;  fi;

                my { x=>ul_x, y=>ul_y } =   gf::upperleft_of_box  visible_box;  # "ul" == "upper left"
                my { x=>lr_x, y=>lr_y } =  gf::lowerright_of_box  visible_box;  # "lr" == "lower right"

                startx = max (ul_x, minx);
                starty = max (ul_y, miny);

                sizex  = min (lr_x, minx+wide) - startx;
                sizey  = min (lr_y, miny+high) - starty;

                high  = truncate high;
                wide  = truncate wide;

                put_in_mailslot ( to_scrollbars_slot,
                       { horizontal => VIEWDIM { min=>truncate (startx-minx), size=>truncate sizex, total=>wide },
                         vertical   => VIEWDIM { min=>truncate (starty-miny), size=>truncate sizey, total=>high }
                       }
                     );
            };

        fun make_coordinate_transformation_functions (window_box, graph_box)
            =
            {   # Generate transformation functions between
                # universal (graph) and window spaces.
                # We assume the two rectangles have
                # the same high/wide ratio:

                window_box ->  { col=>wmin_x, row=>wmin_y, wide=>wdelta_x, high=>wdelta_y };
                graph_box  ->  gf::BOX { x  =>umin_x, y  =>umin_y, wide=>udelta_x, high=>udelta_y };

                wmin_x =  f8b::from_int  wmin_x;
                wmin_y =  f8b::from_int  wmin_y;

                wdelta_x =  f8b::from_int  wdelta_x;
                wdelta_y =  f8b::from_int  wdelta_y;

                # Projection function from universal (graph)
                # to window coordinates:
                #
                fun u2w ({ x, y })
                    = 
                    { col => floor((wdelta_x * (x - umin_x)) / udelta_x + wmin_x),
                      row => floor((wdelta_y * (y - umin_y)) / udelta_y + wmin_y)
                    };

                # Projection function from window
                # to universal (graph) coordinates:
                #
                fun w2u ({ col, row } )
                    =
                              { x => (udelta_x*((f8b::from_int  col) - wmin_x)) / wdelta_x + umin_x,
                                y => (udelta_y*((f8b::from_int  row) - wmin_y)) / wdelta_y + umin_y
                              };

                (u2w, w2u);
            };

        fun find_visible_nodes graph (window_box, graph_to_window_space)
            =
            pg::nodes_fold  note_node_if_visible  graph  []
            where

                gws =  map_box_from_graph_to_window_space  graph_to_window_space;


                fun is_visible  node
                    =
                    g2d::box::intersect (window_box, gws (pg::node_info_of node).bbox);


                fun make_view_node  node
                    =
                    {   (pg::node_info_of  node)
                            ->
                            { bbox, label, shape, ... };

                        (get_draw_fns shape)
                            ->
                            (draw, fill);

                        { node, label, fill, draw,  bbox => gws bbox  };
                    };


                fun note_node_if_visible (node, results)
                    =
                    if (is_visible node)   make_view_node node  !  results;
                    else                   results;
                    fi;

            end;

        # Draw all nodes and edges visible
        # in the current graph viewport:
        #
        fun draw_viewport_full
              (graph, window, white_pen, black_pen, red_pen) 
              ( { graph_to_window_space, visible_nodes, font, picked_node, ... }: View_Data)
            =
            {   drawable =  xc::drawable_of_window window;

                draw_spline
                    =
                    (xc::draw_lines  drawable  black_pen)
                    o
                    bs::b_spline;

                put_text = put_text (window, font, black_pen);


                fun draw_arrowhead points
                    =
                    xc::fill_polygon
                      drawable
                      black_pen
                      { verts =>  map  graph_to_window_space  points,
                        shape =>  xc::CONVEX_SHAPE
                      };


                fun draw_edges ( { node, ... } : Viewnode)
                    =
                    {   pg::out_edges_apply  draw_edge  (graph, node);
                        pg::in_edges_apply   draw_edge  (graph, node);
                    }
                    where
                        fun draw_edge  edge
                            =
                            {   my { points, arrowhead, ... }
                                    =
                                    pg::edge_info_of  edge;

                                draw_spline  (map  graph_to_window_space  points);      # Body of edge.

                                draw_arrowhead  arrowhead;                              # Head of edge.
                            };
                    end;


                # Draw a node, white on black:
                #
                fun draw_node ( { bbox, label, draw, fill, ... }: Viewnode)
                    =
                    {   # clear_area (drawable_of_window window) bbox; 
                        fill (drawable, white_pen) bbox;
                        put_text (label, bbox);
                        draw (drawable, black_pen) bbox;
                    };


                # Draw a node, white on red if
                # picked, else white on black:
                #
                fun draw_node'  picked_node  ({ bbox, label, draw, fill, node }: Viewnode)
                    =
                    {   if (pg::eq_node (node, picked_node))   fill (drawable,   red_pen) bbox;
                        else                                   fill (drawable, white_pen) bbox;
                        fi;

                        put_text (label, bbox);

                        draw (drawable, black_pen) bbox;
                    };

                xc::clear_drawable (xc::drawable_of_window  window);

                apply  draw_edges  visible_nodes;

                case picked_node
                    #
                    NULL            =>  apply  draw_node               visible_nodes;
                    THE picked_node =>  apply (draw_node' picked_node) visible_nodes;
                esac;
            };                                          # fun draw_viewport_full

        # When a window is uncovered, the X server
        # sends a list of sub-rectangles of that
        # window which need redrawing.  Our job
        # here is to redraw one of those sub-rectangles,
        # specified by 'windowspace_clip_box':
        #
        fun draw_viewport_part
              (graph, window, white_pen, black_pen, red_pen) 
              ( { graph_to_window_space, window_to_graph_space, visible_nodes, font, picked_node, ... }: View_Data)
              windowspace_clip_box
            =
            {   drawable = xc::drawable_of_window window;
                #
                draw_spline = (xc::draw_lines drawable black_pen) o bs::b_spline;

                put_text    = put_text (window, font, black_pen);

                graphspace_clip_box
                    =
                    map_box_from_window_to_graph_space
                        window_to_graph_space
                        windowspace_clip_box;

                fun draw_arrowhead points
                    =
                    xc::fill_polygon
                      drawable
                      black_pen
                      { verts =>  map graph_to_window_space points,
                        shape =>  xc::CONVEX_SHAPE
                      };

                fun draw_edges ( { node, ... }: Viewnode)
                    =
                    {  pg::out_edges_apply  draw_edge  (graph, node);
                       pg::in_edges_apply   draw_edge  (graph, node);
                    }
                    where
                        fun draw_edge  edge
                            =
                            {   (pg::edge_info_of  edge)
                                    ->
                                    { points, arrowhead, bbox };

                                if (gf::intersect (bbox, graphspace_clip_box))
                                    #
                                    draw_spline (map graph_to_window_space points);
                                    draw_arrowhead arrowhead;
                                fi;
                            };
                    end;

                # Draw a node, white on black:
                #
                fun draw_node ( { bbox, label, draw, fill, ... }: Viewnode)
                    = 
                    if (g2d::box::intersect (bbox, windowspace_clip_box))
                        #
                        # clear_area (xc::drawable_of_window window) bbox; 
                        #
                        fill (drawable, white_pen) bbox;
                        put_text (label, bbox);
                        draw (drawable, black_pen) bbox;
                    fi;

                # Draw a node, white on red if
                # picked, else white on black:
                #
                fun draw_node'  picked_node  ({ node, bbox, label, draw, fill }: Viewnode)
                    =
                    if (g2d::box::intersect (bbox, windowspace_clip_box))
                        #
                        if (pg::eq_node (node, picked_node))   fill (drawable,   red_pen) bbox;
                        else                                   fill (drawable, white_pen) bbox;
                        fi;

                        put_text (label, bbox);
                        draw (drawable, black_pen) bbox;
                    fi;

                apply draw_edges visible_nodes;

                case picked_node
                    #
                    NULL            =>  apply  draw_node               visible_nodes;
                    THE picked_node =>  apply (draw_node' picked_node) visible_nodes;
                esac;
            };

        # Redraw in black-on-red a node,
        # to acknowledge a user click on it,
        # then update state.picked_node:
        #
        fun set_select (window, black_pen, red_pen) 
            ( { visible_nodes, graph_to_window_space, window_to_graph_space, font, ... }: View_Data, 
              { node, bbox, label, fill, draw }: Viewnode
            )
            = 
            {   fill (xc::drawable_of_window window, red_pen) bbox; 
                put_text (window, font, black_pen) (label, bbox);
                draw (xc::drawable_of_window window, black_pen) bbox; 
                { visible_nodes, graph_to_window_space, window_to_graph_space, font, picked_node => THE node };
            };

        # Redraw in black-on-white a previously black-on-red node,
        # to show that it is no longer the 'picked' node, then
        # clear state.picked_node:
        #
        fun unset_select (window, black_pen, white_pen) 
            ( { visible_nodes, graph_to_window_space, window_to_graph_space, font, ... }: View_Data, node)
            =
            {   (pg::node_info_of  node)
                    ->
                    { bbox, label, shape, ... };
                    
                (get_draw_fns  shape)
                    ->
                    (draw, fill);

                window_box =  map_box_from_graph_to_window_space  graph_to_window_space  bbox;

                fill (xc::drawable_of_window window, white_pen) window_box; 

                put_text (window, font, black_pen)
                         (label, window_box);

                draw (xc::drawable_of_window  window,  black_pen)
                     window_box; 

                { visible_nodes, font, graph_to_window_space, window_to_graph_space, picked_node => NULL };
            };

        # Messages sent from mouse thread to main thread,
        # reflecting mouse actions abstracted into higher-
        # level operations:
        #
        Mouse_Mail
          = PICK     g2d::Point
          | ZOOM_IN  g2d::Box
          | ZOOM_OUT g2d::Box
          | RESET
          | BLOCK
          | UNBLOCK
          | GET_PICK Oneshot_Maildrop( Null_Or( (String, String,  Null_Or( (Int, Int) ) ) ) )
          ;

        fun mouse_thread (root_window, window, m, mouse_slot)
            =
            mouse_loop ()
            where

                fun get_pick ()
                    =
                    {   oneshot =  make_oneshot_maildrop ();
                        #
                        put_in_mailslot (mouse_slot, GET_PICK oneshot);

                        get_from_oneshot  oneshot;
                    };

                mevt     =  if_then' (m, xc::get_contents_of_envelope);                         # "threadkit::if_then'" is the plain name for  threadkit::(==>).
                getrect  =  gm::get_box (window, m);
                no_bttns =  xc::make_mousebutton_state [];

                fun getr msg
                    =
                    {   put_in_mailslot (mouse_slot, BLOCK);
                        #
                        null_or_box = block_until_mailop_fires  (getrect  (xc::MOUSEBUTTON 3));

                        put_in_mailslot (mouse_slot, UNBLOCK);

                        case null_or_box
                            #
                            NULL    =>  ();
                            THE box =>  put_in_mailslot (mouse_slot, msg box);
                        esac;   
                    };

                fun popup menu
                    =
                    pu::make_lowlevel_popup_menu (root_window, menu, NULL);

                fun menu2 (mname, fname, range)
                    =
                    {   fun open_view (loc, range) ()
                            =
                            show_graph::open_viewer  root_window
                              {
                                loc,
                                range,
                                file   => fname,
                                module =>  mname
                              };

                        case range
                            #
                            NULL =>
                                pu::POPUP_MENU
                                  [
                                    pu::POPUP_MENU_ITEM ("View " + mname, open_view (1, NULL))
                                  ];

                            THE (first, last)
                                =>
                                pu::POPUP_MENU
                                  [
                                    pu::POPUP_MENU_ITEM ("View " + fname, open_view (first, NULL)),
                                    pu::POPUP_MENU_ITEM ("View " + mname, open_view (first, THE { first, last } ))
                                  ];
                        esac;
                    };

               popup3
                  =
                  popup
                      (pu::POPUP_MENU
                        [
                          pu::POPUP_MENU_ITEM ("Zoom in",  \\ () = getr ZOOM_IN),
                          pu::POPUP_MENU_ITEM ("Zoom out", \\ () = getr ZOOM_OUT),
                          pu::POPUP_MENU_ITEM ("Reset",    \\ () = put_in_mailslot (mouse_slot, RESET)),
                          pu::POPUP_MENU_ITEM ("Quit",     \\ () = {  widget::delete_root_window root_window;  shut_down_thread_scheduler winix__premicrothread::process::success; }  )
                        ]
                      );

                fun mouse_loop ()
                    =
                    for (;;) {
                        #
                        case (block_until_mailop_fires  mevt)
                            #
                            xc::MOUSE_FIRST_DOWN { mouse_button=>btn, window_point, screen_point, ... }
                                =>
                                case btn
                                    #
                                    xc::MOUSEBUTTON 1
                                        =>
                                        put_in_mailslot (mouse_slot, PICK window_point);

                                    xc::MOUSEBUTTON 2
                                        =>
                                        case (get_pick ())
                                            #
                                            NULL => ();

                                            THE info
                                                =>
                                                {   popup2 =  popup  (menu2 info);
                                                    #
                                                    case (block_until_mailop_fires  (popup2  (btn, pu::PUT_POPUP_MENU_ITEM_BENEATH_MOUSE 0, screen_point, m)))
                                                        #
                                                        NULL       =>  ();
                                                        THE action =>  action ();
                                                    esac;
                                                };
                                        esac;

                                    xc::MOUSEBUTTON 3
                                        =>
                                        case (block_until_mailop_fires  (popup3  (btn,  pu::PUT_POPUP_MENU_ITEM_BENEATH_MOUSE 0, screen_point, m)))
                                            # 
                                            NULL       =>  ();
                                            THE action =>  action ();
                                        esac;

                                    xc::MOUSEBUTTON _
                                        =>
                                        ();
                                 esac;

                            _ => ();

                        esac;
                    };
            end;


        fun make_graphviz_widget (font_family_cache, root_window) graph
            =
            GRAPHVIZ_WIDGET {
                plea_slot,
                to_scrollbars_slot,
                #
                widget => w,
                graph
            }
            where

                (pg::graph_info_of  graph)
                    ->
                    { graph_bbox, fontsize, ... };

                screen =  wg::screen_of  root_window;

                # We draw edges, text and node outlines in black.
                # We fill nodes in white, except that
                # we fill the currently picked node (if any) in red.
                #
                white_pen =  xc::make_pen [xc::p::FOREGROUND  xc::rgb8_white];
                black_pen =  xc::make_pen [xc::p::FOREGROUND  xc::rgb8_black];
                red_pen   =  xc::make_pen [xc::p::FOREGROUND  xc::rgb8_red  ];

                plea_slot          =  make_mailslot ();         # We get external requests via this.
                mouse_slot         =  make_mailslot ();         # we get analysed mouse input from the mouse_thread via this.
                to_scrollbars_slot =  make_mailslot ();         # We send scrollbar update commands to our parent scrollable_graphviz_widget via this.

                min_size = 30;

                # The dimensions should be scaled
                # to fit reasonably in the screen:
                #
                graph_bbox ->  gf::BOX { wide, high, ... };

                size_preferences
                    =
                    { col_preference =>   wg::INT_PREFERENCE { start_at=>0, step_by=>1, min_steps=>min_size, best_steps=>max (min_size, floor wide), max_steps=>NULL },
                      row_preference =>   wg::INT_PREFERENCE { start_at=>0, step_by=>1, min_steps=>min_size, best_steps=>max (min_size, floor high), max_steps=>NULL }
                    };

                set_scrollbars = set_scrollbars (to_scrollbars_slot, graph_bbox);

                # Most 2D graphics are sized in pixels,
                # but fonts are sized in points.  This
                # fn maps pixel sizes to point sizes.
                # We use this as part of selecting an
                # appropriate size font for node text:
                # 
                pix2pts
                    =
                    {   my { high=>htpx, ... } =  wg::size_of_screen     root_window;
                        my { high=>htmm, ... } =  wg::mm_size_of_screen  root_window;

                        fact = (((f8b::from_int htmm)/(f8b::from_int htpx))/25.4) * 72.0;

                        \\ px = floor((f8b::from_int px)*fact);
                    };

                fun scale_font proj
                    =
                    {    my { row=>y,  ... }: g2d::Point =  proj gf::point_zero;
                         my { row=>y', ... }: g2d::Point =  proj ({ x=>0.0, y=> f8b::from_int  fontsize });

                         pix2pts (y' - y);
                    };

                fun set_state (window_box, graph_box, picked_node)
                    =
                    {   (make_coordinate_transformation_functions (window_box, graph_box))
                            ->
                            (graph_to_window_space, window_to_graph_space);

                        { graph_to_window_space,
                          window_to_graph_space,
                          #
                          visible_nodes  =>  find_visible_nodes graph (window_box, graph_to_window_space),
                          #
                          font   =>  ffc::get_font font_family_cache (scale_font graph_to_window_space),
                          picked_node
                        };
                    };

                fun set_view (arg as (_, graph_box, _))
                    #
                    # Set visible part of graph, for ETC_RESIZE, zoom in/out etc.
                    =
                    {   set_scrollbars graph_box;
                        set_state arg;
                    };

                fun realize_widget { kidplug, window, window_size }
                    =
                    {   xtr::make_thread  "graphviz mouse"  {.
                            #
                            mouse_thread (root_window, window, from_mouse', mouse_slot);
                        };

                        xtr::make_thread  "graphviz widget"  {.
                            #
                            main_thread (window_size, initial_state);
                            ();
                        };

                        ();
                    }
                    where
                        my  xc::KIDPLUG { from_mouse', from_other', to_mom, ... }
                            =
                            xc::ignore_keyboard  kidplug;

                        draw_viewport_full' = draw_viewport_full (graph, window, white_pen, black_pen, red_pen);
                        draw_viewport_part' = draw_viewport_part (graph, window, white_pen, black_pen, red_pen);

                        set_select   =  set_select   (window, black_pen,   red_pen);
                        unset_select =  unset_select (window, black_pen, white_pen);

                        window_box = g2d::box::make (g2d::point::zero, window_size);

                        initial_state
                            =
                            set_view (window_box, make_box { containing => graph_bbox, shaped_like => window_box }, NULL);

                        fun main_thread (window_size, state)
                            =
                            main_loop state
                            where

                                fun scroll_mailop (horizontal, newst, { window_to_graph_space, picked_node, ... }: View_Data)
                                    =
                                    {   window_box = g2d::box::make (g2d::point::zero, window_size);

                                        my gf::BOX { x, y, wide, high }
                                            =
                                            map_box_from_window_to_graph_space  window_to_graph_space  window_box;

                                        my (x, y)
                                            =
                                            horizontal
                                                ##
                                                ??  (f8b::from_int  newst, y)
                                                ::  (x, f8b::from_int  newst);

                                        new_state
                                            =
                                            set_state (window_box, gf::BOX { x, y, wide, high }, picked_node);

                                        draw_viewport_full'  new_state;

                                        new_state;
                                    };


                                fun do_mom (xc::ETC_REDRAW damaged_screen_areas, state)
                                        =>
                                        {   # 'damaged_screen_areas' is a list of rectangular
                                            # areas within our window which the X server
                                            # wants us to redraw.
                                            #   
                                            xlogger::log_if xlogger::hostwindow_to_widget_router_tracing 0 {. "redraw"; };
                                            apply (draw_viewport_part' state) damaged_screen_areas;
                                            state;
                                        };

                                    do_mom (xc::ETC_RESIZE ({ wide, high, ... } ),{ graph_to_window_space, window_to_graph_space, picked_node, ... } )
                                        =>
                                        {   new_window_size =  { wide, high };
                                            window_box      =  g2d::box::make (g2d::point::zero, new_window_size);

                                            new_box  = map_box_from_window_to_graph_space  window_to_graph_space  window_box;
                                            old_box  = map_box_from_window_to_graph_space  window_to_graph_space  (g2d::box::make (g2d::point::zero, window_size));

                                            new_state = set_view (window_box, resize_box (graph_bbox, new_box, old_box), picked_node);

                                            xlogger::log_if xlogger::hostwindow_to_widget_router_tracing 0 {. "resize."; };
                                            xc::clear_drawable (xc::drawable_of_window window);
                                            main_thread (new_window_size, new_state);
                                        };

                                    do_mom (_, state)
                                        =>
                                        state;
                                end;


                                fun zoom_in (box, { window_to_graph_space, picked_node, ... }: View_Data)
                                    =
                                    {   window_box =  g2d::box::make (g2d::point::zero, window_size);
                                        graph_box  =  map_box_from_window_to_graph_space  window_to_graph_space  box;

                                        fun check (box as gf::BOX { x, y, wide, high } )
                                            =
                                            if (wide < 30.0 or high < 30.0)   gf::BOX { x, y, wide=>30.0, high=>30.0 };
                                            else                              box;
                                            fi;

                                        new_state = set_view (window_box, make_box { containing => check graph_box, shaped_like => window_box }, picked_node);

                                        draw_viewport_full'  new_state;

                                        new_state;
                                    };


                                fun zoom_out (box, { window_to_graph_space, picked_node, ... }: View_Data)
                                    =
                                    {   window_box =  g2d::box::make (g2d::point::zero, window_size);
                                        graph_box  =  map_box_from_window_to_graph_space window_to_graph_space window_box;
                                        new_state  =  set_state (gf::to_box (make_box { containing => gf::from_box box, shaped_like => window_box }), graph_box, picked_node);

                                        #  Note that the 2nd argument to set_scrollbars is 
                                        #  not graph_box, as we have changed the perspective. 

                                        set_scrollbars (map_box_from_window_to_graph_space new_state.window_to_graph_space window_box);
                                        draw_viewport_full'  new_state;
                                        new_state;
                                    };


                                # Process a user mouseclick in the graph viewport:
                                # Redraw in black-on-white any previously picked node.
                                # If a node was clicked on, redraw it in black-on-red and remember it.
                                # 
                                fun do_pick (point, state as { visible_nodes, picked_node, ... }: View_Data)
                                    =
                                    {   fun accept ( { bbox, ... }: Viewnode)
                                            =
                                            g2d::point::in_box (point, bbox);

                                        case (list::find  accept  visible_nodes)
                                            #
                                            THE (nvn as { node=>newvn, ... } )        #  New pick 
                                                =>
                                                case picked_node
                                                    #
                                                    THE oldvn                        #  Old pick 
                                                        =>
                                                        # If old == new, do nothing:
                                                        #
                                                        if (pg::eq_node (newvn, oldvn))
                                                            #
                                                            state;
                                                        else
                                                            unset_select (state, oldvn);
                                                            set_select (state, nvn);
                                                        fi;

                                                    _   =>
                                                        set_select (state, nvn);
                                                esac;

                                            _   =>                                     #  No new pick 
                                                case picked_node
                                                    #
                                                    THE oldvn =>  unset_select (state, oldvn);      #  Old pick 
                                                    _         =>  state;
                                                esac;
                                        esac;
                                    };


                                fun reset ( { picked_node, ... }: View_Data)
                                    =
                                    {   window_box   =  g2d::box::make (g2d::point::zero, window_size);

                                        new_state
                                            =
                                            set_view
                                              ( window_box,
                                                make_box { containing => graph_bbox, shaped_like => window_box },
                                                picked_node
                                              );

                                        draw_viewport_full'  new_state;

                                        new_state;
                                    };

                                fun block (state, ci_list)
                                    =
                                    do_one_mailop [

                                        take_from_mailslot' mouse_slot
                                            ==>
                                            (\\ msg = case msg
                                                          #     
                                                          UNBLOCK =>  fold_backward (\\ (m, s) =  do_mom (m, s))        # 2009-12-28 CrT: Changed 'fold' to 'fold_backward', I hope that is right!
                                                                           state
                                                                           ci_list;

                                                          _       =>  block (state, ci_list);

                                                      esac
                                            ),

                                        from_other'
                                            ==>
                                            (\\ envelope =  block (state, (xc::get_contents_of_envelope envelope) ! ci_list))
                                    ];


                                fun get_pick ( { picked_node => NULL, ... }: View_Data)
                                        =>
                                        NULL;

                                    get_pick { picked_node => THE node, ... }
                                        =>
                                        {   info =  pg::node_info_of  node;

                                            get_trait = dot_graphtree::get_trait (dot_graphtree::NODE_PART info.base);

                                            fun error trait
                                                =
                                                {   fil::print("missing " + trait + " trait\n");
                                                    #
                                                    raise exception DIE (cat [ "missing ", trait, " trait" ]);
                                                };

                                            fname = case (get_trait "file")
                                                        #
                                                        NULL  => error "file";
                                                        THE s => s;
                                                    esac;

                                            range = case (get_trait "range")
                                                        #
                                                        NULL  => NULL;
                                                        #
                                                        THE s => case (scanf::sscanf_by "%d:%d" s)
                                                                     THE [scanf::INT a, scanf::INT b] =>  THE (a, b);
                                                                     _                                =>  error "range";
                                                                 esac;
                                                     esac;

                                            THE (info.label, fname, range);
                                        };
                                end;


                                fun do_mouse (PICK point,  state:  View_Data)
                                        =>
                                        do_pick (point, state);

                                    do_mouse (ZOOM_IN box, state)     =>  zoom_in  (box, state);
                                    do_mouse (ZOOM_OUT box, state)    =>  zoom_out (box, state);

                                    do_mouse (RESET, state)            =>  reset state;
                                    do_mouse (BLOCK, state)            =>  block (state,[]);

                                    do_mouse (GET_PICK oneshot, state) =>  {  put_in_oneshot (oneshot, get_pick state);   state;  };
                                    do_mouse (_, state)                =>  state;
                                end;


                                fun do_plea (SET_VERTICAL_VIEW   v, state:  View_Data) =>  scroll_mailop (FALSE, v, state);
                                    do_plea (SET_HORIZONTAL_VIEW v, state:  View_Data) =>  scroll_mailop (TRUE,  v, state);
                                    do_plea (DELETE,                state:  View_Data) =>  state;
                                end;


                                fun main_loop state
                                    =
                                    main_loop (
                                        #
                                        do_one_mailop [
                                            #
                                            take_from_mailslot' mouse_slot
                                                ==>
                                                (\\ mail =  do_mouse (mail, state)),

                                            from_other'
                                                ==>
                                                (\\ envelope = do_mom (xc::get_contents_of_envelope envelope, state)),

                                            take_from_mailslot' plea_slot
                                                ==>
                                                (\\ msg = do_plea (msg, state))
                                        ]
                                   );

                           end;

                      end;

                w =  wg::make_widget
                      {
                        root_window,
                        args      =>  (\\ ()  = {   background => NULL }),      # Added 2009-12-28 CrT just to get it to compile
                        size_preference_thunk_of =>  {. size_preferences; },
                        realize_widget
                      };
            end;                                                                # fun make_graphviz_widget


        fun as_widget               (GRAPHVIZ_WIDGET { widget,             ... } )   =  widget;
        fun set_horizontal_view     (GRAPHVIZ_WIDGET { plea_slot,          ... } ) v =  put_in_mailslot (plea_slot, SET_HORIZONTAL_VIEW v);
        fun set_vertical_view       (GRAPHVIZ_WIDGET { plea_slot,          ... } ) v =  put_in_mailslot (plea_slot, SET_VERTICAL_VIEW   v);
        fun to_scrollbars_mailop_of (GRAPHVIZ_WIDGET { to_scrollbars_slot, ... } )   =  take_from_mailslot' to_scrollbars_slot;

    };                          # package graphviz_widget 

end;


Comments and suggestions to: bugs@mythryl.org

PreviousUpNext