%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2011-2012. All Rights Reserved. %% %% The contents of this file are subject to the Erlang Public License, %% Version 1.1, (the "License"); you may not use this file except in %% compliance with the License. You should have received a copy of the %% Erlang Public License along with this software. If not, it can be %% retrieved online at http://www.erlang.org/. %% %% Software distributed under the License is distributed on an "AS IS" %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See %% the License for the specific language governing rights and limitations %% under the License. %% %% %CopyrightEnd% -module(observer_app_wx). -export([start_link/2]). %% wx_object callbacks -export([init/1, handle_info/2, terminate/2, code_change/3, handle_call/3, handle_event/2, handle_sync_event/3, handle_cast/2]). -behaviour(wx_object). -include_lib("wx/include/wx.hrl"). -include("observer_defs.hrl"). %% Import drawing wrappers -import(observer_perf_wx, [haveGC/1, setPen/2, setFont/3, setBrush/2, strokeLine/5, strokeLines/2, drawRoundedRectangle/6, drawText/4, getTextExtent/2]). -record(state, { parent, panel, apps_w, app_w, paint, current, app, sel, appmon, usegc = false }). -record(paint, {font, pen, brush, sel, links}). -record(app, {ptree, n2p, links, dim}). -record(box, {x,y, w,h, s1}). -record(str, {x,y,text,pid}). -define(BX_E, 10). %% Empty width between text and box -define(BX_HE, (?BX_E div 2)). -define(BY_E, 10). %% Empty height between text and box -define(BY_HE, (?BY_E div 2)). -define(BB_X, 16). %% Empty width between boxes -define(BB_Y, 12). %% Empty height between boxes -define(DRAWAREA, 5). -define(ID_PROC_INFO, 101). -define(ID_PROC_MSG, 102). -define(ID_PROC_KILL, 103). -define(ID_TRACE_PID, 104). -define(ID_TRACE_NAME, 105). -define(ID_TRACE_TREE_PIDS, 106). -define(ID_TRACE_TREE_NAMES, 107). -define(wxGC, wxGraphicsContext). start_link(Notebook, Parent) -> wx_object:start_link(?MODULE, [Notebook, Parent], []). init([Notebook, Parent]) -> Panel = wxPanel:new(Notebook, [{size, wxWindow:getClientSize(Notebook)}, {winid, 1} ]), Main = wxBoxSizer:new(?wxHORIZONTAL), Splitter = wxSplitterWindow:new(Panel, [{size, wxWindow:getClientSize(Panel)}, {style, ?wxSP_LIVE_UPDATE}, {id, 2} ]), Apps = wxListBox:new(Splitter, 3, []), %% Need extra panel and sizer to get correct size updates %% in draw area for some reason P2 = wxPanel:new(Splitter, [{winid, 4}]), Extra = wxBoxSizer:new(?wxVERTICAL), DrawingArea = wxScrolledWindow:new(P2, [{winid, ?DRAWAREA}, {style,?wxFULL_REPAINT_ON_RESIZE}]), wxWindow:setBackgroundColour(DrawingArea, ?wxWHITE), wxWindow:setVirtualSize(DrawingArea, 800, 800), wxSplitterWindow:setMinimumPaneSize(Splitter,50), wxSizer:add(Extra, DrawingArea, [{flag, ?wxEXPAND},{proportion, 1}]), wxWindow:setSizer(P2, Extra), wxSplitterWindow:splitVertically(Splitter, Apps, P2, [{sashPosition, 150}]), wxWindow:setSizer(Panel, Main), wxSizer:add(Main, Splitter, [{flag, ?wxEXPAND bor ?wxALL}, {proportion, 1}, {border, 5}]), wxWindow:setSizer(Panel, Main), wxListBox:connect(Apps, command_listbox_selected), wxPanel:connect(DrawingArea, paint, [callback]), wxPanel:connect(DrawingArea, size, [{skip, true}]), wxPanel:connect(DrawingArea, left_up), wxPanel:connect(DrawingArea, left_dclick), wxPanel:connect(DrawingArea, right_down), case os:type() of {win32, _} -> %% Ignore erase on windows wxPanel:connect(DrawingArea, erase_background, [{callback, fun(_,_) -> ok end}]); _ -> ok end, UseGC = haveGC(DrawingArea), Version28 = ?wxMAJOR_VERSION =:= 2 andalso ?wxMINOR_VERSION =:= 8, Font = case os:type() of {unix,_} when UseGC, Version28 -> wxFont:new(12,?wxFONTFAMILY_DECORATIVE,?wxFONTSTYLE_NORMAL,?wxFONTWEIGHT_NORMAL); _ -> wxSystemSettings:getFont(?wxSYS_DEFAULT_GUI_FONT) end, SelCol = wxSystemSettings:getColour(?wxSYS_COLOUR_HIGHLIGHT), GreyBrush = wxBrush:new({230,230,240}), SelBrush = wxBrush:new(SelCol), LinkPen = wxPen:new(SelCol, [{width, 2}]), process_flag(trap_exit, true), {Panel, #state{parent=Parent, panel =Panel, apps_w=Apps, app_w =DrawingArea, usegc = UseGC, paint=#paint{font = Font, pen = wxPen:new({80,80,80}, [{width, 2}]), brush= GreyBrush, sel = SelBrush, links= LinkPen } }}. setup_scrollbar(AppWin, App) -> setup_scrollbar(wxWindow:getClientSize(AppWin), AppWin, App). setup_scrollbar({CW, CH}, AppWin, #app{dim={W0,H0}}) -> W = max(W0,CW), H = max(H0,CH), PPC = 20, if W0 =< CW, H0 =< CH -> wxScrolledWindow:setScrollbars(AppWin, W, H, 0, 0); H0 =< CH -> wxScrolledWindow:setScrollbars(AppWin, PPC, H, W div PPC+1, 0); W0 =< CW -> wxScrolledWindow:setScrollbars(AppWin, W, PPC, 0, H div PPC+1); true -> wxScrolledWindow:setScrollbars(AppWin, PPC, PPC, W div PPC+1, H div PPC+1) end; setup_scrollbar(_, _, undefined) -> ok. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% handle_event(#wx{event=#wxCommand{type=command_listbox_selected, cmdString=AppStr}}, State = #state{appmon=AppMon, current=Prev}) -> case AppStr of [] -> {noreply, State}; _ -> App = list_to_atom(AppStr), (Prev =/= undefined) andalso appmon_info:app(AppMon, Prev, false, []), appmon_info:app(AppMon, App, true, []), {noreply, State#state{current=App}} end; handle_event(#wx{id=Id, event=_Sz=#wxSize{size=Size}}, State=#state{app=App, app_w=AppWin}) -> Id =:= ?DRAWAREA andalso setup_scrollbar(Size,AppWin,App), {noreply, State}; handle_event(#wx{event=#wxMouse{type=Type, x=X0, y=Y0}}, S0=#state{app=#app{ptree=Tree}, app_w=AppWin}) -> {X,Y} = wxScrolledWindow:calcUnscrolledPosition(AppWin, X0, Y0), Hit = locate_node(X,Y, [Tree]), State = handle_mouse_click(Hit, Type, S0), {noreply, State}; handle_event(#wx{event=#wxCommand{type=command_menu_selected}}, State = #state{sel=undefined}) -> observer_lib:display_info_dialog("Select process first"), {noreply, State}; handle_event(#wx{id=?ID_PROC_INFO, event=#wxCommand{type=command_menu_selected}}, State = #state{panel=Panel, sel={#box{s1=#str{pid=Pid}},_}}) -> observer_procinfo:start(Pid, Panel, self()), {noreply, State}; handle_event(#wx{id=?ID_PROC_MSG, event=#wxCommand{type=command_menu_selected}}, State = #state{panel=Panel, sel={#box{s1=#str{pid=Pid}},_}}) -> case observer_lib:user_term(Panel, "Enter message", "") of cancel -> ok; {ok, Term} -> Pid ! Term; {error, Error} -> observer_lib:display_info_dialog(Error) end, {noreply, State}; handle_event(#wx{id=?ID_PROC_KILL, event=#wxCommand{type=command_menu_selected}}, State = #state{panel=Panel, sel={#box{s1=#str{pid=Pid}},_}}) -> case observer_lib:user_term(Panel, "Enter Exit Reason", "kill") of cancel -> ok; {ok, Term} -> exit(Pid, Term); {error, Error} -> observer_lib:display_info_dialog(Error) end, {noreply, State}; %%% Trace api handle_event(#wx{id=?ID_TRACE_PID, event=#wxCommand{type=command_menu_selected}}, State = #state{sel={Box,_}}) -> observer_trace_wx:add_processes(observer_wx:get_tracer(), [box_to_pid(Box)]), {noreply, State}; handle_event(#wx{id=?ID_TRACE_NAME, event=#wxCommand{type=command_menu_selected}}, State = #state{sel={Box,_}}) -> observer_trace_wx:add_processes(observer_wx:get_tracer(), [box_to_reg(Box)]), {noreply, State}; handle_event(#wx{id=?ID_TRACE_TREE_PIDS, event=#wxCommand{type=command_menu_selected}}, State = #state{sel=Sel}) -> Get = fun(Box) -> box_to_pid(Box) end, observer_trace_wx:add_processes(observer_wx:get_tracer(), tree_map(Sel, Get)), {noreply, State}; handle_event(#wx{id=?ID_TRACE_TREE_NAMES, event=#wxCommand{type=command_menu_selected}}, State = #state{sel=Sel}) -> Get = fun(Box) -> box_to_reg(Box) end, observer_trace_wx:add_processes(observer_wx:get_tracer(), tree_map(Sel, Get)), {noreply, State}; handle_event(Event, _State) -> error({unhandled_event, Event}). %%%%%%%%%% handle_sync_event(#wx{event = #wxPaint{}},_, #state{app_w=DA, app=App, sel=Sel, paint=Paint, usegc=UseGC}) -> %% PaintDC must be created in a callback to work on windows. IsWindows = element(1, os:type()) =:= win32, %% Avoid Windows flickering hack DC = if IsWindows -> wx:typeCast(wxBufferedPaintDC:new(DA), wxPaintDC); true -> wxPaintDC:new(DA) end, IsWindows andalso wxDC:clear(DC), GC = case UseGC of true -> GC0 = ?wxGC:create(DC), %% Argh must handle scrolling when using ?wxGC {Sx,Sy} = wxScrolledWindow:calcScrolledPosition(DA, {0,0}), ?wxGC:translate(GC0, Sx,Sy), GC0; false -> wxScrolledWindow:doPrepareDC(DA,DC), DC end, %% Nothing is drawn until wxPaintDC is destroyed. draw({UseGC, GC}, App, Sel, Paint), UseGC andalso ?wxGC:destroy(GC), wxPaintDC:destroy(DC), ok. %%%%%%%%%% handle_call(Event, From, _State) -> error({unhandled_call, Event, From}). handle_cast(Event, _State) -> error({unhandled_cast, Event}). %%%%%%%%%% handle_info({active, Node}, State = #state{parent=Parent, current=Curr, appmon=Appmon}) -> create_menus(Parent, []), Pid = try Node = node(Appmon), Appmon catch _:_ -> {ok, P} = appmon_info:start_link(Node, self(), []), P end, appmon_info:app_ctrl(Pid, Node, true, []), (Curr =/= undefined) andalso appmon_info:app(Pid, Curr, true, []), {noreply, State#state{appmon=Pid}}; handle_info(not_active, State = #state{appmon=AppMon}) -> appmon_info:app_ctrl(AppMon, node(AppMon), false, []), lists:member(node(AppMon), nodes()) andalso exit(AppMon, normal), observer_wx:set_status(""), {noreply, State#state{appmon=undefined}}; handle_info({delivery, Pid, app_ctrl, _, Apps0}, State = #state{appmon=Pid, apps_w=LBox, current=Curr0}) -> Apps = [atom_to_list(App) || {_, App, {_, _, _}} <- Apps0], wxListBox:clear(LBox), wxListBox:appendStrings(LBox, [App || App <- lists:sort(Apps)]), case Apps of [App|_] when Curr0 =:= undefined -> Curr = list_to_atom(App), appmon_info:app(Pid, Curr, true, []), {noreply, State#state{current=Curr}}; _ -> {noreply, State} end; handle_info({delivery, _Pid, app, _Curr, {[], [], [], []}}, State = #state{panel=Panel}) -> wxWindow:refresh(Panel), {noreply, State#state{app=undefined, sel=undefined}}; handle_info({delivery, Pid, app, Curr, AppData}, State = #state{panel=Panel, appmon=Pid, current=Curr, usegc=UseGC, app_w=AppWin, paint=#paint{font=Font}}) -> GC = if UseGC -> ?wxGC:create(AppWin); true -> wxWindowDC:new(AppWin) end, FontW = {UseGC, GC}, setFont(FontW, Font, {0,0,0}), App = build_tree(AppData, FontW), if UseGC -> ?wxGC:destroy(GC); true -> wxWindowDC:destroy(GC) end, setup_scrollbar(AppWin, App), wxWindow:refresh(Panel), wxWindow:layout(Panel), {noreply, State#state{app=App, sel=undefined}}; handle_info({'EXIT', _, noconnection}, State) -> {noreply, State}; handle_info({'EXIT', _, normal}, State) -> {noreply, State}; handle_info(_Event, State) -> %% io:format("~p:~p: ~p~n",[?MODULE,?LINE,_Event]), {noreply, State}. %%%%%%%%%% terminate(_Event, _State) -> ok. code_change(_, _, State) -> State. handle_mouse_click(Node = {#box{s1=#str{pid=Pid}},_}, Type, State=#state{app_w=AppWin,panel=Panel}) -> case Type of left_dclick -> observer_procinfo:start(Pid, Panel, self()); right_down -> popup_menu(Panel); _ -> ok end, observer_wx:set_status(io_lib:format("Pid: ~p", [Pid])), wxWindow:refresh(AppWin), State#state{sel=Node}; handle_mouse_click(_, _, State = #state{sel=undefined}) -> State; handle_mouse_click(_, right_down, State=#state{panel=Panel}) -> popup_menu(Panel), State; handle_mouse_click(_, _, State=#state{app_w=AppWin}) -> observer_wx:set_status(""), wxWindow:refresh(AppWin), State#state{sel=undefined}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% create_menus(Parent, _) -> MenuEntries = [{"File", [#create_menu{id=?ID_PROC_INFO, text="Process info"}, #create_menu{id=?ID_PROC_MSG, text="Send Msg"}, #create_menu{id=?ID_PROC_KILL, text="Kill process"} ]}, {"Trace", [#create_menu{id=?ID_TRACE_PID, text="Trace process"}, #create_menu{id=?ID_TRACE_NAME, text="Trace named process"}, #create_menu{id=?ID_TRACE_TREE_PIDS, text="Trace process tree"}, #create_menu{id=?ID_TRACE_TREE_NAMES, text="Trace named process tree"} ]}], observer_wx:create_menus(Parent, MenuEntries). popup_menu(Panel) -> Menu = wxMenu:new(), wxMenu:append(Menu, ?ID_PROC_INFO, "Process info"), wxMenu:append(Menu, ?ID_TRACE_PID, "Trace process"), wxMenu:append(Menu, ?ID_TRACE_NAME, "Trace named process"), wxMenu:append(Menu, ?ID_TRACE_TREE_PIDS, "Trace process tree"), wxMenu:append(Menu, ?ID_TRACE_TREE_NAMES, "Trace named process tree"), wxMenu:append(Menu, ?ID_PROC_MSG, "Send Msg"), wxMenu:append(Menu, ?ID_PROC_KILL, "Kill process"), wxWindow:popupMenu(Panel, Menu), wxMenu:destroy(Menu). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% locate_node(X, _Y, [{Box=#box{x=BX}, _Chs}|_Rest]) when X < BX -> {left, Box}; locate_node(X,Y, [Node={Box=#box{x=BX,y=BY,w=BW,h=BH}, _Chs}|Rest]) when X =< (BX+BW)-> if Y < BY -> {above, Box}; %% Above Y =< (BY+BH) -> Node; true -> locate_node(X,Y,Rest) end; locate_node(X,Y, [{_, Chs}|Rest]) -> case locate_node(X,Y,Chs) of Node = {#box{},_} -> Node; _Miss -> locate_node(X,Y,Rest) end; locate_node(_, _, []) -> false. locate_box(From, [{Box=#box{s1=#str{pid=From}},_}|_]) -> Box; locate_box(From, [{_,Chs}|Rest]) -> case locate_box(From, Chs) of Box = #box{} -> Box; _ -> locate_box(From, Rest) end; locate_box(From, []) -> {false, From}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% build_tree({Root, P2Name, Links, XLinks0}, FontW) -> Fam = sofs:relation_to_family(sofs:relation(Links)), Name2P = gb_trees:from_orddict(lists:sort([{Name,Pid} || {Pid,Name} <- P2Name])), Lookup = gb_trees:from_orddict(sofs:to_external(Fam)), {_, Tree0} = build_tree2(Root, Lookup, Name2P, FontW), {Tree, Dim} = calc_tree_size(Tree0), Fetch = fun({From, To}, Acc) -> try {value, ToPid} = gb_trees:lookup(To, Name2P), FromPid = gb_trees:get(From, Name2P), [{locate_box(FromPid, [Tree]),locate_box(ToPid, [Tree])}|Acc] catch _:_ -> Acc end end, XLinks = lists:foldl(Fetch, [], XLinks0), #app{ptree=Tree, dim=Dim, links=XLinks}. build_tree2(Root, Tree0, N2P, FontW) -> case gb_trees:lookup(Root, Tree0) of none -> {Tree0, {box(Root, N2P, FontW), []}}; {value, Children} -> Tree1 = gb_trees:delete(Root, Tree0), {Tree, CHs} = lists:foldr(fun("port " ++_, Acc) -> Acc; %% Skip ports (Child,{T0, Acc}) -> {T, C} = build_tree2(Child, T0, N2P, FontW), {T, [C|Acc]} end, {Tree1, []}, Children), {Tree, {box(Root, N2P, FontW), CHs}} end. calc_tree_size(Tree) -> Cols = calc_col_start(Tree, [0]), {Boxes,{W,Hs}} = calc_tree_size(Tree, Cols, ?BB_X, [?BB_Y]), {Boxes, {W,lists:max(Hs)}}. calc_col_start({#box{w=W}, Chs}, [Max|Acc0]) -> Acc = if Acc0 == [] -> [0]; true -> Acc0 end, Depth = lists:foldl(fun(Child, MDepth) -> calc_col_start(Child, MDepth) end, Acc, Chs), [max(W,Max)|Depth]. calc_tree_size({Box=#box{w=W,h=H}, []}, _, X, [Y|Ys]) -> {{Box#box{x=X,y=Y}, []}, {X+W+?BB_X,[Y+H+?BB_Y|Ys]}}; calc_tree_size({Box, Children}, [Col|Cols], X, [H0|Hs0]) -> Hs1 = calc_row_start(Children, H0, Hs0), StartX = X+Col+?BB_X, {Boxes, {W,Hs}} = calc_tree_sizes(Children, Cols, StartX, StartX, Hs1, []), Y = middle(Boxes, H0), H = Y+Box#box.h+?BB_Y, {{Box#box{x=X,y=Y}, Boxes}, {W,[H|Hs]}}. calc_tree_sizes([Child|Chs], Cols, X0, W0, Hs0, Acc) -> {Tree, {W,Hs}} = calc_tree_size(Child, Cols, X0, Hs0), calc_tree_sizes(Chs, Cols, X0, max(W,W0), Hs, [Tree|Acc]); calc_tree_sizes([], _, _, W,Hs, Acc) -> {lists:reverse(Acc), {W,Hs}}. calc_row_start(Chs = [{#box{h=H},_}|_], Start, Hs0) -> NChs = length(Chs), Wanted = (H*NChs + ?BB_Y*(NChs-1)) div 2 - H div 2, case Hs0 of [] -> [max(?BB_Y, Start - Wanted)]; [Next|Hs] -> [max(Next, Start - Wanted)|Hs] end. middle([], Y) -> Y; middle([{#box{y=Y}, _}], _) -> Y; middle([{#box{y=Y0},_}|List], _) -> {#box{y=Y1},_} = lists:last(List), (Y0+Y1) div 2. box(Str0, N2P, FontW) -> Pid = gb_trees:get(Str0, N2P), Str = if hd(Str0) =:= $< -> lists:append(io_lib:format("~w", [Pid])); true -> Str0 end, {TW,TH} = getTextExtent(FontW, Str), Data = #str{text=Str, x=?BX_HE, y=?BY_HE, pid=Pid}, %% Add pid #box{w=round(TW)+?BX_E, h=round(TH)+?BY_E, s1=Data}. box_to_pid(#box{s1=#str{pid=Pid}}) -> Pid. box_to_reg(#box{s1=#str{text=[$<|_], pid=Pid}}) -> Pid; box_to_reg(#box{s1=#str{text=Name}}) -> list_to_atom(Name). tree_map({Box, Chs}, Fun) -> tree_map(Chs, Fun, [Fun(Box)]). tree_map([{Box, Chs}|Rest], Fun, Acc0) -> Acc = tree_map(Chs, Fun, [Fun(Box)|Acc0]), tree_map(Rest, Fun, Acc); tree_map([], _ , Acc) -> Acc. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% draw(_DC, undefined, _, _) -> ok; draw(DC, #app{dim={_W,_H}, ptree=Tree, links=Links}, Sel, #paint{font=Font, pen=Pen, brush=Brush, links=LPen, sel=SelBrush}) -> setPen(DC, LPen), [draw_xlink(Link, DC) || Link <- Links], setPen(DC, Pen), %% ?wxGC:drawRectangle(DC, 2,2, _W-2,_H-2), %% DEBUG setBrush(DC, Brush), setFont(DC, Font, {0,0,0}), draw_tree(Tree, root, DC), case Sel of undefined -> ok; {#box{x=X,y=Y,w=W,h=H,s1=Str1}, _} -> setBrush(DC, SelBrush), drawRoundedRectangle(DC, X-1,Y-1, W+2,H+2, 8.0), draw_str(DC, Str1, X, Y) end. draw_tree({Box=#box{x=X,y=Y,w=W,h=H,s1=Str1}, Chs}, Parent, DC) -> drawRoundedRectangle(DC, X,Y, W,H, 8.0), draw_str(DC, Str1, X, Y), Dot = case Chs of [] -> ok; [{#box{x=CX0},_}|_] -> CY = Y+(H div 2), CX = CX0-(?BB_X div 2), strokeLine(DC, X+W, CY, CX, CY), {CX, CY} end, draw_link(Parent, Box, DC), [draw_tree(Child, Dot, DC) || Child <- Chs]. draw_link({CX,CY}, #box{x=X,y=Y0,h=H}, DC) -> Y = Y0+(H div 2), case Y =:= CY of true -> strokeLine(DC, CX, CY, X, CY); false -> strokeLines(DC, [{CX, CY}, {CX, Y}, {X,Y}]) end; draw_link(_, _, _) -> ok. draw_xlink({#box{x=X0, y=Y0, h=BH}, #box{x=X1, y=Y1}}, DC) when X0 =:= X1 -> draw_xlink(X0,Y0,X1,Y1,BH,DC); draw_xlink({#box{x=X0, y=Y0, h=BH, w=BW}, #box{x=X1, y=Y1}}, DC) when X0 < X1 -> draw_xlink(X0+BW,Y0,X1,Y1,BH,DC); draw_xlink({#box{x=X0, y=Y0, h=BH}, #box{x=X1, w=BW, y=Y1}}, DC) when X0 > X1 -> draw_xlink(X1+BW,Y1,X0,Y0,BH,DC); draw_xlink({_From, _To}, _DC) -> ignore. draw_xlink(X0, Y00, X1, Y11, BH, DC) -> {Y0,Y1} = if Y00 < Y11 -> {Y00+BH-6, Y11+6}; true -> {Y00+6, Y11+BH-6} end, strokeLines(DC, [{X0,Y0}, {X0+5,Y0}, {X1-5,Y1}, {X1,Y1}]). draw_str(DC, #str{x=Sx,y=Sy, text=Text}, X, Y) -> drawText(DC, Text, X+Sx,Y+Sy); draw_str(_, _, _, _) -> ok.