Module: win32-duim Synopsis: Win32 medium implementation Author: David Gray, Scott McKay, Andy Armstrong Copyright: Original Code is Copyright (c) 1995-2004 Functional Objects, Inc. All rights reserved. License: Functional Objects Library Public License Version 1.0 Dual-license: GNU Lesser General Public License Warranty: Distributed WITHOUT WARRANTY OF ANY KIND /// Win32 mediums // Strictly speaking, this is 'limited(, min: 0, max: #x03FFFFFF)', // but that actually makes things a bit slower! define constant = ; //---*** HughG asks: // Is it valid to cache the Windows pen, brush and other DC objects? // The Win32 SDK example calls GetCurrentObject to compare with the // object it's about to select. This may not be just to avoid having // to put caching into the example: CancelDC can be called from one // thread to affect another's DC, so maybe the current object is not // guaranteed to stay the same until you explicitly change it. // (This doesn't make much sense to me but then I don't understand // CancelDC either, so I'm just generally suspicious.) define sealed class () sealed slot %ink-cache :: = make(); // Cached clipping region sealed slot %clip-mask = #f; // #f, #"none", or #[left, top, right, bottom] sealed slot %hRect :: false-or(type-union(, )) = #f; // Cached font sealed slot %hFont :: false-or() = #f; // The last decoded , its cached Windows pen, and the Windows pen to // restore when we're finished sealed slot %cached-pen :: false-or() = #f; sealed slot %hPen :: false-or() = #f; sealed slot %old-hPen :: false-or() = #f; // The last decoded , its cached Windows brush and the Windows brush to // restore when we're finished sealed slot %cached-brush :: false-or(type-union(, )) = #f; sealed slot %hBrush :: false-or() = #f; sealed slot %old-hBrush :: false-or() = #f; sealed slot %brush-color :: = $native-black; sealed slot %ROP2 :: = $R2-COPYPEN; end class ; define sealed domain make (singleton()); define sealed domain initialize (); define variable $black-hbrush :: false-or() = #f; define variable $white-hbrush :: false-or() = #f; define variable $null-hbrush :: false-or() = #f; define variable $black-hpen :: false-or() = #f; define variable $white-hpen :: false-or() = #f; define variable $null-hpen :: false-or() = #f; define variable $system-hfont :: false-or() = #f; define variable $gui-hfont :: false-or() = #f; define variable $fixed-hfont :: false-or() = #f; define variable $variable-hfont :: false-or() = #f; define method initialize-stock-objects (_port :: ) => () $black-hbrush := pointer-cast(, GetStockObject($BLACK-BRUSH)); $white-hbrush := pointer-cast(, GetStockObject($WHITE-BRUSH)); $null-hbrush := pointer-cast(, GetStockObject($NULL-BRUSH)); $black-hpen := pointer-cast(, GetStockObject($BLACK-PEN)); $white-hpen := pointer-cast(, GetStockObject($WHITE-PEN)); $null-hpen := pointer-cast(, GetStockObject($NULL-PEN)); $system-hfont := pointer-cast(, GetStockObject($SYSTEM-FONT)); $gui-hfont := pointer-cast(, GetStockObject($DEFAULT-GUI-FONT)); $fixed-hfont := pointer-cast(, GetStockObject($ANSI-FIXED-FONT)); $variable-hfont := pointer-cast(, GetStockObject($ANSI-VAR-FONT)); end method initialize-stock-objects; define method clear-ink-cache (medium :: ) //--- This should really deallocate any cached pixmaps, etc. remove-all-keys!(medium.%ink-cache) end method clear-ink-cache; define sealed method initialize (medium :: , #key) => () next-method(); let cache = medium.%ink-cache; gethash(cache, $native-black) := $black; gethash(cache, $native-white) := $white; end method initialize; // This is intentionally _not_ sealed define method make-medium (_port :: , sheet :: ) => (medium :: ) make(, port: _port, sheet: sheet) end method make-medium; define sealed method destroy-medium (medium :: ) => () clear-ink-cache(medium); release-DC(medium); next-method(); medium-drawable(medium) := #f end method destroy-medium; define sealed inline method get-DC (medium :: ) => (hDC :: ) get-DC(medium-drawable(medium)) end method get-DC; // Note that this should not be called by the drawing routines themselves; // it will be called automatically when control returns to the event loop. define sealed method release-DC (medium :: ) => () let drawable = medium-drawable(medium); drawable & release-DC(drawable); // be careful during shutdown... // Force 'invalidate-cached-drawing-state'. Note that 'release-DC' on // mirrors is careful not to do any work for CS_OWNDC windows. That is // not the case here -- when 'release-DC' gets called on a medium, it // is done in order to release any allocated pens, brushes, etc. medium-drawing-state-cache(medium) := 0 end method release-DC; define sealed method window-handle (medium :: ) => (handle :: ) window-handle(medium-drawable(medium)) end method window-handle; define sealed method do-attach-medium (sheet :: , medium :: ) => () let mirror = sheet-mirror(sheet); assert(mirror, "Unexpected failure: no mirror when attaching medium for %=", sheet); clear-ink-cache(medium); medium-drawable(medium) := mirror end method do-attach-medium; define sealed method do-detach-medium (sheet :: , medium :: ) => () clear-ink-cache(medium); release-DC(medium); medium-drawable(medium) := #f end method do-detach-medium; define sealed method deallocate-medium (_port :: , medium :: ) => () next-method(); release-DC(medium); medium-drawable(medium) := #f end method deallocate-medium; define sealed method medium-foreground-setter (fg :: , medium :: ) => (foreground :: ) next-method(); // also sets 'medium-drawing-state-cache' appropriately clear-ink-cache(medium); // Force repaint to be done later (#f => don't repaint the background) unless (instance?(medium, )) InvalidateRect(window-handle(medium), $NULL-RECT, #f) end; fg end method medium-foreground-setter; define sealed method medium-background-setter (bg :: , medium :: ) => (background :: ) next-method(); // also sets 'medium-drawing-state-cache' appropriately clear-ink-cache(medium); //---*** Need to create an HBRUSH to be used by the WM_ERASEBKGND message. //---*** Also should remember the color to pass to SetBkColor for text. // Force repaint to be done later (#t => repaint the background) unless (instance?(medium, )) InvalidateRect(window-handle(medium), $NULL-RECT, #t) end; bg end method medium-background-setter; define sealed method medium-clipping-region-setter (region :: , medium :: ) => (region :: ) next-method(); // Don't flush the cache if the region isn't really changing. // This situation comes up all the time during repainting, when we set // the clipping region for every output record, but we almost always // just set it to $everywhere. unless (region == medium-clipping-region(medium)) medium.%clip-mask := #f end; region end method medium-clipping-region-setter; define sealed method invalidate-cached-region (medium :: ) => () medium.%clip-mask := #f; next-method() end method invalidate-cached-region; define sealed method invalidate-cached-transform (medium :: ) => () medium.%clip-mask := #f; next-method() end method invalidate-cached-transform; define sealed method invalidate-cached-drawing-state (medium :: , new-state :: ) => () ignore(new-state); medium.%hFont := #f; when (medium.%hBrush & zero?(logand(new-state, $medium-brush-cached))) // Make sure the DC no longer refers to the cached brush before deleting it let hDC :: = get-DC(medium); SelectObject(hDC, medium.%old-hBrush | $null-hBrush); DeleteObject(medium.%hBrush); medium.%old-hBrush := #f; medium.%hBrush := #f; medium.%cached-brush := #f end; when (medium.%hPen & ( zero?(logand(new-state, $medium-brush-cached)) | zero?(logand(new-state, $medium-pen-cached)))) // Make sure the DC no longer refers to the cached pen before deleting it let hDC :: = get-DC(medium); SelectObject(hDC, medium.%old-hPen | $null-hPen); DeleteObject(medium.%hPen); medium.%old-hPen := #f; medium.%hPen := #f; medium.%cached-pen := #f end end method invalidate-cached-drawing-state; /// Display forcing define sealed method force-display (medium :: ) => () // Although this behavior is not documented anywhere, sometimes the effect // of drawing is not seen on the screen until 'ReleaseDC' is called. release-DC(medium) end method force-display; define sealed method synchronize-display (medium :: ) => () release-DC(medium) end method synchronize-display; /// Drawing state updating define constant $null-region = null-pointer(); // Note that the brush defaults to 'medium-brush(medium)', // but the pen and font default to #f in order to avoid unnecessary work //---*** We need some control over opaque/transparent -- 'SetBkMode' //---*** Shouldn't someone arrange to call 'SetMapMode', too? define sealed method update-drawing-state (medium :: , #key brush, pen, font) => (hDC :: ) let hDC :: = get-DC(medium); let old-cache :: = medium-drawing-state-cache(medium); let new-cache :: = 0; when (old-cache ~= $medium-fully-cached) // Establish a brush, unless it's already cached when (zero?(logand(old-cache, $medium-brush-cached))) let brush = brush | medium-brush(medium); establish-brush(medium, brush, hDC); new-cache := logior(new-cache, $medium-brush-cached) end; // Establish a pen, unless it's already cached // Note that we have to establish a new pen if the brush changed, // because the pen encodes the brush color when ( zero?(logand(old-cache, $medium-brush-cached)) | zero?(logand(old-cache, $medium-pen-cached))) let pen = pen | medium-pen(medium); establish-pen(medium, pen, hDC); new-cache := logior(new-cache, $medium-pen-cached) end; // Establish a font only if requested, unless it's already cached // Note that, even if the font didn't change, we still have to set // the text color if the brush changed when (font & ( zero?(logand(old-cache, $medium-brush-cached)) | zero?(logand(old-cache, $medium-font-cached)))) establish-font(medium, font, hDC); new-cache := logior(new-cache, $medium-font-cached) end; when (~medium.%clip-mask & medium.%hRect) let hrect = medium.%hRect; // If the clip mask is decached, flush the old hRect unless (hRect = $null-region) destroy(hRect) end; medium.%hRect := #f end; unless (medium.%hRect) let mask = compute-clip-mask(medium); if (mask == #"none") medium.%hRect := $null-region else let hRect :: = make(); without-bounds-checks hRect.left-value := mask[0]; hRect.top-value := mask[1]; hRect.right-value := mask[2]; hRect.bottom-value := mask[3] end; medium.%hRect := hRect end; //---*** SelectClipRgn isn't imported yet :-( // SelectClipRgn(hDC, medium.%hRect) new-cache := logior(new-cache, $medium-region-cached) end; medium-drawing-state-cache(medium) := logior(old-cache, new-cache) end; hDC end method update-drawing-state; define constant $rop2-map :: = make(, size: 16); begin $rop2-map[$boole-clr] := $R2-BLACK; $rop2-map[$boole-set] := $R2-WHITE; $rop2-map[$boole-1] := $R2-COPYPEN; $rop2-map[$boole-2] := $R2-NOP; $rop2-map[$boole-c1] := $R2-NOTCOPYPEN; $rop2-map[$boole-c2] := $R2-NOT; $rop2-map[$boole-and] := $R2-MASKPEN; $rop2-map[$boole-ior] := $R2-MERGEPEN; $rop2-map[$boole-xor] := $R2-XORPEN; $rop2-map[$boole-eqv] := $R2-NOTXORPEN; $rop2-map[$boole-nand] := $R2-NOTMASKPEN; $rop2-map[$boole-nor] := $R2-NOTMERGEPEN; $rop2-map[$boole-andc1] := $R2-MASKNOTPEN; $rop2-map[$boole-andc2] := $R2-MASKPENNOT; $rop2-map[$boole-orc1] := $R2-MERGENOTPEN; $rop2-map[$boole-orc2] := $R2-MERGEPENNOT end; define sealed method establish-brush (medium :: , brush :: , hDC :: ) => () let hBrush :: = case brush == medium.%cached-brush => medium.%hBrush; otherwise => let (color :: , fill-style, operation :: , image) = convert-ink-to-DC-components(medium, hDC, brush); //---*** Hack the image //---*** This should set the tile-x/y to (0,0) if the brush is a stipple, or it //---*** should align it to the left/top of the figure if the brush is a pattern //---*** Make use of fill-style by using 'CreateHatchBrush' or //---*** 'CreatePatternBrush' instead of 'CreateSolidBrush' medium.%brush-color := color; medium.%ROP2 := $rop2-map[operation]; check-result("CreateSolidBrush", CreateSolidBrush(color)); end; do-establish-brush(medium, brush, hBrush, hDC) end method establish-brush; define sealed method establish-brush (medium :: , brush :: , hDC :: ) => () let hBrush :: = case brush == medium.%cached-brush => medium.%hBrush; brush == $black => medium.%brush-color := $native-black; medium.%ROP2 := $R2-COPYPEN; $black-hbrush; brush == $white => medium.%brush-color := $native-white; medium.%ROP2 := $R2-COPYPEN; $white-hbrush; otherwise => let color = color->native-color(brush, medium); medium.%brush-color := color; medium.%ROP2 := $R2-COPYPEN; check-result("CreateSolidBrush", CreateSolidBrush(color)); end; do-establish-brush(medium, brush, hBrush, hDC) end method establish-brush; define sealed method establish-brush (medium :: , brush :: , hDC :: ) => () establish-brush(medium, contrasting-color->color(brush), hDC) end method establish-brush; define method do-establish-brush (medium :: , brush, hBrush :: , hDC :: ) => () when (medium.%hBrush ~= hBrush) medium.%cached-brush := brush; medium.%hBrush := hBrush end; let old-hBrush = SelectObject(hDC, hBrush); check-result("SelectObject brush", old-hBrush); if (medium.%old-hBrush == #f) // The old brush was not selected by a call to 'establish-brush', // so we will need to put it back later medium.%old-hBrush := pointer-cast(, old-hBrush) else // The old brush was selected by a call to 'establish-brush', // so we can delete it now DeleteObject(medium.%old-hBrush) end; SetROP2(hDC, medium.%ROP2); //---*** Is this always the right thing? SetBkMode(hDC, $TRANSPARENT) end method do-establish-brush; define sealed method establish-brush (medium :: , brush :: , hDC :: ) => () establish-brush(medium, medium-foreground(medium), hDC) end method establish-brush; define sealed method establish-brush (medium :: , brush :: , hDC :: ) => () establish-brush(medium, medium-background(medium), hDC) end method establish-brush; define sealed method establish-pen (medium :: , pen :: , hDC :: ) => (); let hPen :: = case pen == medium.%cached-pen => medium.%hPen; otherwise => let width = pen-width(pen); let style = select (pen-dashes(pen)) #f => $PS-SOLID; #t => $PS-DASH; pen-dashes($dotted-pen) => $PS-DOT; pen-dashes($dash-dot-pen) => $PS-DASHDOT; pen-dashes($dash-dot-dot-pen) => $PS-DASHDOTDOT; otherwise => $PS-DASHDOT; end; // Note well! Depends on the brush having been established first! check-result("CreatePen", CreatePen(style, width, medium.%brush-color)); end; when (medium.%hPen ~= hPen) medium.%cached-pen := pen; medium.%hPen := hPen end; let old-hPen = SelectObject(hDC, hPen); check-result("SelectObject pen", old-hPen); if (medium.%old-hPen == #f) // The old pen was not selected by a call to 'establish-pen', // so we will need to put it back later medium.%old-hPen := pointer-cast(, old-hPen) else // The old pen was selected by a call to 'establish-pen', // so we can delete it now DeleteObject(medium.%old-hPen) end end method establish-pen; define sealed method establish-font (medium :: , font :: , hDC :: ) => () let hFont = font.%font-handle; unless (hFont = medium.%hFont) when (SetTextColor(hDC, medium.%brush-color) = $CLR-INVALID) report-error("SetTextColor") end; check-result("SelectObject font", SelectObject(hDC, hFont)); medium.%hFont := hFont end end method establish-font; define generic convert-ink-to-DC-components (medium :: , hDC :: , brush) => (color :: , fill-style, operation :: , image :: false-or()); define sealed method convert-ink-to-DC-components (medium :: , hDC :: , brush :: ) => (color :: , fill-style, operation :: , image :: false-or()) convert-ink-to-DC-components(medium, hDC, medium-foreground(medium)) end method convert-ink-to-DC-components; define sealed method convert-ink-to-DC-components (medium :: , hDC :: , brush :: ) => (color :: , fill-style, operation :: , image :: false-or()) convert-ink-to-DC-components(medium, hDC, medium-background(medium)) end method convert-ink-to-DC-components; define sealed method convert-ink-to-DC-components (medium :: , hDC :: , brush :: ) => (color :: , fill-style, operation :: , image :: false-or()) values(color->native-color(brush, medium), #"solid", $boole-1, #f) end method convert-ink-to-DC-components; define sealed method convert-ink-to-DC-components (medium :: , hDC :: , brush :: ) => (color :: , fill-style, operation :: , image :: false-or()) values(color->native-color(contrasting-color->color(brush), medium), #"solid", $boole-1, #f) end method convert-ink-to-DC-components; define sealed method convert-ink-to-DC-components (medium :: , hDC :: , brush :: ) => (color :: , fill-style, operation :: , image :: false-or()) let cache = medium.%ink-cache; let bitmap :: false-or() = gethash(cache, brush) | begin let (array, colors) = decode-pattern(brush); let width :: = image-width(brush); let height :: = image-height(brush); let ncolors :: = size(colors); //---*** Should we create a DIB here or what? let pixels :: = make(, size: ncolors); let pixarray :: = make(, dimensions: list(width, height)); without-bounds-checks for (n :: from 0 below ncolors) let pixel = convert-ink-to-DC-components(medium, hDC, colors[n]); pixels[n] := pixel end; for (y :: from 0 below height) for (x :: from 0 below width) pixarray[y,x] := pixels[array[y,x]] end end end; //---*** Fill in the DIB from 'pixels' and 'pixarray' let bitmap = make(, width: width, height: height, handle: #f); gethash(cache, brush) := bitmap; bitmap end; values($native-white, #"solid", $boole-1, bitmap) end method convert-ink-to-DC-components; define sealed method convert-ink-to-DC-components (medium :: , hDC :: , brush :: ) => (color :: , fill-style, operation :: , image :: false-or()) //---*** Implement it! not-yet-implemented("convert-ink-to-dc-components for ") end method convert-ink-to-DC-components; define sealed method convert-ink-to-DC-components (medium :: , hDC :: , brush :: ) => (color :: , fill-style, operation :: , image :: false-or()) let (color :: , fill-style, operation :: , pattern) = convert-ink-to-DC-components (medium, hDC, brush-tile(brush) | brush-stipple(brush) | brush-foreground(brush)); // ignore(operation); values(color, fill-style, brush-mode(brush), pattern) end method convert-ink-to-DC-components; // Compute and cache the clip rectangle define sealed method compute-clip-mask (medium :: ) => (mask) medium.%clip-mask | (medium.%clip-mask := begin let sheet = medium-sheet(medium); let sregion = sheet-device-region(sheet); let mregion = medium-clipping-region(medium); let valid? = #t; if (sregion == $nowhere | mregion == $nowhere) #"none" else let (sleft, stop, sright, sbottom) = box-edges(sregion); unless (mregion == $everywhere) let (mleft, mtop, mright, mbottom) = box-edges(mregion); let (mleft, mtop, mright, mbottom) = transform-box(sheet-device-transform(sheet), mleft, mtop, mright, mbottom); let (v, left, top, right, bottom) = ltrb-intersects-ltrb?(sleft, stop, sright, sbottom, mleft, mtop, mright, mbottom); sleft := left; stop := top; sright := right; sbottom := bottom; valid? := v end; if (valid?) vector(sleft, stop, sright, sbottom) else #"none" end end end) end method compute-clip-mask;